Verdicts
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.NOdoes not mean "I don't know."INDETERMINATE. The SDK cannot answer. Thereasonfield says why, and theproofisnil.
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
- Time Semantics for how
atTime,validity, and the staleness window interact. - Regions for why
NO_PROOF_AT_RESOLUTIONcan fire even when you "have" a proof. - OctetVerdict API Reference for the exact field types.