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:
- If
causeis present, the answer is UNKNOWN. - Otherwise, if
decisionequals the policy'syes_is, the answer is YES. - 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.