Every refusal, and what it means
The names 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
Every refusal names what is wrong with the receipt, and the rule of the specification it was refused under. Both SDKs print the same line for the same refusal, Name: what it means (rule): read name and clause on the refusal, and action for what to do. Each rule links to its section of the specification.
| Refusal | What it means | What you do | Rule |
|---|---|---|---|
ReceiptVersionUnsupported | What you passed as the receipt is not a version 3 receipt. | Pass the receipt exactly as the decision API returned it, unchanged. If you did, report it with the receipt. | AB-0 |
ReceiptTooLarge | The receipt's signed body is larger than the 4,096 bytes a receipt is signed within. | Do not act on the receipt and do not retry. Report it with the receipt. | AB-6 |
SuiteUnknown | The receipt names a signature suite this verifier does not know or names none, or the suite floor you configured is not a suite it knows. | Check the spelling of the suite floor you configured (ZIFFER_SUITE_FLOOR), then update the SDK. If both are right, report it with the receipt. | CR-1 |
SuiteBelowFloor | The receipt, or an approval sent with it, is not signed with a suite that contains every algorithm your suite floor requires. | Confirm the suite floor you configured is the one you meant. If it is, do not act on the receipt and report it with the receipt. | CR-4 |
NotCanonicallyEncodable | The receipt, an approval or the proposal you passed holds a value with no canonical encoding, such as a fractional number, or the proposal bytes are not UTF-8 JSON. | Pass the exact proposal bytes you sent and the receipt exactly as it was returned. Remove any step that re-encodes either one on the way. | AT-8a |
SignatureInvalid | The receipt's signature does not verify under your trust anchor's keys. | Do not act on the receipt and do not retry. Confirm your trust anchor is the one on your handover sheet, then report it with the receipt. | 9.3-1 |
DecisionNotAllow | The receipt records a decision other than ALLOW. | Do not run the action. Report it with the receipt. | 9.3-2 |
ReceiptNotBoundToProposal | The receipt is not bound to the proposal you passed. | Pass the exact proposal this receipt answered. When several calls are in flight, match each receipt to its own proposal before verifying. | 9.3-3 |
ReceiptTimeUnreadable | The receipt's issue or expiry time is missing or is not an RFC 3339 UTC instant. | Do not act on the receipt. Pass it exactly as the decision API returned it; if you did, report it with the receipt. | 9.3-5 |
ReceiptExpired | Your clock is past the receipt's expiry time. | Keep the verifying host's clock on UTC time, and verify a receipt as soon as it arrives: it is valid for 120 seconds. Propose the action again for a new receipt. | 9.3-5 |
ReceiptIssuedInFuture | The receipt's issue time is more than 5 seconds ahead of your clock. | Synchronise the verifying host's clock with UTC time, then verify the receipt again. | 9.3-5 |
ValidityWindowTooLong | The receipt is valid for longer than the 120 seconds a receipt may be. | Do not act on the receipt and do not retry. Report it with the receipt. | L-14 |
NonceMalformed | A nonce in the receipt, or in an approval sent with it, is not b64: followed by padded base64. | Do not act on the receipt and do not retry. Report it with the receipt. | WE-4 |
ReceiptNonceWrongSize | The receipt's nonce is well formed but is not 128 bits long. | Do not act on the receipt and do not retry. Report it with the receipt. | L-17 |
ApprovalsUnverifiable | The approvals sent with the receipt cannot be matched to the list the receipt signed: a list is malformed, an approval is not on it, or the verifier was given no attester registry. | Give the verifier your attester registry (ZIFFER_ATTESTER_REGISTRY, or the registry argument). If you did, do not act on the receipt and report it with the receipt. | AB-1 |
ApprovalDigestsUnsorted | The receipt's list of approval digests is not in sorted order. | Do not act on the receipt and do not retry. Report it with the receipt. | AB-2 |
ApprovalDigestsDuplicated | The receipt's list of approval digests names one approval more than once. | Do not act on the receipt and do not retry. Report it with the receipt. | AB-3 |
ApprovalCountMismatch | The number of approvals sent with the receipt is not the number of approvals the receipt signed. | Pass the receipt and its approvals exactly as the decision API returned them. If you did, report it with the receipt. | AB-4 |
ApprovalsMissing | The receipt commits to approvals and none were sent with it. | Pass the receipt with its attestations exactly as the decision API returned them. If you did, report it with the receipt. | INV-1-HIGH |
ApprovalObjectMissing | An approval sent with the receipt carries no approval object. | Do not act on the receipt and do not retry. Report it with the receipt. | AT-8 |
ApprovalObjectMalformed | An approval object lacks a required field, carries a field that is not defined, or holds a field of the wrong type. | Do not act on the receipt and do not retry. Report it with the receipt. | AT-8b |
ApprovalNonceWrongSize | An approval's nonce is well formed but is not 128 bits long. | Do not act on the receipt and do not retry. Report it with the receipt. | AT-1 |
ApprovalSignatureInvalid | An approval's signature does not verify against the key your attester registry holds for that approver, or your registry does not enrol that approver. | Check that the approver is enrolled in your attester registry with their current key. If they are, do not act on the receipt and report it with the receipt. | 9.3-7b-i |
ApprovalNotBoundToProposal | An approval sent with the receipt is not bound to the proposal you passed. | Do not act on the receipt and do not retry. Report it with the receipt and the proposal. | 9.3-7b-ii |
ApprovalContextMismatch | An approval names another policy version than the receipt, expired before the receipt was issued, or has an unreadable policy, expiry or operator field. | Propose the action again so that it is approved under the current policy. If it happens again, report it with the receipt. | 9.3-7b-iii |
ApprovalsDisagreeOnOperator | The approvals sent with the receipt name different operators. | Do not act on the receipt and do not retry. Report it with the receipt. | 9.3-7b-iii-a |
ApprovalQuorumSizeMismatch | An approver consented to a number of required approvals other than the quorum your attester registry sets. | Propose the action again so that it is approved under the current policy. If it happens again, report it with the receipt. | AT-9 |
ApprovalIdMismatch | An approval's transmitted identifier is not the identifier computed from its content. | Do not act on the receipt and do not retry. Report it with the receipt. | Y1b |
ApproverAssuranceTooLow | An approver is enrolled at an assurance level below your attester registry's floor, or at a level outside AS0 to AS2. | Enrol the approver at the assurance level your registry requires, or have an approver enrolled at that level approve instead. | AT-10 |
QuorumNotMet | Fewer distinct approvers approved the action than the quorum your attester registry sets. | Do not run the action. Check your registry's approvers and roles against who approved; propose the action again once enough approvers can answer. | AT-3 |
OperatorApprovedOwnAction | The operator who proposed the action is counted among its approvers. | Do not run the action. Have approvers other than the operator approve it. | AT-2 |
ApproverKeyTypeMismatch | An approval claims a kind of key, a person's passkey or a machine key, that is not the kind your attester registry enrols for that approver. | Do not act on the receipt and do not retry. Report it with the receipt. | WebauthnAlgMismatch |
PasskeyCounterNotAdvanced | A passkey or security key's signature counter did not advance, so the credential was copied or the approval was replayed. | Do not act on the receipt. Treat the approver's authenticator as compromised and re-enrol it before it approves anything else. | WebauthnCounter |
ApprovalMalformed | A person's approval, or the key your attester registry holds for them, is not in the shape a passkey approval takes. | Check the approver's entry in your attester registry against the credential they enrolled. If it matches, report it with the receipt. | Malformed |
PasskeyAssertionTypeInvalid | A person's approval is not a passkey sign-in assertion. | Ask the approver to approve again from your approval page. | WebauthnType |
PasskeyChallengeMismatch | A person's approval answers a different request than this approval. | Do not act on the receipt. Ask the approver to approve again from your approval page; if it happens again, report it with the receipt. | WebauthnChallenge |
PasskeyOriginMismatch | A person's approval was made on a site other than your approval page. | Ask the approver to approve again from your own approval page. If they did, report it with the receipt. | WebauthnOrigin |
PasskeyAuthenticatorDataInvalid | A person's approval names another site, or does not show that the person was present and verified. | Ask the approver to approve again and to complete the fingerprint, face or PIN step their authenticator asks for. | WebauthnAuthenticatorData |
PasskeySignatureInvalid | A person's approval signature does not verify against the passkey or security key your attester registry holds for them. | Check that the approver's entry in your attester registry holds their current credential. If it does, do not act on the receipt and report it with the receipt. | WebauthnSignature |
verify runs every check answerable from a receipt, a proposal and a key.
Single use is checked against the ledger inside our deployment, not by
verify: act on each receipt once.
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. |
ListQueryMalformed | 400 | The query string of GET /v1/decisions is not a legal one. | Drop every parameter, confirm the call works, then add them back one at a time. |
DecisionUnknown | 404 | No decision with that id. | Check the locator you are reading with. |
RateLimited | 429 | This key's allowance on the decision routes is spent. Nothing was decided. | Wait the seconds Retry-After names, then send the same request again. |
AdmissionUnavailable | 502 | We could not reach the component that decides. | Retry this one. Nothing was decided, and the row stays pending. |
GatewayUnavailable | 503 | The process serving the API could not answer at all. | A short retry is reasonable. Nothing decided anything. |
QueueFull | 503 | The approval queue is full, so the submission was not taken. No row was written. | Wait the seconds Retry-After names and send the same bytes again. |
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 these.
A decision the API did answer, and refused, carries no clause id. It carries a
refusal_category, which says whether to try again and nothing about which
rule refused. The rule is in the console and in your audit chain.
| Category | What it means | What you do |
|---|---|---|
PolicyRefused | A rule refused the action. | Never retry the same proposal. |
PolicyBasisMoved | The policy changed under the decision, in the window right after a publish. | Retry once, after the activation window. |
RateBounded | A rate bound refused it. | Retry later. |
ProposalMalformed | The submission is not a valid proposal. | Fix the submission. |
What your SDK raises on its own
| Name | Language | What it means |
|---|---|---|
RefusedError | Python | The receipt did not hold. name is the refusal, clause the rule it was refused under, and action what to do. |
Refusal | TypeScript | The same, thrown, with the same name, clause and action. |
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 | An ApiError name, status 0: no HTTP answer at all, such as a refused connection, DNS or a timeout. Named apart from the API's own outage because the advice differs. |
UnexpectedResponse | Python | An ApiError name: an answer 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. |
DeadlineExceeded | both | The deadline you passed would be crossed by the next retry, so the client stopped before sleeping. It carries the last failure's name and status. |
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.
Running a sandbox tenant
Who this is for: you, if ZIFFER has provisioned you a sandbox tenant, or if you are deciding whether to ask for one.
The ZIFFER MCP server
Who this is for: the developer whose AI assistant helps set ZIFFER up: it scans the code, writes the integration, checks the policy and tries it in a sandbox.