intermediate 2 min answer

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.

api-designerrorsretryabilityintegratorsobservability
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 codecard_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-After removes 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-After and 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.