Error responses are the least designed and most consumed part of most APIs. Teams spend weeks on the happy-path schemas and twenty minutes on the error body, which is what every client's retry logic, support tooling, and user-facing message depends on. The result is a dozen services with a dozen envelopes: {error: "msg"}, {code, message}, {errors: [...]}, {status, error: {message}}. In 2026 there is a mature standard — RFC 9457 Problem Details for HTTP APIs — and the case for adopting it is stronger than ever, especially once agents start calling your API.

What Problem Details looks like

The media type is application/problem+json, and the body has well-defined fields:

{
  "type": "https://api.example.com/errors/plan-limit-reached",
  "title": "Plan limit reached",
  "status": 403,
  "detail": "The free plan allows 1 active project. Archive a project or upgrade to Pro.",
  "instance": "/v1/projects",
  "errors": [
    {
      "detail": "Project count (1) exceeds plan limit (1).",
      "pointer": "#/data/active_projects"
    }
  ]
}

The five core fields:

  • type: a URI identifying the problem. Resolvable to human documentation is ideal, but even an opaque stable URN works as a machine key.
  • title: a short human-readable summary, stable for the type.
  • status: the HTTP status repeated in the body, for clients that only see the body (logs, intermediaries).
  • detail: a human-readable explanation specific to this occurrence.
  • instance: the specific request URI or correlation reference.

Extensions are explicitly allowed — errors above is an extension for field-level validation detail. This is the standard's best design decision: it gives you a common spine without forbidding domain data.

Why a standard beats a bespoke envelope

  1. Generic tooling understands it. Gateways, API clients, and agent frameworks increasingly render Problem Details natively; a bespoke format always needs custom parsing.
  2. The distinction between type and detail forces discipline. type is the stable machine code clients branch on; detail is the occurrence-specific sentence humans read. Conflating them — one free-text message field used for both — is the most common design error and the source of brittle string-matching in clients.
  3. HTTP status stays authoritative. The envelope reinforces rather than competes with the status code, so proxies and caches behave.
  4. AI agents handle it without guessing. When an MCP tool call fails, an agent reads type and detail and can correct the request (fix a field, request a different scope) rather than asking the user what a vendor-specific blob means.

Documenting it in OpenAPI 3.2

Define the problem schema once in components and reference it from every error response:

components:
  responses:
    BadRequest:
      description: Malformed or invalid request.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Problem:
      type: object
      description: RFC 9457 Problem Details.
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          description: Stable identifier for the problem kind.
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              detail: { type: string }
              pointer: { type: string, description: 'JSON Pointer to the offending field.' }
              parameter: { type: string }

Then each operation lists the statuses it actually returns — not a generic "500 for everything." The documented error set is part of the contract: a 409 Conflict with type: project-name-taken tells the client to surface a rename prompt; that flow cannot be built against an undocumented 400.

The status code conventions worth keeping

Problem Details does not replace HTTP semantics; it sharpens them:

StatusMeaning the client needsTypical type
400Malformed request; do not retry unchangedvalidation-failed
401Missing/invalid authenticationunauthorized
403Authenticated but not permittedplan-limit-reached
404No such resource, or hidden for securitynot-found
409Conflict with current statename-taken, version-conflict
422Well-formed request with semantic errorssemantic-validation
429Rate limited; honor Retry-Afterrate-limited
5xxServer fault; retry with backoffinternal-error

Two conventions matter for agents and automation: include a stable, documented type for every branch a client might take (retry, prompt, fail), and on 429/503 include Retry-After as a header — the spec should document that the client must honor it.

Migration without a big-bang

Existing APIs usually cannot replace their envelope overnight. A low-risk path:

  1. Add application/problem+json as an additional error media type, negotiated via Accept or enabled per API version; keep the legacy format for old clients.
  2. Standardize internally first: new services emit Problem Details; a gateway adapter can translate legacy envelopes into it for generic tooling.
  3. Document both during the deprecation window, with the legacy response marked deprecated in the OpenAPI document and a sunset note.
  4. Align SDK error classes on type rather than regex-matching messages.

What not to do

  • Do not put stack traces or internal service names in detail. It leaks topology and confuses users; log those server-side and return a correlation id in instance.
  • Do not vary title per occurrence. It belongs to the type; per-occurrence text goes in detail.
  • Do not return 200 with an error body. A surprising amount of legacy API behavior does this, and it makes agents and caches unrecoverably wrong.
  • Do not forget validation arrays. A form with five bad fields needs five entries; a single top-level error forces five round trips.

Designing the error model in the spec — and generating mocks that return realistic problems — lets frontend and agent developers build against failure on day one instead of discovering it in production. In Powerduck the problem schema lives in components like any other, scenario tests assert on both status and type, and mocks return the documented errors so failure flows are testable before the backend exists. The demo shows the workflow on a sample spec.

What to read next: a 200 is not done — run the business scenario makes the case for testing failure branches, and detect breaking API changes in CI catches the moment an error contract changes.