Response envelope
Every/v1/* response — success or error — is generated from the same zod
schemas in lib/schemas.ts that publish the
OpenAPI spec, so the shape below
is a stable contract to code against, not incidental JSON. A failure looks
like this:
status is always one of ok, action_required,
or failed on every /v1/* response — success bodies included — so it’s
always safe to switch on. code values are stable identifiers wherever they
appear (every error, and the confirmation-gate action_required responses
below) — match on them, never on reason wording. reason is human prose
(log it, don’t parse it), and next is the recovery playbook — what a
support person would tell you, machine-readable. The same envelope is
published as the Error component in the OpenAPI spec, generated from the
server’s own schemas so it cannot drift.
Retry semantics
The same rule reaches agents as a boolean: every MCP tool failure carries
retryable, computed from the status above, so a tool-calling loop does not
have to parse prose to decide whether to try again.
Always send an Idempotency-Key on POST /v1/orders. With it, every
retry above is safe by construction: a replay returns the original order
("replayed": true, same id) instead of creating a second one. This is why a
timeout is never a reason to fear resubmitting — proven live in the
benchmark.
Every code below is one entry in lib/error-codes.ts, and a test fails the
build if this page documents a code that does not exist, or omits one that
does. The tables are the catalogue, not a summary of it.
Request errors — fix the input
Credential errors — only if you send a key
An API key is optional on every route: omittingAuthorization is a supported
way to call, and a key exists to narrow what a caller may do (hand an agent a
mail:quote key and it can price a letter but cannot spend a cent). A key that
is sent and doesn’t verify is refused rather than quietly downgraded to
anonymous, so a typo fails loudly instead of silently dropping the restriction
you meant to apply.
Confirmation errors (action_required) — the spend-safety gate
A send is never the first call: POST /v1/orders on the agent path requires a
single-use confirmation_token from POST /v1/quotes, bound to recipient,
content, class, options, and price.
Capability errors — order-scoped grants
cancel_token and review_token both carry the ppc_ prefix; they’re told
apart by the action baked into the signature, not by the prefix. Both live 7
days.
Payment errors
State errors — the order is real and isn’t in the state you asked about
Limits — back off, don’t rewrite the request
Every 429 carriesRetry-After in seconds.
Platform errors — our side, retryable
If fulfillment fails after payment capture, you don’t handle it: an hourly
reconcile loop detects the failure and refunds automatically.
failed orders
in the lifecycle below are always accompanied by a refund.Platform errors that are 503 and must NOT be retried
These two are the exception to everything above. They are503, which
normally means “try again” — here it means we stopped on purpose, because
retrying could put a second physical piece in a real person’s mailbox. Mail is
not idempotent once it is printed, and no status code can be un-mailed.
Order lifecycle
What actually creates a charge
Verified againstlib/service.ts and app/v1/orders/route.ts — not a guess.
The only request that can ever spend real money is a live (non-sandbox)
POST /v1/orders, and only once payment actually completes — either via
credit_code (debited in that same call) or a completed Stripe checkout
(captured after the order clears screening). Quotes, sandbox orders, and
every GET in this document are free by construction.