Errors
The error envelope, every code, and what to do with each.
Every failure — from a malformed key to a database conflict — comes back in one shape. Write one error handler, not twelve.
{
"error": {
"code": "NOT_FOUND",
"message": "Order not found.",
"request_id": "4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59"
}
}The error object
| Field | Always present | What it is for |
|---|---|---|
code | Yes | A stable machine-readable identifier. Branch on this. |
message | Yes | A human-readable sentence. Show it to a developer; never parse it. |
fields | Only on validation failures | A map of field name to the list of things wrong with it. |
request_id | Whenever a request id was stamped | Correlates with our logs. Echoed in the X-Request-Id header. |
There is no success field and no envelope around the error. The HTTP status
and the code are the answer.
Every error code
Authentication and authorization
| HTTP | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | No Authorization header, a token that is not salaf_sk_…, an unknown key, a plan without API access, or a store or company that is not live. |
| 401 | KEY_REVOKED | The key exists and was deliberately revoked. |
| 401 | KEY_EXPIRED | The key's expiry date passed, or its rotation grace window closed. |
| 403 | FORBIDDEN_SCOPE | The key is valid but was not granted the scope this endpoint requires. Scopes are fixed at creation — make a new key. |
Request problems
| HTTP | Code | When |
|---|---|---|
| 404 | NOT_FOUND | No such record in the data this key can see. Also what a record belonging to another tenant returns. |
| 422 | VALIDATION_ERROR | A parameter or body field was missing, the wrong type, out of range, or not in the allowed set. Carries fields. |
| 400 | VALIDATION_ERROR | The one case that stays 400: a required Idempotency-Key header is absent. fields names the header. |
| 409 | CONFLICT | The write collides with an existing record — a duplicate unique value, or a reference to something that is not there. |
| 429 | RATE_LIMITED | Too many requests for this key in the current window. See Rate limits. |
Ours, not yours
| HTTP | Code | When |
|---|---|---|
| 500 | INTERNAL | Something failed on our side. The message is always "Something went wrong on our end." — deliberately generic, because the alternative is leaking our internals into your logs. The real cause is recorded against the request_id. |
Retry a 500 once, with a short delay. If it persists, send us the
request_id.
On the write endpoints
| HTTP | Code | When |
|---|---|---|
| 400 | INSUFFICIENT_STOCK | Not enough stock to satisfy the request. fields.items names the offending line, with what was requested and what is available. |
| 400 | INVALID_STATUS_TRANSITION | The order cannot move from its current status to the requested one. fields.status says which move was refused. |
| 422 | IDEMPOTENCY_CONFLICT | The Idempotency-Key was reused with a different request body. |
| 409 | IDEMPOTENCY_IN_FLIGHT | The same Idempotency-Key is still being processed. Retry shortly, with the same key. |
Full detail on Writes & idempotency.
Validation errors
fields maps each parameter to everything wrong with it. Ordinary validation
failures are normalised to 422, so there is one status meaning "your request
was wrong" — the exceptions are the three 400s named above, which carry
their own codes.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request was not valid.",
"fields": {
"limit": [
"limit must not be greater than 100",
"limit must be an integer number"
]
},
"request_id": "4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59"
}
}Request ids
Every request through the public API is stamped with an id, returned in the
X-Request-Id response header and included in the body of any error.
HTTP/1.1 404 Not Found
X-Request-Id: 4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59
Content-Type: application/jsonLog it on every failure. It is the only handle that finds your exact request in our logs — far more useful than a timestamp and an endpoint name, particularly for a sync that makes thousands of calls an hour.
You can also supply your own: send an X-Request-Id header and we will use
your value instead of generating one. That lets you carry a trace id from your
own system straight through ours, so one identifier spans both sides.
Handling errors well
- Switch on
code, with a default branch. New codes may be added in v1 — treat the list as open, and let anything unrecognised fall through to "unexpected error, log and alert". - Do not retry
4xx. A401,403,404or422will fail exactly the same way the second time. The only retryable statuses are429(wait forRetry-After) and500(once, briefly). - Distinguish
KEY_REVOKEDfromUNAUTHORIZEDin your alerting. A revoked key means a human deliberately turned your integration off; it should page someone, not silently retry forever. - Keep
request_idin your own error records, not just in a log line that rotates out after a week.