A public API's errors are a mixture of HTTP status codes, free-text messages and inconsistent bodies. Design an error contract that integrators can automate against, and explain what each part is for.
Show the full answer Hide the answer
What integrators actually need from an error
Three questions, in order: is this my fault or yours? should I retry? what specifically do I fix? An error design is good exactly to the extent that code can answer all three without parsing prose.
The contract
- A stable, machine-readable error code —
card_declined,rate_limited,invalid_parameter— that never changes once published, because integrators branch on it. This is the most important field and the one most often missing. - An HTTP status used correctly: 4xx for client faults, 5xx for server faults. The 200-with-error-body pattern is actively harmful, because every generic HTTP client, proxy and monitoring tool treats it as success.
- An explicit retryability signal. Do not make integrators infer it from the status code, because the
inference is wrong often enough to matter: some 4xx are retryable (429, 409 on a lock) and some 5xx are
not (501, or a 500 caused by a deterministic bug in the request). A boolean plus a
Retry-Afterremoves the guessing. - The offending field, for validation errors, as a structured path rather than embedded in a sentence — so the integrator can highlight the right input.
- A human-readable message for the developer's logs, explicitly not intended for end users and explicitly not stable — say so, or someone will parse it.
- A request identifier, echoed in every response and every log line, so a support conversation begins with a key rather than a description of the weather.
- A documentation link for the specific code.
Categories that deserve distinct treatment
- Validation — 400 with the field path; never retryable; the integrator must change the request.
- Authentication and authorisation — 401 versus 403, kept distinct, because the remedies differ (get a token versus obtain permission) and merging them costs integrators hours.
- Not found versus gone — 404 versus 410, since 410 tells a webhook or a sync client to stop asking.
- Conflict — 409, with enough information to resolve it, typically the current state or version.
- Rate limited — 429 with
Retry-Afterand quota headers. - Idempotency conflict — a distinct code for "same key, different payload", which is a client bug that must be loud.
- Upstream failure — 502/503/504, retryable, ideally distinguishing "we failed" from "our provider failed" because the integrator's response differs.
The properties that make it usable
Consistency across every endpoint, enforced by a shared error type in the framework rather than by convention — an error contract maintained by discipline degrades. Every code documented and enumerable, ideally machine-readable, so client libraries can generate typed errors. A stable code set, with new codes added and old ones never repurposed.
And errors instrumented as first-class telemetry: error rate by code, by tenant, by endpoint. A spike in one error code for one integrator is the earliest and clearest signal that a partner's deployment has broken something, and it is available only if the codes are structured.