## The shape

Every error is `application/problem+json`, not a bespoke body per endpoint:

```json
{
  "type": "https://developer-api.stylumia.net/errors#unknown_category",
  "title": "Unknown category",
  "status": 400,
  "detail": "'power tool' is not a recognized category for retailer 'home_depot'.",
  "code": "unknown_category",
  "retryable": false,
  "recovery": "Call GET /v1/coverage?retailer=... to list that retailer's categories, or use one of the suggestions in valid_options.",
  "field": "category",
  "valid_options": ["power_tools", "hand_tools", "tool_storage"],
  "request_id": "req_9f2c31a4d8e0176b3f52"
}
```

`unknown_retailer`, `unknown_category`, and `unknown_department` share this
exact shape — only `field`, `detail`, and `valid_options` differ by which
parameter was wrong. All three are the vocabulary lookup this API has, since
there's no separate endpoint listing retailers, categories, or departments
on their own; see [Coverage & RCD combinations](/docs/objects/reference-data).

| Member | What it's for |
|---|---|
| `type` | A URI that resolves to this code's entry on [/errors](/errors) |
| `title` | Human-readable name — for display, not for branching |
| `status` | The HTTP status code |
| `detail` | Human prose about this specific occurrence — wording may change over time |
| `code` | The stable identifier — branch on this |
| `retryable` | `false` = fix the request; `true` = back off and retry |
| `recovery` | What to do instead, in one sentence |
| `field` | Which parameter caused it, when applicable |
| `valid_options` | Close real values, when the error is about a guessed vocabulary term |
| `request_id` | This request's id — quote it to support |

## Branch on `code`, never `detail`

`detail` is written for a person reading a log. Its wording can change
without notice — it is not a contract. `code` is: it's enumerated at
`/errors`, append-only, and every `type` URI resolves there. If
you're writing `if "not a recognized" in detail:` anywhere, stop — match
`code == "unknown_category"` instead.

New codes may appear over time as the API grows. Treat a code you don't
recognize by its `status` class rather than failing closed on it.

## Three response classes

| Class | Status | Meaning | What to do |
|---|---|---|---|
| Fix the request | 4xx (except 429) | The request itself is wrong | Read `recovery` and `valid_options`, fix it, resend — don't retry unchanged |
| Retry with backoff | 429, 5xx | Transient — [rate limit](/docs/rate-limits) or our side | Back off and retry; honor `Retry-After` when present |
| Ask a human | 403 | Your team lacks the entitlement | Check your team's [entitlement](/docs/authentication) — retrying won't fix this |

## The `retryable` recipe

```python
attempts = 0
while attempts < 5:
    resp = call()
    if resp.ok:
        break
    body = resp.json()
    if not body.get("retryable"):
        raise Exception(body["detail"])  # fix the request instead
    sleep_for = min(2 ** attempts + random.uniform(0, 1), 60)
    time.sleep(sleep_for)
    attempts += 1
```

All `/v1` endpoints are reads — every request is safe to repeat, with or
without this loop. There are no idempotency keys to manage.

## `recovery` and `valid_options` in practice

Guess a category that doesn't exist and you get exactly the error shown
above: [`unknown_category`](/errors#unknown_category), with `valid_options`
holding the top five real values for the retailer you asked about. A
five-line client reads it and
retries without a human involved:

```python
resp = requests.get(url, headers=headers)
if resp.status_code == 400 and resp.json()["code"] == "unknown_category":
    fixed = resp.json()["valid_options"][0]
    url = url.replace(f"category={bad_category}", f"category={fixed}")
    resp = requests.get(url, headers=headers)
```

## Trigger one on purpose

Errors are safe to trigger on purpose — every endpoint here is a read. Call the same endpoint with a junk category
and watch the shape:

```bash
curl "https://api.stylumia.com/v1/catalog?retailer=home_depot&category=not_a_real_category" \
  -H "Authorization: Bearer $STYLUMIA_KEY"
  # Never commit your key — see /docs/key-security
```

That's a deliberate, safe way to see `recovery` and `valid_options` before
you need them in production code.

The full catalogue — every code, its status, causes, and extension
members — is at `/errors`. Each `code` here resolves to an anchor
there.
