import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# `OctetVerdict`

The outcome of a predicate call. Returned by `isWithin`, `isOutside`, `contains` on `OctetSdk.loc`.

## Shape

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

```swift
public struct OctetVerdict: Sendable {
    public let result: Result
    public let reason: ReasonCode
    public let message: String
    public let proof: LocationProof?
    public let validity: Interval?
    public let queriedAt: Date
    public let confidence: ConfidenceSummary
    public let achievableLevel: ProofLevel?   // non-nil only when reason == .insufficientPrecision

    public enum Result: Sendable, Hashable {
        case yes, no, indeterminate
    }

    public enum ReasonCode: Sendable, Hashable {
        case ok
        case noFix
        case futureTime
        case staleFix
        case noProofAtResolution
        case insufficientPrecision
        case attestationFailed
        case mockLocationDetected
        case sdkNotRunning
        case notYetReleased
        case invalidSessionNonce
        case invalidDecisionRef
        case invalidDecisionTime
        case spoofingDetected
        case tampering
        case regionUnresolved
        case permissionRequired
        case invalidCallerContent
        case callerContentNotEnabled
        case licenseInactive
    }
}
```

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

```kotlin
data class OctetVerdict(
    val result: Result,
    val reason: ReasonCode,
    val message: String,
    val proof: LocationProof?,
    val validity: Interval?,
    val queriedAt: Instant,
    val confidence: ConfidenceSummary,
    val achievableLevel: ProofLevel? = null,  // non-null only when reason == INSUFFICIENT_PRECISION
) {
    enum class Result { YES, NO, INDETERMINATE }

    enum class ReasonCode {
        OK, NO_FIX, FUTURE_TIME, STALE_FIX, NO_PROOF_AT_RESOLUTION,
        INSUFFICIENT_PRECISION, ATTESTATION_FAILED, MOCK_LOCATION_DETECTED,
        SDK_NOT_RUNNING, NOT_YET_RELEASED, INVALID_SESSION_NONCE,
        INVALID_DECISION_REF, INVALID_DECISION_TIME, SPOOFING_DETECTED, TAMPERING, REGION_UNRESOLVED,
        PERMISSION_REQUIRED, INVALID_CALLER_CONTENT, CALLER_CONTENT_NOT_ENABLED, LICENSE_INACTIVE,
    }
}
```

  </TabItem>
</Tabs>

## Field invariants

- **`proof` is `nil`/`null` iff `result == .indeterminate`/`INDETERMINATE`.** Enforced as a precondition / `require` at construction. A `YES` or `NO` always carries a proof. An `INDETERMINATE` never does.
- **`validity` is non-nil exactly when `proof` is non-nil.** Both move together.
- **`queriedAt` is the `atTime` that was passed in.** Echoed back so callers do not have to thread it themselves.
- **`confidence` is always populated**, even for `INDETERMINATE` (with an empty summary in that case). Since 3.1 it holds neutral values. See [`confidence`](#confidence).
- **`achievableLevel` is non-nil only when `reason == .insufficientPrecision`.** It names the best [proof level](/docs/mobile/concepts/regions/) the SDK could reach for this query right now. New in 1.1. The SDK never falls back to this level automatically.

Near a border between two subdivisions, a device whose accuracy area crosses that border can only be proven at the country level. A subdivision query returns `insufficientPrecision` with `achievableLevel` `country`, and a policy that accepts only one of the two subdivisions sees the device as unknown, not as inside or outside. See [Regions](/docs/mobile/concepts/regions/#near-a-subdivision-border).

## `result`

| Value | Meaning |
|---|---|
| `.yes` / `YES` | Predicate holds. Proof attached. |
| `.no` / `NO` | Predicate provably does not hold. Proof attached. |
| `.indeterminate` / `INDETERMINATE` | SDK cannot answer. See `reason`. |

Never collapse `.indeterminate` to `.no` silently. See [Verdicts](/docs/mobile/concepts/verdicts/) for the rationale.

## `reason`

| Code | Result | Meaning |
|---|---|---|
| `ok` / `OK` | `YES` or `NO` | Proof covered `atTime`. Predicate evaluated cleanly. |
| `noFix` / `NO_FIX` | `INDETERMINATE` | SDK started but no fix yet (also: running on simulator / emulator). |
| `futureTime` / `FUTURE_TIME` | `INDETERMINATE` | `atTime` is in the future beyond ±2 s clock-skew tolerance. |
| `staleFix` / `STALE_FIX` | `INDETERMINATE` | `atTime` falls outside the validity window of any cached proof. |
| `noProofAtResolution` / `NO_PROOF_AT_RESOLUTION` | `INDETERMINATE` | Cached proof is too coarse for the query (country proof, city query). |
| `insufficientPrecision` / `INSUFFICIENT_PRECISION` | `INDETERMINATE` | Conditions cannot support the requested precision. Read `achievableLevel`. |
| `attestationFailed` / `ATTESTATION_FAILED` | `INDETERMINATE` | Device attestation did not pass. |
| `mockLocationDetected` / `MOCK_LOCATION_DETECTED` | `INDETERMINATE` | OS flagged mocked location. |
| `sdkNotRunning` / `SDK_NOT_RUNNING` | `INDETERMINATE` | `Octet.start(...)` was never called or session ended. |
| `notYetReleased` / `NOT_YET_RELEASED` | `INDETERMINATE` | Predicate path declared by the API but not available in this SDK build. |
| `invalidSessionNonce` / `INVALID_SESSION_NONCE` | `INDETERMINATE` | Supplied `sessionNonce` is empty or exceeds 512 bytes. No proof is produced. Added in 1.2. |
| `invalidDecisionRef` / `INVALID_DECISION_REF` | `INDETERMINATE` | Supplied `decisionRef` exceeds 256 characters. No proof, no network call. Added in 2.0. |
| `invalidDecisionTime` / `INVALID_DECISION_TIME` | `INDETERMINATE` | A query with `decisionRef` set was pinned to a past `atTime`. A decision ref asks for a fresh proof, so it can't answer a past moment. No proof, no network call. Leave out `decisionRef` to ask about a past time. Added in 2.0. |
| `invalidCallerContent` / `INVALID_CALLER_CONTENT` | `INDETERMINATE` | Supplied `content` breaks a rule (see [Content](/docs/mobile/api-reference/predicates/#content)), or a file can't be read or is over 64 MiB. No proof is produced. `message` names the rule. Added in 3.1. |
| `callerContentNotEnabled` / `CALLER_CONTENT_NOT_ENABLED` | `INDETERMINATE` | You passed `content`, and caller content isn't enabled for your license. No proof is produced. Added in 3.1. |
| `permissionRequired` / `PERMISSION_REQUIRED` | `INDETERMINATE` | 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. |
| `licenseInactive` / `LICENSE_INACTIVE` | `INDETERMINATE` | Your license was withdrawn or expired while the session was running. No proof is produced. `licenseStatus.state` says which: `revoked` or `expired`. Added in 3.1. |
| `spoofingDetected` / `SPOOFING_DETECTED` | `INDETERMINATE` | The spoof pipeline flagged the fix. Adversarial. Added in 2.0. |
| `tampering` / `TAMPERING` | `INDETERMINATE` | Device-integrity tampering (root / hook). Also an attached debugger or native tracer outside a sandbox session. Android refuses these since 3.0, as iOS already did. Adversarial. Added in 2.0. |
| `regionUnresolved` / `REGION_UNRESOLVED` | `INDETERMINATE` | 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. A benign measurement gap, not a detected spoof. Added in 2.0. |

### Adversarial and benign reasons

Added in 2.0, the measurement-related `INDETERMINATE` reasons split into two classes so a policy layer can treat them differently. The remaining reasons come from the calling code or the SDK build:

- **Adversarial** (a detected forgery or compromise): `spoofingDetected`, `mockLocationDetected`, `tampering`, `attestationFailed`. A policy layer should deny these.
- **Benign** (the SDK could not measure): `regionUnresolved`, `insufficientPrecision`, `noFix`, `staleFix`, `futureTime`, `noProofAtResolution`. A policy layer can permit these with a step-up. `noProofAtResolution` means the proof on hand is too coarse for the question: ask again, or ask at the level the proof covers.
- **Caller or availability** (fix the call, not the device): `sdkNotRunning`, `notYetReleased`, `invalidSessionNonce`, `invalidDecisionRef`, `invalidDecisionTime`, `invalidCallerContent`, `callerContentNotEnabled`. None of these is adversarial. Fix the calling code rather than stepping up the user.
- **Permission and license:** `permissionRequired` means your app must ask for the permission. `licenseInactive` points to your license, not to the user or the device.

Both classes carry no proof. The distinction is in the reason, not the result.

## `message`

`message` is a free-form string for log lines and debug UI. Its wording can change between SDK versions. Use `reason` for control flow, not `message`.

## `proof`

`LocationProof` is the signed protobuf that a verifier checks against Octet's public keys. Its full shape is part of the protocol, not the SDK's public surface. See [Serialization](/docs/mobile/api-reference/serialization/) for the v1 alpha caveat about how `LocationProof` is currently rendered through `.toJson()`.

For most integrators the proof is opaque. Forward it to your verifier as-is.

### The assurance tier

`LocationProof.spoofingVerdict` is the proof's assurance tier. Since 3.1 it is `UNSPECIFIED`, and the answer is `result`. If your license includes the tier, it reads `VERIFIED` (independent evidence supports the fix) or `PLAUSIBLE` (the fix is consistent with your query but not independently corroborated), also as `spoofing_verdict` in the `proof` JSON. A disc query (`contains(...)` or a disc `isWithin(...)`) applies its tier rule either way. See [Predicates](/docs/mobile/api-reference/predicates/#plausible-containment).

## `validity`

The interval over which the underlying proof answers the predicate as stated. It does not predict how long the verdict will stay true. See [Time Semantics](/docs/mobile/concepts/time-semantics/).

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

```swift
public struct Interval: Sendable, Hashable {
    public let from: Date
    public let to: Date
}
```

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

```kotlin
data class Interval(val from: Instant, val to: Instant)
```

  </TabItem>
</Tabs>

Inclusive-inclusive, `from <= to`.

## `confidence`

`ConfidenceSummary` is always present. Since 3.1 it holds neutral values (a score of `0` and empty lists), and `.toStr()` prints no `conf=`. Act on `result` and `reason`, with no threshold to pick. `overallScore`, `spoofingRiskScore`, `sensorContributions` and `flags` are deprecated and are removed in 4.0.

If you need more device intelligence or your own scoring logic, Octet offers it as a custom feature or license.

## See also

- [Verdicts](/docs/mobile/concepts/verdicts/). Why the trichotomy matters.
- [Time Semantics](/docs/mobile/concepts/time-semantics/). How `validity` and `queriedAt` interact.
- [Serialization](/docs/mobile/api-reference/serialization/). `.toStr()` / `.toJson()` / `.toJsonl()` on verdicts.
