Content-Type: application/problem+json. No error: {} wrapper and no generic
details.
Every
type resolves to this page: the final slug is the anchor of its
section, like #not-found-404.The common shape
Six fields, always present:string
required
Canonical, stable URI of the type. It is the only field worth comparing in code.
string
required
Stable text per type. It doesn’t describe the occurrence.
integer
required
The same HTTP code as the response.
string
required
Describes this occurrence. Don’t parse it: it is not an interface and
can change without notice.
string
required
Short alias in
snake_case. It is an open string: new codes can appear
without breaking an old client. The slug of type
is the same code in kebab-case.string
required
Identifies the occurrence and matches the
X-Request-ID header. It goes in
every bug report.string
Optional in RFC 9457, and the API doesn’t emit it.
X-Request-ID header. The 401 adds
WWW-Authenticate: Bearer, and the 429 adds Retry-After.
The full catalog
Extensions sit at the root level of the Problem, not nested.
invalid_request — 400
The body or the parameters don’t meet the contract: a required field is
missing, an undeclared one is present, or a value doesn’t validate.
array
required
At least one element, with a required
code and detail and an optional
pointer: an RFC 6901 JSON Pointer to the offending field, starting with
/. Use it to flag the wrong input instead of showing a generic message.unauthorized — 401
You didn’t send Authorization, or the key you sent is not valid. It is the
same error for both causes: telling them apart would tell an attacker whether
a key exists.
forbidden_connection — 403
The key authenticated fine, but the requested connection_id doesn’t belong
to that store.
403 and not 404 because the connection travels in the path: the
client already knows it asked for it. Catalog resources work the other way:
see not_found.
forbidden_scope — 403
The key authenticated fine, but it doesn’t have the operation’s scope:
recommendations:read, catalog:read or signals:write. A key doesn’t change
scopes: you need another one that has it.
not_found — 404
The resource doesn’t exist, or it exists and belongs to another store. No detail says which of the two it is.
conflict — 409
The request clashes with the current state: a source_version regression
in catalog ingestion. A signal older than the applied one doesn’t overwrite the
catalog.
cart_items_limit_exceeded — 422
The cart in a recommendations request exceeds the configured limit.
integer
required
The limit in effect,
200 by default. It comes in the response so you don’t
hardcode it.integer
required
How many items you sent.
signal_batch_limit_exceeded — 422
A storefront signal batch carries more than allowed.
integer
required
The limit in effect,
50 by default. The SDK already splits batches by this number.integer
required
How many signals you sent.
rate_limited — 429
The key spent its 60 requests per minute; Retry-After says how many seconds
to wait. See Quota and cache.
internal_error — 500
Something broke on the server side. The detail never carries the stack,
the internal message or a component’s name.
request_id is essential: it connects what you saw with the server
log.
signals_unavailable — 503
The storefront signal store is unavailable. It is transient: retry with
backoff. The SDK does it on its own, with the same signal_id, so a retry
never counts twice.
How to handle them from the SDK
The SDK translates any of these Problems into a typedCrossUpApiError and
preserves the extensions it doesn’t know, so it doesn’t break when the API
adds fields.
Use
isCrossUpApiError(err) and not err instanceof CrossUpApiError:
instanceof fails if there are two copies of the package in the module tree.- The
Content-Typeis not checked; the shape of the body is. A proxy can rewrite the header, and the error is still typed. - The transport status wins. If the body declares another, the
difference is kept in
extensions.status: proof that something in the middle rewrites responses. errorFromResponsenever throws. Given HTML, an empty body or JSON that is not a Problem, it returns a synthetic error withcode: "unexpected_response", the real status and the raw body.