Errors
The Distribu API uses HTTP status codes for success and failure signals, and every error response has the same JSON shape:
{
"error": "A human-readable message."
}
A handful of errors add one additional field (like retryAfter on 429
responses), but the error field is always present.
Status codes
| Code | Meaning |
|---|---|
200 | Success |
201 | Created (only POST /api/v1/orders) |
400 | Bad request — malformed JSON or failed validation |
401 | Authentication failed — missing, malformed, or invalid key |
403 | Authenticated, but missing the required scope |
404 | Resource not found, or belongs to another company |
422 | Request understood, but the business rule rejected it |
429 | Rate limit exceeded |
500 | Something broke on our end |
400 — Bad request
Returned when your request body is malformed or fails validation. Typical messages:
{ "error": "Invalid JSON body." }
{ "error": "customerId is required" }
{ "error": "productId is required" }
{ "error": "quantity must be a number" }
{ "error": "quantity must be a whole number" }
{ "error": "quantity must be at least 1" }
{ "error": "At least one item is required" }
{ "error": "Invalid order status." }
{ "error": "Validation failed." }
Check the error string first — it tells you exactly which field failed.
401 — Authentication
{ "error": "Missing or invalid Authorization header. Use: Bearer <api_key>" }
{ "error": "Invalid, expired, or revoked API key." }
Both indicate the request couldn't be associated with an API key. The server doesn't distinguish between "wrong key," "expired key," and "revoked key" in the response — intentionally, to avoid leaking which state a leaked key is in.
403 — Insufficient scope
{
"error": "Insufficient permissions. This key lacks the \"orders:write\" scope."
}
The key is valid but doesn't have the scope this endpoint requires. See Scopes for the endpoint → scope mapping. Fix by revoking the key and creating a new one with a broader scope set.
404 — Not found
{ "error": "Product not found." }
{ "error": "Order not found." }
{ "error": "Customer not found." }
Three possibilities for any 404:
- The resource ID is wrong (typo, truncation).
- The resource belongs to another company — the API treats cross-company reads as 404, not 403, to avoid leaking existence.
- The resource was deleted.
Check your own records before assuming the API lost something.
422 — Business rule failure
Returned when the request is well-formed and authorized but violates a data-consistency rule.
{
"error": "Cannot update a cancelled order."
}
The one 422 today is on PATCH /api/v1/orders/{id} for an order that's
already in CANCELLED status — there's no way forward from there.
Note that POST /api/v1/orders returns 400 (not 422) for stock and
active-product violations, which is a minor inconsistency in the API's
status-code vocabulary — the messages are still specific enough to
distinguish:
{ "error": "Product \"clxxABC...\" not found or is inactive." }
{ "error": "Insufficient stock for product \"Widget Blue\". Available: 3, requested: 10." }
429 — Rate limit
{
"error": "Rate limit exceeded. Try again shortly.",
"retryAfter": 42
}
The retryAfter field is the number of seconds until your rate-limit
bucket resets. Sleep at least that long before retrying. The default
budget is 60 requests per minute per API key — see the
overview for details.
Implement a retry-with-backoff in your client for 429 responses,
including the retryAfter hint.
500 — Server error
If something breaks server-side, you'll see:
Internal Server Error
…with HTTP status 500. These shouldn't happen during normal operation; when they do, they're logged and alerted on our side. Retry once; if it keeps failing, email support@distribu.app with the time, endpoint, and payload (minus secrets).
Error handling pattern
A reasonable client approach:
async function callDistribu(path, options = {}) {
const res = await fetch(`https://distribu.app${path}`, {
...options,
headers: {
Authorization: `Bearer ${process.env.DISTRIBU_API_KEY}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (res.status === 429) {
const body = await res.json();
await new Promise((r) => setTimeout(r, (body.retryAfter ?? 5) * 1000));
return callDistribu(path, options); // one retry
}
if (!res.ok) {
const body = await res.json().catch(() => ({ error: res.statusText }));
throw new DistribuError(res.status, body.error);
}
return res.json();
}
class DistribuError extends Error {
constructor(public status, message) {
super(message);
}
}
No machine-readable error codes
There's no error_code or error_type field today — just the free-form
error string and the HTTP status. If you need to branch on specific
errors, match on the HTTP status plus substring in the message. This is a
known limitation; structured error codes are on the roadmap.
Next: Products endpoints.
