# Verdicts

:::note[In one sentence]

A verdict is one of three values, `YES`, `NO`, or `INDETERMINATE`, where the third lets the SDK report that it cannot answer instead of falsely returning `NO`.

:::

## Why not a boolean

A boolean would collapse two different outcomes: the device was not in the region, and the SDK could not tell. The SDK cannot tell for a range of distinct reasons, and the `reason` code says which. The main cases:

- It has started but has not seen a usable fix yet.
- The query is about a moment in the future.
- The query is about a moment too old to have a cached proof.
- The cached proof is too coarse for the question (for example, a country-level proof cannot answer a city-level question).
- Conditions cannot support a proof at the precision you asked for.
- The OS flagged the location as mocked.
- The device's platform attestation did not pass.
- The session ended.
- The predicate path is not available in this SDK build.
- The supplied session nonce was empty or larger than 512 bytes.

Collapsing these into `NO` would hide the difference between *answered no* and *could not answer*. Code that retries on failure, or forwards a `NO` as if it were a proof, would then do the wrong thing. The three values keep the two cases separate.

## The three values

```mermaid
flowchart TD
    Q[Predicate query] --> E{Can the SDK<br/>evaluate it?}
    E -->|Yes, condition holds| YES[YES + proof]
    E -->|Yes, condition does not hold| NO[NO + proof]
    E -->|No| IND[INDETERMINATE<br/>+ ReasonCode]
```

- **`YES`**. The predicate holds, and a proof is attached.
- **`NO`**. The predicate does *not* hold, and a proof of the negative is attached. `NO` does not mean "I don't know."
- **`INDETERMINATE`**. The SDK cannot answer. The `reason` field says why, and the `proof` is `nil`.

## Reason codes

| `ReasonCode` | When | Result |
|---|---|---|
| `OK` | Proof covers `atTime`. Predicate evaluated cleanly. | `YES` or `NO` |
| `NO_FIX` | SDK started but no fix yet. | `INDETERMINATE` |
| `FUTURE_TIME` | `atTime` is in the future beyond ±2 s clock-skew tolerance. | `INDETERMINATE` |
| `STALE_FIX` | `atTime` falls outside the validity window of any cached proof. | `INDETERMINATE` |
| `NO_PROOF_AT_RESOLUTION` | Cached proof is too coarse for the query (for example, country-level proof vs. city-level question). | `INDETERMINATE` |
| `INSUFFICIENT_PRECISION` | Conditions cannot support a proof at the requested precision. `achievableLevel` names the best level reachable. | `INDETERMINATE` |
| `ATTESTATION_FAILED` | Device attestation did not pass. | `INDETERMINATE` |
| `MOCK_LOCATION_DETECTED` | OS flagged mocked location for this fix. | `INDETERMINATE` |
| `SDK_NOT_RUNNING` | `Octet.start(...)` was never called or session ended. | `INDETERMINATE` |
| `NOT_YET_RELEASED` | Predicate path declared in the API but not available in this SDK build. | `INDETERMINATE` |
| `INVALID_SESSION_NONCE` | Supplied `sessionNonce` is empty or exceeds 512 bytes. Added in 1.2. | `INDETERMINATE` |
| `INVALID_DECISION_REF` | Supplied `decisionRef` exceeds 256 characters. Added in 2.0. | `INDETERMINATE` |
| `SPOOFING_DETECTED` | The spoof pipeline flagged the fix. Adversarial. Added in 2.0. | `INDETERMINATE` |
| `TAMPERING` | Device-integrity tampering (root / hook). Adversarial. Added in 2.0. | `INDETERMINATE` |
| `REGION_UNRESOLVED` | A country or subdivision predicate could not resolve the region at the required confidence. Benign. Added in 2.0. | `INDETERMINATE` |

### Adversarial and benign

The `INDETERMINATE` reasons split into two classes so a policy layer can treat them differently. **Adversarial** (a detected forgery or compromise): `SPOOFING_DETECTED`, `MOCK_LOCATION_DETECTED`, `TAMPERING`, `ATTESTATION_FAILED`. Deny. **Benign** (the SDK could not measure): `REGION_UNRESOLVED`, `INSUFFICIENT_PRECISION`, `NO_FIX`, `STALE_FIX`, `FUTURE_TIME`. A policy layer can permit these with a step-up. Both classes carry no proof. The distinction is in the reason.

The `reason` code names the category that blocked the proof (for example `ATTESTATION_FAILED` or `MOCK_LOCATION_DETECTED`). What the SDK does not surface is the sensor-level detail beneath it: which signal fired, or how. Exposing that is a security risk: it tells an attacker exactly what to defeat. The emulator / simulator hint is a separate developer-environment indicator, not a security one.

## Achievable level

When the result is `INDETERMINATE` with reason `INSUFFICIENT_PRECISION`, the
verdict carries an `achievableLevel`: the best precision the SDK could actually
reach right now. The SDK does not down-level silently on your behalf. It tells
you what it can prove and lets you decide, rather than quietly answering a coarser
question than you asked.

Read it to choose between re-requesting at the coarser level or applying your own
fallback:

```swift
// iOS
if verdict.reason == .insufficientPrecision, let level = verdict.achievableLevel {
    // e.g. you asked for city, the SDK can prove country right now.
    // Re-request at `level`, or treat as not-good-enough for your use case.
}
```

```kotlin
// Android
if (verdict.reason == VerdictReason.INSUFFICIENT_PRECISION) {
    val level = verdict.achievableLevel  // best level reachable now, or null
}
```

## Strict-boolean callers

If your logic needs a boolean, collapse the verdict yourself. Only a `YES` maps to `true`. `NO` and `INDETERMINATE` both map to `false`, so read `reason` first if you need to tell them apart:

```swift
// iOS
let ok = verdict.result == .yes
```

```kotlin
// Android
val ok = verdict.result == OctetVerdict.Result.YES
```

Some domains (KYC, sanctions, payments) need a stricter bar than a `YES`, for example also requiring the proof to pass [verification](/docs/v2.0/concepts/verifying-proofs/) before you accept it. The SDK does not set that bar for you.

## Where to go next

- [Time Semantics](/docs/v2.0/concepts/time-semantics/) for how `atTime`, `validity`, and the staleness window interact.
- [Regions](/docs/v2.0/concepts/regions/) for why `NO_PROOF_AT_RESOLUTION` can fire even when you "have" a proof.
- [OctetVerdict API Reference](/docs/v2.0/api-reference/octet-verdict/) for the exact field types.
