# 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 nine distinct reasons:

- 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.
- App Attest or Play Integrity returned a non-compliant verdict.
- The session ended.
- The predicate path is not wired in this SDK build yet.

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` | Play Integrity / App Attest verdict not COMPLIANT. | `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 wired in this SDK build. | `INDETERMINATE` |

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 **never silently down-levels** 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, and make the threshold explicit:

```swift
// iOS
let ok = verdict.result == .yes && verdict.confidence.overallScore > 0.8
```

```kotlin
// Android
val ok = verdict.result == OctetVerdict.Result.YES &&
         verdict.confidence.overallScore > 0.8
```

A low-confidence `YES` is still a valid proof. Some domains (KYC, sanctions, payments) need a tighter bar. The SDK does not set that bar. The confidence score is on the verdict so you can set it yourself.

## Where to go next

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