Best Practices for Documenting API Error Responses

API7.ai

July 21, 2026

API 101

Key Takeaways

  • Document errors as part of the API contract. Status codes, media types, fields, headers, and retry behavior deserve the same precision as successful responses.
  • Separate machine decisions from human explanations. Clients should branch on stable status codes and problem types or error codes, never on mutable message text.
  • Tell developers how to recover. A useful error entry explains the cause, what can be changed, whether retrying is safe, and where to get help.
  • Show realistic failures. Include representative authentication, authorization, validation, conflict, rate-limit, and server-error responses for each operation where they can occur.
  • Protect sensitive details. Error documentation should define useful diagnostics without encouraging stack traces, secrets, internal hostnames, or private user data in production responses.

The happy path proves that an API can work. Error documentation determines whether an integration can survive reality. Credentials expire, inputs fail validation, resources change between reads and writes, quotas are reached, and dependencies become unavailable. If developers have to reverse-engineer those situations, each client invents different recovery behavior.

Good error documentation creates a predictable interface between provider and consumer. It answers five questions quickly: What happened? Why did it happen? Can the caller fix it? Is a retry safe? How can this occurrence be traced?

This chapter assumes the provider has already designed its error semantics and focuses on documenting that public contract. For provider-side decisions about status codes, payload structure, and gateway behavior, start with API Error Handling: Status Codes, Error Responses, and Best Practices. For client implementation patterns, see Error Handling When Consuming APIs.

Start with HTTP Semantics

For HTTP APIs, select status codes according to their standardized semantics. RFC 9110 defines the core classes and codes. The status code is not decoration: generic clients, caches, proxies, observability tools, and gateways use it even when they do not understand the response body.

Common distinctions should be explicit in documentation:

StatusTypical MeaningConsumer Action
400 Bad RequestMalformed syntax or generally invalid requestCorrect the request before retrying
401 UnauthorizedMissing, invalid, or expired authenticationObtain valid credentials and retry
403 ForbiddenAuthenticated caller lacks permissionRequest access or change the operation
404 Not FoundTarget resource is unavailable to the callerVerify the identifier; do not retry blindly
409 ConflictRequest conflicts with current resource stateRefresh state or use conflict-specific guidance
422 Unprocessable ContentSyntax is valid but one or more values fail semantic rulesCorrect the identified fields
429 Too Many RequestsA rate or quota policy rejected the requestRespect Retry-After or the documented reset signal
500 Internal Server ErrorUnexpected provider failureRetry only according to policy and report the trace ID
502 Bad GatewayAn intermediary received an invalid upstream responseApply bounded transient-failure retry policy
503 Service UnavailableService cannot currently handle the requestHonor Retry-After; use backoff and jitter

Do not use 200 OK with an error hidden in the body. It breaks generic tooling and makes dashboards report failures as successes. Do not assign different meanings to the same status code across operations without a domain-specific machine-readable discriminator.

flowchart TD
    E["Request failed"] --> A{"Authenticated?"}
    A -- "No" --> S401["401: obtain valid credentials"]
    A -- "Yes" --> P{"Authorized?"}
    P -- "No" --> S403["403: request access or change action"]
    P -- "Yes" --> V{"Request valid?"}
    V -- "No" --> S400["400 or 422: correct request"]
    V -- "Yes" --> C{"State conflict?"}
    C -- "Yes" --> S409["409: refresh or resolve conflict"]
    C -- "No" --> T{"Rate or service transient?"}
    T -- "Yes" --> S429["429 or 503: bounded retry"]
    T -- "No" --> S500["5xx: trace and escalate"]

    classDef action fill:#f6ffed,stroke:#389e0d,color:#173b0b;
    classDef question fill:#fff7e6,stroke:#d48806,color:#5c3b00;
    class A,P,V,C,T question;
    class S401,S403,S400,S409,S429,S500 action;

Use a Consistent Error Envelope

RFC 9457 defines Problem Details for HTTP APIs. Its JSON media type is application/problem+json, with standard members:

  • type: a URI identifying the problem category;
  • title: a short, human-readable summary of that category;
  • status: the HTTP status code, repeated for convenience;
  • detail: an occurrence-specific human-readable explanation;
  • instance: a URI reference identifying this occurrence.

APIs can add extension members when clients need structured domain information. A validation response might be:

HTTP/1.1 422 Unprocessable Content Content-Type: application/problem+json Content-Language: en X-Request-ID: req_01J1K9Y7A6E5Q4P3 { "type": "https://developer.example.com/problems/validation-error", "title": "The request contains invalid values.", "status": 422, "detail": "Correct the listed fields and submit the request again.", "instance": "/problems/req_01J1K9Y7A6E5Q4P3", "errors": [ { "code": "minimum", "pointer": "/items/0/quantity", "detail": "Value must be greater than or equal to 1." } ] }

The top-level problem identifies the primary error. The errors extension gives field-level detail for problems of the same validation type. Document the extension schema, pointer format, maximum number of returned entries, and whether the order is stable.

Problem Details is a strong default, not a requirement for every API style. An established API may retain a different consistent envelope to avoid breaking clients. GraphQL commonly returns an errors array under its response model, and asynchronous protocols may use their own error frames. Whatever the format, preserve the same design properties: stable machine identifiers, clear human context, documented recovery, and traceability.

Keep Identifiers Stable

Human-readable messages will change for clarity or localization. Client code must not parse them. Document the exact value consumers should branch on—usually type, a domain code, or both.

Good identifiers describe the condition, not the implementation:

https://developer.example.com/problems/insufficient-balance inventory_unavailable idempotency_conflict

Avoid identifiers such as database_error_42 that expose internals and cannot remain stable when architecture changes. If a problem type is an HTTPS URL, keep it under a domain you control and make it resolve to durable documentation for that problem.

Create an Error Catalog Developers Can Scan

Operation pages should list the errors relevant to that operation. A central catalog should define reusable problem types in detail. Do not make readers choose between a giant undifferentiated table and no information at all.

For every documented error type, include:

  1. Machine identifier — the exact type URI or error code.
  2. HTTP status and media type — including relevant headers.
  3. Condition — the circumstances that produce the error.
  4. Consumer action — how to prevent, correct, retry, or escalate it.
  5. Retry policy — whether retry is safe, under what conditions, and with what delay.
  6. Example — a realistic request context and full response.
  7. Security notes — whether 404 intentionally hides resource existence, for example.
  8. Lifecycle — when the type was introduced or changed.

Use headings and searchable names. “Payment Errors” is less findable than “insufficient-balance — Add Funds or Choose Another Account.” Link from the operation's 409 response directly to the catalog entry.

Document Retry Behavior Precisely

“Try again later” is not a retry policy. State whether the operation is idempotent, whether the caller needs an idempotency key, which response headers control delay, and the maximum retry behavior expected from clients.

For 429 and 503, document Retry-After if the service sends it. For other transient failures, recommend exponential backoff with jitter and a bounded attempt or time budget. Warn clients not to retry validation, authorization, or conflict errors until something changes.

If a POST can be retried safely with an idempotency key, document the key's scope, retention period, format, and conflict behavior. Error handling cannot be accurate without those details.

Describe Errors in OpenAPI

OpenAPI should include every response a consumer is expected to handle, not only 200 and a generic default. Reuse a base problem schema while preserving operation-specific descriptions and examples:

paths: /orders: post: operationId: createOrder responses: '201': description: Order created. '409': description: The idempotency key conflicts with an earlier request. content: application/problem+json: schema: $ref: '#/components/schemas/Problem' examples: idempotencyConflict: value: type: https://developer.example.com/problems/idempotency-conflict title: The idempotency key was already used. status: 409 detail: Use a new key or resend the original request body. instance: /problems/req_01J1K9Y7A6E5Q4P3 '429': description: The client exceeded its request rate. headers: Retry-After: description: Seconds to wait before another request. schema: type: integer minimum: 1 content: application/problem+json: schema: $ref: '#/components/schemas/Problem' components: schemas: Problem: type: object required: [type, title, status] properties: type: type: string format: uri-reference title: type: string status: type: integer minimum: 400 maximum: 599 detail: type: string instance: type: string format: uri-reference

Validate examples against schemas and contract-test actual failures. Happy-path tests will never tell you that production emits HTML from a proxy while the contract promises JSON.

Test Documentation Across Every Error Source

Errors can originate at several layers: client-facing gateway, identity provider, service code, downstream service, or infrastructure. The documented contract should describe what the consumer receives at the public boundary.

sequenceDiagram
    participant C as Client
    participant G as API Gateway
    participant S as Service
    participant D as Dependency

    C->>G: Request
    alt Authentication or rate policy fails
        G-->>C: Standard public problem response
    else Request reaches service
        G->>S: Forward request with trace context
        alt Domain validation fails
            S-->>G: Domain problem
            G-->>C: Preserve documented status and type
        else Dependency fails
            S->>D: Call dependency
            D-->>S: Timeout or internal failure
            S-->>G: Sanitized service problem
            G-->>C: Stable public problem plus request ID
        end
    end

An API gateway such as Apache APISIX can enforce authentication, rate limits, and routing before an upstream is called. Document those gateway-generated responses alongside application responses. If a gateway normalizes errors, preserve the correct HTTP semantics and useful trace identifiers; do not turn every condition into an indistinguishable 500.

Build negative tests for missing credentials, insufficient scopes, malformed JSON, every important validation rule, unknown resources, stale revisions, duplicate idempotency keys, rate limits, timeouts, and intentionally injected service failures. Assert status, content type, stable identifier, required headers, schema, and absence of sensitive fields.

Write Secure and Useful Messages

Error detail should help the consumer correct the request, not debug the provider's implementation. Never return stack traces, SQL fragments, internal hostnames, filesystem paths, access tokens, session IDs, or other customers' data. RFC 9457 explicitly warns that problem details can expose attack vectors and implementation internals.

At the same time, “Something went wrong” is inadequate. A safe response can identify the invalid field, expected constraint, public request ID, and support route without revealing internal state.

Treat instance and trace identifiers carefully. If an occurrence URL resolves to diagnostic detail, require authorization and prevent cross-tenant access. Document whether users should send the request ID to support and how long it remains searchable.

Localization affects human-readable title and detail, not machine identifiers. If responses honor Accept-Language, document the fallback language and send Content-Language. Clients must continue to make decisions using stable structured fields.

Error Documentation Review Checklist

  • Are HTTP status codes semantically correct at the public boundary?
  • Does every expected failure have a stable machine identifier?
  • Can human messages change or be translated without breaking clients?
  • Does each entry explain correction, retry, and escalation?
  • Are Retry-After, rate-limit, authentication, and trace headers documented?
  • Do operation pages link to relevant catalog entries?
  • Do OpenAPI schemas and examples match real responses from every layer?
  • Are batch and field-level errors bounded and precisely structured?
  • Are secrets, personal data, and implementation details excluded?
  • Are breaking changes to errors detected and communicated like any other contract change?

Conclusion

Error responses are not an afterthought; they are the part of an API contract that production clients exercise most defensively. Use HTTP status codes accurately, give clients stable problem identifiers, pair every failure with recovery guidance, and test the documented response at the actual consumer boundary.

Next, turn those successful and unsuccessful scenarios into code developers can safely copy, understand, and maintain with Incorporating Code Samples in API Documentation.