Reason codes
Seventeen producible codes, six agent-profile codes and two reserved ones explain every accept and reject, byte-identical in the Python SDK, the HTTP contract and every client, and fixed within a contract major.
Producible codes
Every decision carries exactly one of these codes. Changing the list is a major contract version that reaches every language at once, so within a contract major your switch never meets a code it does not know.
| Code | Condition | Meaning | Typical handling |
|---|---|---|---|
accepted | — | all nine conditions passed; verified is present | proceed |
request_malformed | precondition | the challenge body violates the signed-challenge shape (e.g. empty claim policy) | fix challenge construction |
verifier_not_active | 1 | your verifier registration is not ACTIVE on proven state | check your registry state |
origin_not_registered | 1 | the origin does not resolve to your verifier (no trust-by-name) | register/renew the exact origin |
revalidation_expired | 1 | domain-control proof for the origin lapsed | renew the origin registration |
request_key_not_active | 2 | the challenge's signing key is unregistered or rotated out | rotate/register keys properly |
request_not_ours (1.5.0) | after 2 | the request is valid on chain but names another verifier or origin than this service's own — a presentation made for someone else, handed to you | reject; a replay across relying parties, never retried |
nonce_replayed | 3 | this challenge's nonce was already consumed | issue a fresh challenge; investigate replays |
deadline_exceeded | 4 | the response arrived at or after the 10-second deadline | issue a fresh challenge |
proof_invalid | 5 | the proof failed verification or is bound to a different challenge context | reject; possible tampering |
predicate_missing | 6 | a requested predicate is not among the proven ones | reject; holder cannot satisfy policy |
issuer_not_accepted | 7 | the credential's issuer is outside your accepted-issuer policy | policy decision — yours |
issuer_not_active | 7 | the issuer is not operational (or below your state minimum) | reject; retry later is legitimate |
status_root_stale | 7 | proof built against a root that is no longer acceptable (superseded and outside its validity window), or a root that has expired (fail closed) | holder retries with a fresh proof |
height_stale | 8 | the challenge's chain height fell outside [latest − max_height_lag, latest] | issue a fresh challenge |
state_unproven | 1/8 | a chain read could not be cryptographically proven — including claimed absence | infrastructure alert; never skip the check |
nonce_race_lost | 9 | a concurrent presentation consumed the nonce first (atomic accept gate) | reject; exactly one winner exists |
Notes:
- Issuer-state policy nuance (condition 7): with the default
issuer_state_minimum: "ACTIVE", an issuer under enhanced monitoring rejects asissuer_not_active; opting into"ENHANCED_MONITORING"accepts it. This is the only policy knob in the vocabulary's behavior, and it is your policy, expressed in the challenge. state_unprovenis your pager, not your bug. It means the SDK refused to decide on unproven state — a node lying, a proof failing, an anchor unreachable. The correct response is operational, never a retry-until-accept loop.
Agent-profile codes (contract 1.2.0, 1.3.0, 1.4.0, 1.5.0, 1.8.0)
Produced only when the challenge was created with profile: "agent_v1" — an agent answering for a principal, under a claim policy of the complete payment-identity profile.
| Code | Where | Meaning | Typical handling |
|---|---|---|---|
scope_value_not_in_vocabulary | challenge (HTTP 422), or precondition | the policy names a scope value (or a key-custody class) that no vocabulary version admitted at the challenge's height carries — unknown, gated, or not in its canonical form | fix the policy; the service never rewrites a value |
policy_axis_uncovered | challenge (HTTP 422), or precondition | the policy's transaction limit is at or above your configured fail-closed value threshold, and it does not request every axis (action, counterparty, category, region, time + value) | request the missing axes, or lower the limit |
scope_axis_unprovable | 5 | a scope value the policy names has no anchor at the credential's issuer — the axis cannot be proven, so nothing is accepted | reject; that issuer delegates no agent for that value |
pseudonym_verifier_mismatch (1.3.0) | challenge (HTTP 422), or precondition | the policy's pairwise_pseudonym entry names a verifier id other than your own — asking for the identifier another verifier knows the agent by is refused | name your own verifier id |
pseudonym_required (1.4.0) | challenge (HTTP 422), or precondition | the policy carries no pairwise_pseudonym entry — mandatory, exactly once, in every agent policy (every agent is recognisable by the party that verifies it) | add {"claim": "pairwise_pseudonym", "value": "<your verifier id>"} |
unexpired_required (1.5.0) | challenge (HTTP 422), or precondition | the policy does not ask unexpired — mandatory in every agent policy: an agent past its lease is refused by every relying party, whatever else is asked | add {"claim": "unexpired", "value": true} |
dispute_retention_unavailable (1.8.0) | 9 | the verifier could not keep the agent presentation's dispute retention record (no store, or the store failed), so it does not accept it: an agent presentation that could not be disputed is never accepted. Decided before the nonce is spent | operator alert: fix the retention store; the holder may retry with a fresh challenge |
At challenge time scope_value_not_in_vocabulary, policy_axis_uncovered, pseudonym_verifier_mismatch, pseudonym_required and unexpired_required are an HTTP 422 answer (policy_refused, with the code) — no challenge and no session exist. They can also appear as a decision if a challenge comes back for validation that your deployment would not have issued (a lower threshold since, a hand-edited request). An agent presentation whose scope evidence is anchored under an older root than the one it claims is status_root_stale, and one whose evidence does not check is proof_invalid — the identity meanings, unchanged. Since 1.8.0 every agent presentation carries a dispute ciphertext: a missing or invalid one is proof_invalid too, and a presentation whose dispute retention record your verifier cannot keep is dispute_retention_unavailable (condition 9, in the table above), never accepted.
Reserved codes
Present in the contract enum for defence in depth; structurally unreachable through the validation entrypoint — do not wait for them:
| Code | Why it cannot occur |
|---|---|
origin_invalid | a non-exact origin already fails the shape precondition and rejects as request_malformed |
domain_validation_failed | a generic reason; a more specific code is always returned instead |
Seventeen producible codes (sixteen, plus request_not_ours), seven agent-profile codes and two reserved ones: that is the whole enum in the contract, and nothing outside it can reach your switch.