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? |
release, 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 |
ClaimedArea? |
The area the device disclosed in its proof: a country, a subdivision such as a state, or only "Earth". Never coordinates |
decideMany returns a DecisionSet with the shared decisionRef and claimedArea, and one PolicyDecision per policy in results. Each carries its policy name, its own decision, cause, env and receipt. forPolicy looks one up by name.
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 |
release, or sandbox for a simulator, emulator or debug build |
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/bundle?policy=<policy> returns question and yes_is beside the signed bundle. These two fields aren't signed. GET /v1/policies lists them for Octet's demo policies. Both are null for a policy that wasn't written in the studio's shape. Read decision directly for those.
Whether YES is good news depends on the question, so map each answer to your own action, such as allow, block or step up, per policy.
Cause
cause says why the location couldn't be settled:
| Cause | Meaning |
|---|---|
absence |
No proof for something the policy needs, often because the device had no location fix yet. Try again. 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 |
Treat a cause you don't know as other.
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.