Every refusal, and what it means
The clause ids the receipt verifier raises, the names the decision API sends, and the classes the executor alerts on - with what to do about each.
Every refusal carries a name. The name is the machine-readable half and the message beside it is for a person; dispatch on the name, log the message.
A refusal is not a network error, and you should not retry it. A receipt that fails one of these fails it the same way every time, so asking again gets the same answer with an extra round trip. Treat it as you would a failed authorisation: stop, and record the name.
What the receipt verifier refuses
verify in Python and verifyReceipt in TypeScript raise on all of these,
with the same spelling. So does the engine.
| Clause | What it means | What you do |
|---|---|---|
AB-0 | It is not a version 3 receipt. | You are holding the wrong artifact, or one from an older format. |
AB-6 | Its signed body is over the size cap. | Report it. A receipt this size did not come from a normal decision. |
CR-1 | Its signature suite is not one this build knows, or it declares none. Also raised when your own ZIFFER_SUITE_FLOOR is not a suite this build knows. | Check your floor first: an unknown floor is your configuration, not the receipt. |
CR-4 | Its suite does not contain every primitive your floor requires. | The receipt is signed more weakly than you said you would accept. Do not lower the floor to make it pass. |
9.3-1 | A signature primitive did not verify. The composition is conjunctive, never any. | Stop. Either the receipt is not ours, or it is another tenant's: a sandbox receipt fails here against a production anchor, by construction. |
9.3-2 | Its decision is not one this domain defines. | Stop and report it. |
AT-8a | The bytes you passed are not UTF-8 JSON at all. | A bug in your call, not a verdict on the receipt. A truncated read or a double-encoded string does this. |
9.3-3 | It is not bound to the proposal you passed. | The object differs by at least one field value. Key order and whitespace are not the cause: the verifier canonicalises your bytes itself. |
9.3-5 | Your clock says it has expired, or its temporal position is wrong. | Check your clock before you report it. |
L-14 | Its validity window is implausibly long. | Report it. |
WE-4 | Its nonce is not the well-formed wire type. | Report it. |
L-17 | Its nonce is not a well-formed 128-bit value. | Report it. |
Two places where a name is not spelled identically everywhere. Neither changes a verdict, and both are pinned from both sides so they cannot move quietly.
- An unknown suite name. The SDKs and the engine say
CR-1. The reference implementation the specification ships saysCR-4, because its floor test answers "no" for a name it does not recognise instead of refusing it as unrecognised.CR-4asks whether one suite contains another's primitives, and an unregistered name names no primitives at all. - A receipt wrong in more than one way. Each verifier stops at the first
check it reaches, and the two SDKs do not order the floor check and the
signature-shape check the same way. A receipt that is both below your floor
and carrying a signature whose primitives are not its declared suite's is
CR-4from Python and9.3-1from TypeScript. Both refuse; only the name differs.
What the verifier does not check matters as much. It runs the half of the checklist that is answerable from a receipt, a proposal and a key. It cannot check that this receipt has not already been used, because that is a claim against a ledger inside our deployment and your process cannot reach it.
What the decision API refuses
These names arrive verbatim from the API. Both SDKs surface them without guessing.
| Name | HTTP | What it means | What you do |
|---|---|---|---|
ProposalMalformed | 400 | The body is not a valid proposal. | Fix the body. No clause travels with this one. |
ApiKeyUnknown | 401 | The key is not one we hold. | An expired key answers exactly like a key that never existed, byte for byte, so check the expiry date you were given first. |
TenantMismatch | 403 | The body names a tenant that is not the key's. | Send the key's tenant. The gateway refuses rather than rewriting your body, because a gateway that edited it would have become its author. |
DecisionUnknown | 404 | No decision with that id. | Check the locator you are reading with. |
AdmissionUnavailable | 502 | We could not reach the component that decides. | Retry this one. Nothing was decided, and the row stays pending. |
The set is open on purpose: the API may name a refusal that is not in this table, and both clients carry any name through verbatim rather than folding it into one of the five.
What your SDK raises on its own
| Name | Language | What it means |
|---|---|---|
RefusedError | Python | The receipt did not hold. name is the clause id. |
Refusal | TypeScript | The same, thrown. clause is the clause id. |
ApiError | Python | The API refused, or answered outside its contract. name is the API's name, or UnexpectedResponse. |
ApiRefusal | TypeScript | The API refused. error is the API's name. |
GatewayUnreachable | Python | No HTTP answer at all: refused connection, DNS, timeout. Named apart from the API's own outage because the advice differs. |
UnexpectedResponse | Python | A 2xx that was not the contract's shape. It fails closed under its own name rather than being guessed into one of the API's. |
ResponseMalformed | TypeScript | The same, thrown. An SDK that repairs a malformed answer is an SDK that invents decision state. |
WaitTimeout | both | The deadline passed with the decision still pending. Not a refusal and not an answer: the decision may still decide, and the id remains readable. |
AnchorError | Python | Your trust anchor document could not be accepted. Omitting the suite floor raises this one, naming MinSuiteRequired. It is a problem on your side, kept distinct from a verdict about any receipt. |
A ValueError from Python's verify is not a refusal. If the bytes are not
JSON at all, it raises that deliberately, so that a broken integration is not
mistaken for a caught attack.
What the executor refuses
When an executor refuses a receipt, the refusal reaches two places: an audit record, and a critical alert whose audience is named in your signed bundle rather than by us. Each alert carries one of eight classes.
| Class | What raised it |
|---|---|
LEDGER_REPLAY | A consumed identifier, or a rebound origin. |
RECOMPUTATION_MISMATCH | The receipt's stated risk or reversibility is not what your bundle says. |
SUITE_BELOW_FLOOR | A signature suite under your floor. |
CONTEXT_STORE | The live capability check disagrees with what was decided. |
AUDIT_INTEGRITY | The audit chain, or its anchors. |
ATTESTATION_INVALID | A quorum that does not hold, including a human approver's assertion. |
DEFERRED_RELEASE | The hold, the notice, or an acknowledgement. |
RECEIPT_INVALID | Anything else the executor refuses on the receipt's own bytes. |
Two behaviours to know before you build alerting on these.
A refusal is terminal, not a retry. The work closes as refused. Nothing is lost by that: the refusal is already an audit record and already a critical alert, and leaving it open would wedge every later item behind one poisoned receipt.
A dependency that could not answer is not a refusal. If the audit service, the context store or the ledger cannot be reached, the executor reaches no verdict, reports nothing, and the work stays queued. A store that could not answer is not a store that said no, and recording a verdict nobody reached would close a receipt nobody decided.