Errors
Errors are JSON. There are two body shapes, depending on which layer produced the error — always branch on the HTTP status code first, then inspect the body.
Shape A — simple message (middleware and newer endpoints):
{ "message": "Human-readable explanation" }
Shape B — platform envelope (most core endpoints):
{
"errors": {
"some_key": [
{ "message": "Human-readable explanation", "code": "MachineReadableCode", "data": [] }
]
},
"status": "failure",
"message": "Human-readable explanation"
}
In Shape B, errors is a map of key → array of error objects. The key names what the error is about (token, user, url, a field name, or a model class). code is a stable machine-readable identifier (e.g. Unauthorized, NotFound, CustomerExists, IsLocked); message may change, code won't. The top-level message mirrors the first error's message.
401 Unauthenticated
Missing, malformed, expired, or revoked token:
{
"errors": {
"token": [
{ "message": "Unauthorized", "code": "Unauthorized", "data": [] }
]
},
"status": "failure",
"message": "Unauthorized"
}
Fix the credential and retry. Don't retry automatically — 401 never resolves by itself.
403 Forbidden
Three distinct causes, distinguishable by body:
Missing token scope (Shape A, includes required_scope):
{
"message": "Token is missing the required scope: orders:write",
"required_scope": "orders:write"
}
PAT on an endpoint not open to tokens (Shape A):
{ "message": "This endpoint is not available to API tokens." }
User-level permission denial (Shape B) — the token has the scope, but the user account behind it lacks the permission:
{
"errors": {
"user": [
{ "message": "This action is unauthorized for you.", "code": "Forbidden", "data": [] }
]
},
"status": "failure",
"message": "This action is unauthorized for you."
}
A token pinned to an IP allowlist also returns 403 from other addresses: "This token is not permitted from 203.0.113.5."
404 Not Found
Unknown route:
{
"errors": {
"url": [
{ "message": "No route found for this url.", "code": "NotFound", "data": [] }
]
},
"status": "failure",
"message": "No route found for this url."
}
Missing record — the key is the model class instead of url:
{
"errors": {
"App\\Models\\Customer": [
{ "message": "No query results for model [App\\Models\\Customer] 999", "code": "NotFound", "data": [] }
]
},
"status": "failure",
"message": "No query results for model [App\\Models\\Customer] 999"
}
405 Method Not Allowed
Wrong HTTP verb on a real path — same envelope with "code": "MethodNotAllowed" under the method key.
422 Validation Failed
errors maps each invalid field to an array of message strings:
{
"message": "The name field is required. (and 1 more error)",
"errors": {
"name": ["The name field is required."],
"email": ["The email must be a valid email address."]
},
"status": "failure"
}
Domain-rule 422s use error objects with a stable code and extra data instead of plain strings — for example creating a duplicate customer:
{
"message": "You have this address for another customer",
"errors": {
"address": [
{
"message": "You have this address for another customer",
"code": "CustomerExists",
"data": { "existing_customer_id": 15 }
}
]
},
"status": "failure"
}
Handle both: strings mean "fix this field"; objects carry a code you can switch on and data you can act on.
429 Too Many Requests
See Rate Limits for both 429 variants. Always honor the Retry-After header.
500 Internal Server Error
{ "message": "Server Error" }
Not actionable client-side. Retry idempotent requests with backoff; if it persists, contact support with the X-Request-Id response header value — every API response carries one, and it lets support find the exact failing request in the logs.
Handling guidance
- Branch on HTTP status first; then on
errors.*[].codewhere present. - Log the
X-Request-Idheader for every non-2xx response. - Retry only 429 (per
Retry-After) and 5xx (with exponential backoff, idempotent requests only). - Expect either error shape from any endpoint and parse defensively:
messageis always present;errorsmay be absent, a field→strings map, or a key→objects map. - One success caveat: endpoints using the platform envelope return HTTP 200 even for creates, and business failures inside those endpoints still surface as 4xx with the envelope. A
2xxplus"status": "success"is the reliable success signal there — see API Conventions.