Every error, visually
69 documented codes, each rendered exactly as the refusal card an agent sees on a real call — not a table to cross-reference by hand. The full retry-semantics writeup, with the response envelope and the idempotency guarantee, is the error contract. This page and that one are generated from the same list (lib/error-codes.ts); a build fails if they ever disagree.
Request errors — fix the input
✕ validation_error
A field failed schema validation; `reason` lists `field: problem` pairs.
HTTP 400
✕ invalid_request
Body was well-formed JSON but not usable as sent.
HTTP 400
✕ invalid_page_count
page_count is not a whole number of at least 1.
HTTP 400
✕ page_limit_exceeded
The document is longer than the 12-page maximum.
HTTP 400
✕ tracking_included
Tracking was added to a class that already includes it — first_class only.
HTTP 400
✕ empty_document
The PDF or text contains nothing printable.
HTTP 400
✕ unsupported_characters
The letter text contains characters the chosen font cannot print (e.g. CJK, Arabic, or emoji). Upload as a PDF instead to send it as-is.
HTTP 400
✕ invalid_pdf
The bytes aren't a readable PDF.
HTTP 400
✕ invalid_base64
pdf_base64 is not valid base64.
HTTP 400
✕ low_text_density
Scanned-image PDF. Text documents only at launch.
HTTP 400
✕ file_too_large
PDF is over the 10MB cap (413 when PUT to the upload route).
HTTP 400 (also 413)
✕ pdf_fetch_failed
pdf_url wasn't publicly fetchable as a PDF.
HTTP 400
✕ pdf_required
This piece format needs a PDF and none was supplied.
HTTP 400
✕ blocked_address
pdf_url points at a private, reserved, or non-http(s) address and was refused.
HTTP 400
✕ dns_failed
pdf_url's host did not resolve.
HTTP 400
✕ redirect_limit
pdf_url redirected too many times.
HTTP 400
✕ invalid_upload_key
upload_key isn't a key this server issued.
HTTP 400
✕ upload_not_found
The upload_key expired or was already consumed.
HTTP 400
✕ unknown_pack
No credit pack with that id.
HTTP 400
✕ invalid_stars
A review was submitted with stars outside 1–5.
HTTP 400
✕ unsupported_mail_class
The rail that prints this piece does not offer that mail class.
HTTP 400
✕ unsupported_piece
The rail does not produce that piece format.
HTTP 400
✕ unknown_format
No such piece format.
HTTP 400
✕ class_unavailable_for_format
That mail class cannot carry that physical piece — a postcard cannot go certified.
HTTP 400
Credential errors — only if you send a key
✕ invalid_key
An Authorization header was sent and did not verify. Omit it entirely to call anonymously.
HTTP 401
✕ key_disabled
The key is real and has been turned off.
HTTP 403
✕ insufficient_scope
Valid key, but it does not carry the scope this route needs (`reason` names what it does carry).
HTTP 403
✕ key_store_unavailable
The key could not be checked, so it is not honoured. A credential we cannot verify is refused, never downgraded.
HTTP 503
Confirmation errors (action_required) — the spend-safety gate
✕ confirmation_required
No confirmation_token on a surface that requires one. Quote first; a send is never the first call.
HTTP 428
✕ confirmation_malformed
Not a paperplane quote token.
HTTP 400
✕ confirmation_invalid
Signature check failed — the token was altered or signed by another deployment.
HTTP 400
✕ confirmation_expired
Past its 30-minute window. Quote again.
HTTP 400
✕ confirmation_mismatch
Valid token, different letter — recipient, class, colour, tracking, content, or price changed since the quote.
HTTP 409
✕ confirmation_used
Already redeemed. **The letter you quoted was sent.** This is replay protection working; do not resend.
HTTP 409
✕ quote_content_required
A quote carrying no letter content cannot mint a token. A token binds a quote to one specific letter, so one bound to no document is not a weaker quote -- it is a blank cheque for any document the send path cannot fingerprint.
HTTP 400
✕ confirmation_unavailable
No signing secret configured, so no token could be minted or checked.
HTTP 503
Capability errors — order-scoped grants
✕ invalid_capability
The token is missing, malformed, or was issued for a different order or action.
HTTP 403
✕ expired_capability
Past its 7-day window. Gone, not forbidden — create a new order for a fresh pair.
HTTP 410
✕ capability_unavailable
No signing secret configured, so capability tokens cannot be issued or checked.
HTTP 503
Payment errors
✕ unknown_code
No credit account with that code (404 from the balance lookup, 402 when used to pay for an order).
HTTP 402 (also 404)
✕ insufficient_balance
The credit code exists but cannot cover this order; `next` states the balance.
HTTP 402
✕ x402_not_configured
Agent payments are off on this deployment. Use /v1/credits/checkout or a credit_code.
HTTP 501
✕ payment_required
No X-PAYMENT header (or MCP `payment_header`) was supplied; the response body is a full x402 payment-required challenge — sign it and resubmit with the header set.
HTTP 402
✕ region_unsupported
The x402 stablecoin lane is hidden in this region (New York) because the underlying Stripe payment method is not offered there; use /v1/credits/checkout or a credit_code.
HTTP 403
✕ x402_verify_failed
The facilitator would not verify the X-PAYMENT payload.
HTTP 402
✕ x402_settle_failed
Verification passed and settlement did not. Nothing was minted.
HTTP 402
✕ x402_payment_invalid
The X-PAYMENT header is not a payload we can read.
HTTP 402
✕ payments_not_configured
This deployment can't take payments right now.
HTTP 503
✕ no_checkout_url
Stripe accepted the session and returned no URL to send the payer to.
HTTP 502
✕ no_client_secret
Stripe accepted an embedded-checkout session (checkout_ui_mode: "embedded") and returned no client_secret to mount.
HTTP 502
State errors — the order is real and isn't in the state you asked about
✕ not_found
No order with that id. Sandbox orders are cleared on redeploy.
HTTP 404
✕ too_late
Already handed to the carrier; the cancellation window is closed.
HTTP 409
✕ not_reviewable
That order is not delivered, or has already been reviewed.
HTTP 409
✕ not_held
A review action on an order that is not held.
HTTP 409
✕ address_undeliverable
USPS verification rejected the recipient address; `reason` says why.
HTTP 422
✕ rejected
The fulfillment rail terminally refused the piece. Not retryable — fix the piece.
HTTP 422
✕ idempotency_key_in_flight
An earlier request with this `Idempotency-Key` has not finished. Retry with **the same key** after `Retry-After` to receive that request's order -- retrying with a new key is the intuitive move and it mails a second letter.
HTTP 409
Limits — back off, not rewrite
✕ rate_limited
Per-IP throttle on a guest endpoint. Honour Retry-After.
HTTP 429
✕ velocity_limit
Per-sender send cap (5/day, 20/month).
HTTP 429
✕ spend_limit
Per-credential spend cap ($50/day, $200/month).
HTTP 429
✕ caller_unidentified
We could not identify the caller well enough to hold it to a cap, so the send is refused rather than uncapped.
HTTP 403
Platform errors — our side, retryable (with two exceptions, marked below)
✕ unavailable
Transient fulfillment failure. Safe to retry.
HTTP 503
✕ rail_unavailable
Valid request, but no rail is currently configured to mail it.
HTTP 503
✕ limit_check_unavailable
The cap ledger was unreachable, so the send is refused rather than run uncapped.
HTTP 503
✕ audit_unavailable
The audit trail was unreachable. We do not mail what we cannot attribute.
HTTP 503
✕ below_cost_floor
This piece cannot be sent at its current price — a rail cost or plan-tier fault on our side, not your request. Nothing was charged; try a different format or mail class, or contact support.
HTTP 503
✕ retry_unsafe
The rail did not confirm the send and cannot be asked, so we refused to retry rather than risk a second physical piece. Nothing was sent and nothing was charged. **Do not auto-retry** — resubmit only if a human has confirmed nothing arrived.
next
- → Do not auto-retry — see the error contract for the safe recovery step.
HTTP 503
✕ submit_outcome_unknown
The rail could not tell us whether the piece reached the print facility, so it was not sent again. Nothing was charged, but a piece may already be in the mail. **Do not auto-retry** — resubmitting could produce a duplicate.
next
- → Do not auto-retry — see the error contract for the safe recovery step.
HTTP 503
✕ internal
Unexpected. Retry once, then report it with the response body.
HTTP 500