Skip to content

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.

ErrorEnvelope Structure
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>,
}
  • 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
  • retry_after: Option<32>

    • Seconds to wait before retrying
    • MUST be present on every retry-after error 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
  • Registry authority:

    • Every error value a server emits MUST appear in the registry below
    • Servers MUST NOT return an undocumented code, and clients MUST treat one as INTERNAL_ERROR
  • Stability:

    • A code’s meaning MUST NOT change once assigned
    • Retired codes MUST NOT be reused for a different meaning

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: true when the token was spent, even though the request failed
  • Servers MUST report token_consumed: false when the request failed before redemption
  • A client that receives token_consumed: false MUST retry with the same token rather than spending a new one

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

Rate limiting binds to token issuance, never to redemption - a server cannot meter a redemption, because it does not know which device is spending.

  • 429 MUST be returned only by issuance operations and by inbound queue protection
  • Servers MUST NOT return 429 from 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.

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
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
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
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.

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
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.

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
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.

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
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
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

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_UNKNOWN identically for an address that never existed and one that expired
  • Servers MUST NOT include a reporter identity, a source address, or a device_id in message
  • Servers MUST NOT log source addresses alongside redemption failures