Error handling
The shape
Every error is application/problem+json, not a bespoke body per endpoint:
{
"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.
| Member | What it's for |
|---|---|
type |
A URI that resolves to this code's entry on /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 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 — retrying won't fix this |
The retryable recipe
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, 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:
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:
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.