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.
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, and Adversarial and benign explains the split. Here is the same table in code, for a check that gates a trade:
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()
}
}
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()
}
}
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
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, 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.
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:
// 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. Only a YES maps to true. NO and INDETERMINATE both map to false, so read reason first if you need to tell them apart.
// iOS
let ok = verdict.result == .yes
// 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 for how
atTime,validity, and the staleness window interact. - Regions for why
NO_PROOF_AT_RESOLUTIONcan occur when a cached proof exists. - OctetVerdict API Reference for the exact field types.