Docs/Concepts/Verdicts

Verdicts

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

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. Telling an attacker that would help them tune around it. 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:

// 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.
}
// 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:

// iOS
let ok = verdict.result == .yes && verdict.confidence.overallScore > 0.8
// 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