Reason codes
One decision, a closed vocabulary: how rejections are structured, the families they fall into, and the two rules of handling them.
One decision, a closed vocabulary
Every validation returns exactly one decision: accepted, or a rejection carrying one machine-readable reason code from a closed vocabulary — byte-identical across the Python SDK, the HTTP contract and every client. Any change to that vocabulary is a major version of the contract and flows to every language at once, so your switch will not silently meet an unknown code within a contract major. The full vocabulary ships with the SDK and the frozen service contract; what matters when you design an integration is the families the rejections fall into.
| Family | What it means | Typical handling |
|---|---|---|
| Challenge problems | the challenge your side built is malformed or violates the contract shape | fix challenge construction — this is always a bug on the verifier side |
| Registration & origin problems | your verifier registration, exact origin, or signing key is not active on proven chain state | operational: check your registry state, renew the origin proof, rotate keys properly |
| Freshness & replay problems | the presentation arrived too late, or its one-time challenge was already consumed | issue a fresh challenge; repeated replays are worth investigating |
| Proof & predicate problems | the zero-knowledge proof failed verification, is bound to a different challenge, or does not prove what your policy asked | reject — the holder cannot satisfy your policy, or something was tampered with |
| Issuer policy & status problems | the credential’s issuer is outside your accepted-issuer policy, not operational, or its status root is stale (fail closed) | a policy decision on your side, or a legitimate holder retry with a fresh proof |
| Proven-state problems | a chain read could not be cryptographically proven — including a claimed absence | an infrastructure alert, never a bypass: the SDK refuses to decide on unproven state |
Two rules of handling
- Rejections are answers, not errors. A rejection is a first-class, expected outcome of the pipeline: log the reason code, decide per family, move on. Only transport-level failures (a body that cannot be parsed at all) surface as errors instead of decisions.
- The proven-state family is 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, and there is no configuration that makes it accept.