Errors
Every error is an RFC 9457 problem document with a stable type, a sentence for people and a path for every invalid field.
Any non-2xx answer has content-type: application/problem+json:
{
"type": "https://shopfa.st/problems/validation",
"title": "Validation failed",
"status": 422,
"detail": "The request body is invalid.",
"fields": [
{ "path": "body.priceMinor", "message": "Too small: expected number to be >=0" }
]
}
typeis stable: branch on its last segment (validation,forbidden,rate-limited,edit-conflict…). It is an identifier, not a page to fetch.detailis one plain sentence you can show to a person.fieldslists every invalid input by dotted path:params.storeId,query.limit,body.items.0.quantity.
Status codes
| Status | When |
|---|---|
| 400 | The body is not JSON, or the request is malformed |
| 401 | No credentials, or a key or token that is not valid |
| 403 | Your scopes, role or IP allowlist do not cover this |
| 404 | It does not exist, or belongs to another store |
| 409 | A conflict: a limit, a state that does not allow this, an edit conflict |
| 413 | The body is larger than 6 MB |
| 422 | Validation failed (see fields) |
| 429 | Rate limited; wait for Retry-After seconds |
| 5xx | Our side. Safe to retry with backoff for GET, and for writes that carry an idempotency key |
In code
const res = await fetch(url, init);
if (!res.ok) {
const problem = await res.json();
const kind = problem.type.split('/').at(-1);
if (kind === 'validation') showFieldErrors(problem.fields);
else if (res.status === 429) await sleep(Number(res.headers.get('retry-after') ?? 1) * 1000);
else throw new Error(problem.detail);
}