# 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`.

:::

## What to do with each result

| You get | What it means | What your app does |
|---|---|---|
| `YES` | The predicate holds. For `isWithin`, the device is inside the region. A proof is attached. | Act on it. Send the proof to your backend if it checks proofs. |
| `NO` | The predicate does not hold. For `isWithin`, the device is outside the region. A proof is attached. | Act on it. Keep the proof if you need a record of the answer. |
| `INDETERMINATE` with an adversarial reason | A forgery or compromise was detected: `SPOOFING_DETECTED`, `MOCK_LOCATION_DETECTED`, `TAMPERING` or `ATTESTATION_FAILED`. No proof. | Deny. |
| `INDETERMINATE` with a benign reason | The SDK could not measure, for example `NO_FIX` indoors. No proof. | Ask again, or permit with a step-up. |
| `INDETERMINATE` with any other reason | The calling code, the SDK build or your license is the problem, for example `SDK_NOT_RUNNING`. With `PERMISSION_REQUIRED`, your app hasn't been granted location. No proof. | Fix the call, or ask for the permission. Don't step up the user. |

Every reason is listed under [Reason codes](#reason-codes), and [Adversarial and benign](#adversarial-and-benign) explains the split. Here is the same table in code, for a check that gates a trade:

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
switch v.result {
case .yes:
    allowTrade(proofBytes: v.proof!.proofBytes)
case .no:
    blockTrade()
case .indeterminate:
    switch v.reason {
    case .spoofingDetected, .mockLocationDetected,
         .tampering, .attestationFailed:
        blockTrade()
    case .sdkNotRunning, .notYetReleased, .invalidSessionNonce,
         .invalidDecisionRef, .invalidDecisionTime,
         .invalidCallerContent, .callerContentNotEnabled, .licenseInactive:
        reportBug(v.reason)
    case .permissionRequired:
        askForLocationPermission()
    default:
        // Benign reasons, such as NO_FIX
        askAgainOrStepUp()
    }
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
when (v.result) {
    OctetVerdict.Result.YES -> allowTrade(v.proof!!.proofBytes)
    OctetVerdict.Result.NO -> blockTrade()
    OctetVerdict.Result.INDETERMINATE -> when (v.reason) {
        OctetVerdict.ReasonCode.SPOOFING_DETECTED,
        OctetVerdict.ReasonCode.MOCK_LOCATION_DETECTED,
        OctetVerdict.ReasonCode.TAMPERING,
        OctetVerdict.ReasonCode.ATTESTATION_FAILED ->
            blockTrade()
        OctetVerdict.ReasonCode.SDK_NOT_RUNNING,
        OctetVerdict.ReasonCode.NOT_YET_RELEASED,
        OctetVerdict.ReasonCode.INVALID_SESSION_NONCE,
        OctetVerdict.ReasonCode.INVALID_DECISION_REF,
        OctetVerdict.ReasonCode.INVALID_DECISION_TIME,
        OctetVerdict.ReasonCode.INVALID_CALLER_CONTENT,
        OctetVerdict.ReasonCode.CALLER_CONTENT_NOT_ENABLED,
        OctetVerdict.ReasonCode.LICENSE_INACTIVE ->
            reportBug(v.reason)
        OctetVerdict.ReasonCode.PERMISSION_REQUIRED ->
            askForLocationPermission()
        // Benign reasons, such as NO_FIX
        else -> askAgainOrStepUp()
    }
}
```

  </TabItem>
</Tabs>

`allowTrade`, `blockTrade`, `reportBug`, `askForLocationPermission` and `askAgainOrStepUp` stand for your own code. A reason in the third group means the code calling the SDK needs fixing, so report it to your developers rather than asking the user to try again.

## 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, for example a city query when the user allows only approximate location. `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` |
| `INVALID_DECISION_TIME` | A query with `decisionRef` set was pinned to a past `atTime`. Added in 2.0. | `INDETERMINATE` |
| `INVALID_CALLER_CONTENT` | Supplied `content` breaks a rule, or a file can't be read. Added in 3.1. | `INDETERMINATE` |
| `CALLER_CONTENT_NOT_ENABLED` | Supplied `content`, and caller content isn't enabled for your license. Added in 3.1. | `INDETERMINATE` |
| `PERMISSION_REQUIRED` | A permission the SDK needs, such as location, isn't granted. The SDK doesn't ask for it: request it in your app and try again. Added in 2.0. | `INDETERMINATE` |
| `LICENSE_INACTIVE` | Your license was withdrawn or expired while the session was running. `licenseStatus.state` says which. Added in 3.1. | `INDETERMINATE` |
| `SPOOFING_DETECTED` | The spoof pipeline flagged the fix. Adversarial. Added in 2.0. | `INDETERMINATE` |
| `TAMPERING` | Device-integrity tampering (root / hook), or an attached debugger or native tracer outside a sandbox session. Adversarial. Added in 2.0. | `INDETERMINATE` |
| `REGION_UNRESOLVED` | A country or subdivision predicate could not resolve the region at the required confidence. Since 3.0, also a pinned `claimedRegion` that is not an assigned ISO 3166 code. Benign. Added in 2.0. | `INDETERMINATE` |

### Adversarial and benign

The measurement-related `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`, `NO_PROOF_AT_RESOLUTION`. A policy layer can permit these with a step-up. `NO_PROOF_AT_RESOLUTION` means the proof on hand is too coarse for the question: ask again, or ask at the level the proof covers. Both classes carry no proof. The distinction is in the reason.

The remaining reasons are not detections. `SDK_NOT_RUNNING`, `NOT_YET_RELEASED`, `INVALID_SESSION_NONCE`, `INVALID_DECISION_REF`, `INVALID_DECISION_TIME`, `INVALID_CALLER_CONTENT` and `CALLER_CONTENT_NOT_ENABLED` point to the calling code or the SDK build, so fix the call rather than stepping up the user. `PERMISSION_REQUIRED` means your app must ask for the permission. `LICENSE_INACTIVE` points to your license, not to the user or the device.

The `reason` code names the category that blocked the proof (for example `ATTESTATION_FAILED` or `MOCK_LOCATION_DETECTED`). The SDK does not surface the sensor-level detail beneath it (which signal fired, or how), because that detail would tell an attacker what to defeat. On a simulator or emulator the SDK returns `NO_FIX`. That reflects the development environment, not a detected attack.

## Assurance tier

A `YES` or `NO` carries a proof, and the proof carries an assurance tier, `spoofingVerdict`: `VERIFIED` when independent evidence supports the fix, `PLAUSIBLE` when the fix is consistent with the query but not independently corroborated. Since 3.1 `spoofingVerdict` is `UNSPECIFIED` by default, and the answer is the result. A proof from approximate location is never `VERIFIED`. See [`OctetVerdict`](/docs/mobile/api-reference/octet-verdict/#the-assurance-tier).

## Achievable level

When the result is `INDETERMINATE` with reason `INSUFFICIENT_PRECISION`, the
verdict carries an `achievableLevel`: the best precision the SDK can reach for
this query right now. The SDK does not answer a coarser question than the one
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 acceptance rule than any `YES`. The SDK does not apply one. Base yours on the result and the reason.

## Where to go next

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