---
name: http-problem-details
source: https://app.decimal.ai/s/http-problem-details@2/SKILL.md
source_sha256: bb3cdc1d8b64
---

# HTTP Problem Details error bodies

## Contract

Enforces the Problem Details object shape on every JSON error body an HTTP API returns for a
4xx or 5xx response. Apply whenever the task is to produce the body of a failure response, an
API error payload, or an error-envelope schema — not a success payload and not the choice of
status code itself.

## Rules

An error body is a single JSON object whose members come from this fixed set. Do not invent
other top-level names for them.

1. **`title`** — a short, human-readable summary of the *problem type*. It is the same string
   for every occurrence of that type; it does not embed the specific request's details. Use it
   in place of any `error`, `message`, `msg`, or `reason` member.

2. **`status`** — the HTTP status code as a **JSON number (integer)**, equal to the status on
   the response line. Never a string (`"404"`), never a made-up internal `code`.

3. **`detail`** — a human-readable explanation specific to *this occurrence*: what went wrong
   for this particular request. This is where the request-specific sentence goes, not `title`.

4. **`type`** — a URI reference (string) identifying the problem type. When you have no
   dedicated documentation URI, use the exact literal `"about:blank"`; in that case `title`
   should be the HTTP status phrase (e.g. `"Not Found"`).

5. **`instance`** — an optional URI reference identifying the specific occurrence (often the
   request path or a trace URL). Include it when a caller could act on it.

6. **Extension members go at the TOP level.** Machine-readable extras (a validation list, a
   remaining balance, a retry hint) are added as sibling members of `title`/`status`, NOT
   nested under an `error`, `data`, `meta`, or `problem` wrapper. The object is never wrapped.

7. **Validation failures** carry the extension member **`invalid-params`**: an array of objects
   each with exactly `name` (the offending field) and `reason` (why it failed).

8. **Content type**, when the task asks for headers, is **`application/problem+json`** — not
   `application/json`.

## Worked examples

The base's wrong default is on the left; the conforming Problem Details object is on the right.

Internal error, generic (no dedicated type URI):

```
BEFORE  {"success": false, "error": "Internal Server Error", "code": 500}

AFTER   {
          "type": "about:blank",
          "title": "Internal Server Error",
          "status": 500,
          "detail": "The request could not be completed due to an unexpected condition."
        }
```

Conflict on a versioned resource (dedicated type, request-specific detail):

```
BEFORE  {"error": "conflict", "message": "The document was modified by someone else"}

AFTER   {
          "type": "https://api.example.com/problems/edit-conflict",
          "title": "Edit Conflict",
          "status": 409,
          "detail": "The document changed since revision 41; reload before saving.",
          "instance": "/documents/9c2/revisions/41"
        }
```

Validation failure (extension member at the top level):

```
BEFORE  {"error": "Validation failed", "fields": {"email": "invalid", "age": "too low"}}

AFTER   {
          "type": "https://api.example.com/problems/validation",
          "title": "Your request parameters didn't validate.",
          "status": 422,
          "invalid-params": [
            {"name": "email", "reason": "must be a well-formed address"},
            {"name": "age", "reason": "must be at least 18"}
          ]
        }
```

## Edge cases & exceptions

- **No documentation URI available** → set `type` to the literal `"about:blank"` and make
  `title` the status phrase; do not omit `type` and do not invent a fake URL.
- **`title` vs `detail`** → `title` is constant per problem type ("Out of Credit"); `detail`
  varies per request ("Your balance is 30 but the item costs 50."). If a value names a specific
  amount, id, or path, it belongs in `detail`, not `title`.
- **Multiple failures in one response** → keep ONE problem object and put the list in an
  extension member (`invalid-params` for validation); do not return a bare JSON array of
  errors as the whole body.
- **Extra machine-readable data** (a balance, a retry-after seconds value, an account id) →
  add it as a top-level extension member with a descriptive name; consumers ignore members
  they don't recognize.

## Do / Don't

- Do name the human summary `title` and the specific explanation `detail`. Don't use `error`,
  `message`, `msg`, or `errorMessage`.
- Do make `status` an integer equal to the response's HTTP status. Don't quote it and don't
  substitute a private numeric `code`.
- Do keep every member at the top level of the object. Don't wrap the object under `error`,
  `data`, `result`, or `meta`.
- Do use `"about:blank"` when there is no type URI. Don't drop the `type` member entirely.
- Do serve it as `application/problem+json`. Don't label it `application/json` when a content
  type is requested.

## Common mistakes

- Returning `{"error": "..."}` or `{"message": "...", "code": ...}` — the ad-hoc shape the base
  reaches for by default.
- Putting the request-specific sentence in `title` and leaving `detail` generic (they are
  swapped): `title` is type-level, `detail` is occurrence-level.
- Wrapping the whole object under an `error` or `data` envelope instead of top-level members.
- Emitting `"status": "404"` (string) instead of the integer `404`.
- Spelling the validation array `errors`/`fields`/`violations` instead of the registered
  `invalid-params`, or giving its entries keys other than `name` and `reason`.

## Quick checklist

- Human summary is `title`; specific explanation is `detail`; they are not swapped.
- `status` is the integer HTTP code, not a string, not a private code.
- `type` is present — a URI, or the literal `"about:blank"`.
- All members (including extensions and any validation list) are at the top level, unwrapped.
- Validation uses `invalid-params` with `name` + `reason` entries.