Error Model
Every operation in this specification reports failure the same way. Transport bindings map the envelope onto their own conventions - HTTP carries it as a response body under a status code, WebSocket as an error frame - but the envelope and the code are identical across all of them.
Error Envelope
Section titled “Error Envelope”struct ErrorEnvelope { // Machine-readable identifier, UPPER_SNAKE_CASE. Stable across versions. error: String,
// Human-readable description for logs and development. Never shown to // end users, and never parsed by clients. message: String,
// Numeric code. First digit mirrors the HTTP class, so 4xxx is a client error // and 5xxx a sever error. code: u16,
// Seconds to wait before retrying. Present only on retry-after errors. retry_after: Option<u32>,
// Whether the request spent a PrivacyPass token or not. Absent on paths that // redeem no token. Lets a client re-send without re-spending, rather than // pessimistically assuming a token was burned. token_consumed: Option<bool>,}Field Descriptions
Section titled “Field Descriptions”-
error: String- The identifier clients match on
- MUST be one of the codes registered on this page
- Servers MUST NOT invent codes outside the registry
-
message: String- Diagnostic text for operators and developers
- Client MUST NOT parse it or display it to end users
- Servers WOULD NOT include values that identify a device, mailbox, or reporter
-
code: u16- Numeric form of
error, for languages where matching an integer is cheaper than matching a string - The first digit mirrors the HTTP class
- Numeric form of
-
retry_after: Option<32>- Seconds to wait before retrying
- MUST be present on every
retry-aftererror and absent otherwise - Clients MUST respect it
-
token_consumed: Option<bool>- Whether this request spent a PrivacyPass token
- MUST be present on any response to a token-bearing request
- MUST be absent on paths that redeem no token
Requirements
Section titled “Requirements”-
Registry authority:
- Every
errorvalue a server emits MUST appear in the registry below - Servers MUST NOT return an undocumented code, and clients MUST treat one as
INTERNAL_ERROR
- Every
-
Stability:
- A code’s meaning MUST NOT change once assigned
- Retired codes MUST NOT be reused for a different meaning
Token Consumption
Section titled “Token Consumption”A token is spent at redemption, so a request that never reached a server costs nothing - but a request the server rejected may already have consumed one.
token_consumed makes that explicit. Without it, a client must assume the worst and re-spend on every retry, which burns its budget fastest exactly when something is already going wrong.
- Servers MUST report
token_consumed: truewhen the token was spent, even though the request failed - Servers MUST report
token_consumed: falsewhen the request failed before redemption - A client that receives
token_consumed: falseMUST retry with the same token rather than spending a new one
Retry Classes
Section titled “Retry Classes”Every code carries exactly one retry class. This is what a client switches on.
| Class | Meaning | Client Action |
|---|---|---|
terminal |
The request will never succeed as written | Do not retry. Surface or log it |
retry-after |
Transient; capacity will return | Wait retry_after, then retry unchanged |
retry-with-new-token |
The token was the problem, not the request | Obtain a fresh token, then retry |
retry-as-is |
A short-lived precondition lapsed | Re-establish it, then retry |
Metering and 429
Section titled “Metering and 429”Rate limiting binds to token issuance, never to redemption - a server cannot meter a redemption, because it does not know which device is spending.
429MUST be returned only by issuance operations and by inbound queue protection- Servers MUST NOT return
429from a redemption path
This is the distinction a client needs most often. BUDGET_EXHAUSTED means your bucket is empty, wait for it to refill. TOKEN_INVALID means this particular token is bad, get another one. They are never the same condition, and they never share a code.
Failover Eligibility
Section titled “Failover Eligibility”A device MAY hold several mailboxes for one group across different servers. Senders attempt the primary first and fail over only on a hard rejection; a transient condition means retry the primary instead, because failing over on overload would spread load onto every server in the list.
Each attempt requires re-sealing, since the AAD and mailbox key differ per address, so a failover is never free.
| Failover Eligible | Not Eligible |
|---|---|
MAILBOX_UNKNOWN |
MAILBOX_OVERLOADED |
SERVICE_UNAVAILABLE |
BUDGET_EXHAUSTED |
| Connection failure or timeout | Any terminal token error |
- Senders MUST attempt the primary address first
- Senders SHOULD fail over on an eligible error
- Senders MUST NOT fail over on an ineligible one, and SHOULD retry the primary
Error Codes
Section titled “Error Codes”Request
Section titled “Request”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4001 | MALFORMED_REQUEST | 400 | terminal | Body failed to decode, or a required field is absent |
| 4002 | UNSUPPORTED_CONTENT_TYPE | 415 | terminal | Content type not accepted by this operation |
| 4003 | PAYLOAD_TOO_LARGE | 413 | terminal | Request exceeds the server’s size limit |
| 4004 | UNKNOWN_OPERATION | 404 | terminal | No such operation in this binding |
Token Redemption
Section titled “Token Redemption”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4101 | TOKEN_MISSING | 401 | retry-with-new-token | Operation requires a token; none was presented |
| 4102 | TOKEN_INVALID | 401 | retry-with-new-token | Authenticator failed to verify, or token_type is not 0x0001 |
| 4103 | TOKEN_KEY_UNKNOWN | 401 | retry-with-new-token | token_key_id names no current or overlapping issuance key |
| 4105 | TOKEN_REPLAYED | 409 | terminal | Nonce is already in the spent set |
Issuance
Section titled “Issuance”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4111 | BUDGET_EXHAUSTED | 429 | retry-after | Token bucket is empty. Partial grants are returned as success, so this means zero were available |
| 4112 | DEVICE_BLOCKED | 403 | terminal | An operator has zeroed this device’s issuance budget |
| 4113 | VDF_PROOF_INVALID | 400 | terminal | Proof failed verification on first-time announcement |
| 4114 | ANNOUNCEMENT_RATE_LIMITED | 429 | retry-after | Announcement limit reached for this source |
| 4115 | DEVICE_SIGNATURE_INVALID | 401 | terminal | Signature over device_id || timestamp failed to verify |
| 4116 | DEVICE_ID_MISMATCH | 400 | terminal | device_id is not Blake3(public_key) |
| 4117 | DEVICE_NOT_ANNOUNCED | 401 | retry-as-is | No device record; announce first, then retry |
| 4118 | TOKEN_TYPE_UNSUPPORTED | 400 | terminal | token_type is not 0x0001 |
DEVICE_BLOCKED is terminal rather than retry-after because no refill will clear it. It needs an operator decision. Existing tokens remain spendable until they expire. DEVICE_NOT_ANNOUNCED is the mechanism by which the VDF gates budget - issuance refuses a device with no record.
Mailbox
Section titled “Mailbox”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4201 | MAILBOX_UNKNOWN | 404 | terminal | No registration for this address |
| 4202 | MAILBOX_EXISTS | 409 | terminal | Prefix is already registered |
| 4203 | BINDING_INVALID | 400 | terminal | Prefix does not derive from the submitted mailbox_public |
| 4204 | CHALLENGE_EXPIRED | 410 | retry-as-is | Registration, retrieval, or announcement challenge lapsed; request a fresh nonce |
| 4205 | CHALLENGE_INVALID | 400 | terminal | Signature over the challenge failed to verify |
| 4206 | MAILBOX_OVERLOADED | 429 | retry-after | Server is shedding inbound volume for this address |
| 4207 | SESSION_INVALID | 401 | retry-as-is | Retrieval session expired or names a different mailbox |
KeyPackages
Section titled “KeyPackages”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4301 | HANDLE_NOT_FOUND | 404 | terminal | No entries under this handle |
| 4302 | HANDLE_EXHAUSTED | 410 | terminal | Handle exists but every entry has been consumed |
| 4303 | ENTRY_CAP_EXCEEDED | 409 | terminal | Upload exceeds the per-handle entry cap |
Because fetch deletes on read, HANDLE_NOT_FOUND and HANDLE_EXHAUSTED are both ordinary outcomes. A client SHOULD retry against the adjacent epoch before treating either as failure.
InfoPackages
Section titled “InfoPackages”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4311 | INFOPACKAGE_NOT_FOUND | 404 | terminal | No InfoPackage at this segment |
| 4312 | INFOPACKAGE_EXPIRED | 410 | terminal | TTL has elapsed |
| 4313 | INFOPACKAGE_EXHAUSTED | 410 | terminal | max_uses reached |
| 4314 | TTL_OUT_OF_RANGE | 400 | terminal | TTL outside 60 seconds to 30 days |
| 4315 | MAX_USES_OUT_OF_RANGE | 400 | terminal | max_uses outside 1 to 1000, on an InfoPackage or a blob |
| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4401 | BLOB_NOT_FOUND | 404 | terminal | No blob under this handle |
| 4402 | BLOB_EXPIRED | 410 | terminal | TTL has elapsed |
| 4403 | BLOB_USES_EXHAUSTED | 410 | terminal | max_uses reached for this handle |
| 4404 | BLOB_TOO_LARGE | 413 | terminal | Upload exceeds the host’s published limit |
Capability Tokens
Section titled “Capability Tokens”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4451 | REVOCATION_TOKEN_INVALID | 403 | terminal | Token does not match the one issued at upload |
Used by both InfoPackage revocation and blob deletion. Servers MUST compare revocation tokens in constant time, and MUST return this code rather than a not-found error when the resource exists but the token is wrong.
Federation
Section titled “Federation”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4501 | ORIGIN_SIGNATURE_INVALID | 401 | terminal | X-Origin-Signature failed to verify |
| 4502 | TRANSACTION_MISMATCH | 400 | terminal | Body transaction_id does not match the one in the path |
| 4503 | VERSION_UNSUPPORTED | 400 | terminal | Peer’s cryptid_version is unparseable, or a differing major |
| 4504 | FEATURE_UNSUPPORTED | 400 | terminal | Request uses a construct this server has not advertised |
| 4505 | ORIGIN_BLOCKED | 403 | terminal | Origin is defederated |
| 4506 | CERT_FINGERPRINT_MISMATCH | 401 | terminal | TLS certificate does not match the published fingerprint |
Administration
Section titled “Administration”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 4601 | ADMIN_UNAUTHORIZED | 401 | terminal | Not an administrator of this server |
| 4602 | ADMIN_INSUFFICIENT_PERMISSIONS | 403 | terminal | Administrator lacks the required permission |
| 4603 | CONFIG_INVALID | 400 | terminal | Configuration value rejected |
Server
Section titled “Server”| Code | Error | HTTP | Retry Class | Description |
|---|---|---|---|---|
| 5001 | INTERNAL_ERROR | 500 | retry-after | Unexpected server fault |
| 5002 | SERVICE_UNAVAILABLE | 503 | retry-after | Server is overloaded or in maintenance |
| 5003 | FEDERATION_UNAVAILABLE | 502 | retry-after | Remote server could not be reached |
| 5004 | FEDERATION_DISABLED | 501 | terminal | This server does not federate |
Privacy Requirements
Section titled “Privacy Requirements”Errors are a side channel, and a careless one leaks what the rest of the design withholds.
- Servers MUST NOT vary an error, a code, or response timing in a way that reveals whether a mailbox belongs to the same device as another
- Servers MUST return
MAILBOX_UNKNOWNidentically for an address that never existed and one that expired - Servers MUST NOT include a reporter identity, a source address, or a
device_idinmessage - Servers MUST NOT log source addresses alongside redemption failures