The error envelope
Every failure comes back in one shape:detail is a string for simple failures and an object or array for structured ones — validation errors, or a partial success that needs to name what worked. trace_id is echoed in the X-Trace-Id response header and is on every response, not just errors. Quote it in a bug report; it is the only thing linking your request to the span that holds the details.
Status codes
402 rather than 403 is deliberate for plan limits: it is not a permission you can be granted, it is a tier you can change. The CLI surfaces the two differently for the same reason.Rate limits
Two layers. A broad ceiling on every request from one caller, plus tighter budgets on the handful of routes where the cost of a single request is what matters — credential guessing, outbound email, share redemption, billing calls.Who a budget belongs to
The first two require a credential that survived a signature check, so neither can be minted to escape a bucket. Anonymous callers share an address with everyone behind the same NAT, which is why the unauthenticated budgets are set generously.
The budgets
Sensitive account actions covers minting an API key, revoking sessions, updating a profile and deleting an account.
Headers
Every limited response carries the state of the budget it was checked against:429 adds Retry-After in seconds. Wait it out rather than retrying immediately — the window is a fixed one, so a retry inside it just spends another attempt.
What is exempt
The payment webhook and/health are outside the limiter entirely. The webhook arrives from a small pool of addresses, so a burst of legitimate events looks exactly like one caller flooding the API, and a dropped delivery means a subscription silently stops being billed — the signature check is the right gate there. /health is polled on a fixed cadence by load balancers and uptime monitors, where a 429 reads as an outage. Static assets are exempt for the same reason.
Retrying well
- Treat
429and503as retryable, with backoff, honouringRetry-After. - Treat
401,402,403,404,409and422as terminal — retrying changes nothing. - Treat
207as success with a gap, and log thefailed_ids. - Batch. One bulk create costs one request no matter how many keys it carries; a loop over 200 secrets costs 200.
