Docs/Magistrate/Reference/Decision Response

Decision Response

This page describes what the Octet policy engine returns for a decision, and the signed receipt that carries it to your backend.

DecisionResult

decide in the Magistrate module returns a DecisionResult:

Field Type (Swift / Kotlin) Description
decision String permit or deny
cause String? Set when the location couldn't be settled, which is UNKNOWN. See Cause. nil or null when the location was decided
env String prod, or sandbox for a simulator, emulator or debug build
isPermit Bool / Boolean true only when decision is permit
obligations [PolicyObligation] / List<PolicyObligation> Conditions attached to a permit. See Obligations
reasons [String] / List<String> Why the engine decided as it did, in plain words. For logs and support, not for program logic
policyVersion UInt64 / ULong The version of the policy that decided
proofRoot String A commitment to the proofs the decision was made on
decisionRef String The reference of this check
receipt String? The signed receipt. Send it to your backend
claimedArea optional The country or state the device disclosed in its proof. Never coordinates

decideMany returns a DecisionSet with the shared decisionRef and claimedArea, and one result per policy in results. Each carries its policy name, its own cause, env and receipt.

Receipt claims

The receipt is a compact JWS. Its header is {"alg": "EdDSA", "typ": "octet-decision-receipt+jwt", "kid": "<key id>"}. Its claims:

Claim Description
iss Always octet-policy-engine
iat, exp Issued at and expiry, Unix seconds. exp is 300 seconds after iat
decision permit or deny
cause Present only when the decision came from on_indeterminate. See Cause
obligations The obligations attached to the decision
policy, policy_version The policy and the version that decided
decision_ref The reference of the check this receipt answers
license_id, account_id Your license, and the account you passed to decide, if any
proof_root A commitment to the proofs the decision was made on
seq The engine's sequence number for this decision
check_policies For decideMany, every policy in the check
env prod, or sandbox for a simulator, emulator or debug build. A receipt without env is prod

Verify and Enforce shows how to check a receipt.

YES, NO and UNKNOWN

The studio shows each policy as a question with three answers. The engine returns permit or deny. Read a result, or a verified receipt, in this order:

  1. If cause is present, the answer is UNKNOWN.
  2. Otherwise, if decision equals the policy's yes_is, the answer is YES.
  3. Otherwise, the answer is NO.

yes_is depends on how the question is asked. For "Is the device in Austria?" it is permit. For "Is the device in Iran?" it is deny. The studio's Ship page shows your policy's question and which decision is YES. GET /v1/policies lists question and yes_is for Octet's demo policies.

Cause

cause says why the location couldn't be settled:

Cause Meaning
absence No proof for something the policy needs. This includes a device whose location the SDK didn't trust, because the SDK then sends no proof
stale The proofs were older than the policy's freshness
authenticity A proof didn't verify
unbound A proof wasn't bound to this check's challenge
inconsistent A proof contradicts the engine's own check, or the proofs disagree on the country
undecidable The proofs are valid but can't settle a rule, for example a country-level proof for a state rule
other Something other than a region couldn't be settled, such as a requires threshold

Obligations

A permit can carry obligations. A permit with obligations counts only once your app and backend have fulfilled every one. The Magistrate module returns them as they are and never acts on them.

Swift Kotlin In the receipt Means
.stepUpAuth StepUpAuth "StepUpAuth" Authenticate the user again before going ahead
.log(channel:) Log {"Log": {"channel": "audit"}} Record the decision in the named log
.degrade(capability:) Degrade {"Degrade": {"capability": "hd_video"}} Go ahead without the named capability
.ttl(durationMs:) Ttl {"Ttl": {"duration": {"ms": 300000}}} The permit lasts only this long
.custom(name:args:) Custom {"Custom": {"name": "x-…", "args": []}} A rule of your own, named in the policy
.unknown(rawJson:) Unknown anything else Treat it as not fulfilled

Policies from the Magistrate studio don't attach obligations yet. Handle them anyway, so a later policy version can't widen what a permit allows.

TermsPrivacy© 2026 Understone, Inc. d/b/a Octet