ZIFFER home

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.

ClauseWhat it meansWhat you do
AB-0It is not a version 3 receipt.You are holding the wrong artifact, or one from an older format.
AB-6Its signed body is over the size cap.Report it. A receipt this size did not come from a normal decision.
CR-1Its 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-4Its 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-1A 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-2Its decision is not one this domain defines.Stop and report it.
AT-8aThe 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-3It 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-5Your clock says it has expired, or its temporal position is wrong.Check your clock before you report it.
L-14Its validity window is implausibly long.Report it.
WE-4Its nonce is not the well-formed wire type.Report it.
L-17Its 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 says CR-4, because its floor test answers "no" for a name it does not recognise instead of refusing it as unrecognised. CR-4 asks 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-4 from Python and 9.3-1 from 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.

NameHTTPWhat it meansWhat you do
ProposalMalformed400The body is not a valid proposal.Fix the body. No clause travels with this one.
ApiKeyUnknown401The 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.
TenantMismatch403The 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.
DecisionUnknown404No decision with that id.Check the locator you are reading with.
AdmissionUnavailable502We 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

NameLanguageWhat it means
RefusedErrorPythonThe receipt did not hold. name is the clause id.
RefusalTypeScriptThe same, thrown. clause is the clause id.
ApiErrorPythonThe API refused, or answered outside its contract. name is the API's name, or UnexpectedResponse.
ApiRefusalTypeScriptThe API refused. error is the API's name.
GatewayUnreachablePythonNo HTTP answer at all: refused connection, DNS, timeout. Named apart from the API's own outage because the advice differs.
UnexpectedResponsePythonA 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.
ResponseMalformedTypeScriptThe same, thrown. An SDK that repairs a malformed answer is an SDK that invents decision state.
WaitTimeoutbothThe deadline passed with the decision still pending. Not a refusal and not an answer: the decision may still decide, and the id remains readable.
AnchorErrorPythonYour 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.

ClassWhat raised it
LEDGER_REPLAYA consumed identifier, or a rebound origin.
RECOMPUTATION_MISMATCHThe receipt's stated risk or reversibility is not what your bundle says.
SUITE_BELOW_FLOORA signature suite under your floor.
CONTEXT_STOREThe live capability check disagrees with what was decided.
AUDIT_INTEGRITYThe audit chain, or its anchors.
ATTESTATION_INVALIDA quorum that does not hold, including a human approver's assertion.
DEFERRED_RELEASEThe hold, the notice, or an acknowledgement.
RECEIPT_INVALIDAnything 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.

On this page