Error Handling
The DayZero API uses standard HTTP status codes and returns one consistent JSON envelope for every non-2xx response (schema ErrorResponse in the OpenAPI spec).
Error Response Format
{
"error": "not_found",
"message": "Invoice '01912345-abcd-7000-8000-000000000042' not found",
"code": "NOT_FOUND_006",
"request_id": "3f0e7c2a-4b1d-4a8e-9c2f-1d2e3f4a5b6c"
}| Field | Description |
|---|---|
error |
Machine-readable category: validation_error, unauthorized, forbidden, not_found, conflict, rate_limited, server_error, service_unavailable |
message |
Human-readable explanation. May change between releases — don't branch on it |
code |
Stable, specific code in the form CATEGORY_NNN (e.g. AUTH_010, NOT_FOUND_006). Branch on this |
request_id |
Correlation ID for this request; also returned in the X-Request-ID response header. Quote it when contacting support |
errors |
Only on 400/422 validation failures: a map of field name → message |
request_id echoes your X-Request-ID request header when you send one; otherwise DayZero generates it.
Validation errors name the offending fields:
{
"error": "validation_error",
"message": "Validation failed: due_date: Input should be a valid date",
"errors": {
"due_date": "Input should be a valid date"
},
"code": "GEN_002",
"request_id": "3f0e7c2a-4b1d-4a8e-9c2f-1d2e3f4a5b6c"
}Send Accept: application/json on every request. A request whose Accept header prefers text/html (browser navigation) gets an HTML error page instead of JSON, so stating JSON removes the ambiguity.
Status Codes
| Status | error |
Typical code |
When |
|---|---|---|---|
200 |
— | — | Request completed |
201 |
— | — | Resource created |
400 |
validation_error |
GEN_002, VALIDATION_* |
Request body or parameters failed schema validation, or a business-rule violation; errors names the fields |
401 |
unauthorized |
AUTH_001, AUTH_008 (missing x-business-id), AUTH_010 (no access to that business) |
Missing, invalid, expired or revoked token, or wrong business |
403 |
forbidden |
AUTH_002, AUTH_048 (token scope), AUTH_028 (IP not allowed), AUTH_070 (token limited to other businesses) |
Token valid but its scope, role, source IP or business limit doesn't permit the operation (e.g. a read_only token calling POST) |
404 |
not_found |
NOT_FOUND_* |
Resource doesn't exist — or belongs to a different business than x-business-id |
409 |
conflict |
CONFLICT_* |
Duplicate or state conflict (e.g. creating an invoice with a number another invoice already uses) |
413 |
— | BODY_TOO_LARGE |
Request body over the size limit. This response comes from a body-size guard, not the standard envelope: {"error": "Request body too large", "code": "BODY_TOO_LARGE"} |
422 |
validation_error |
GEN_002 |
Same shape as 400. Used for a malformed cursor query parameter (see Pagination) and for OAuth callbacks; ordinary request validation failures are 400 |
429 |
rate_limited |
GEN_005, AUTH_058 (the token's own request budget) |
Too many requests — wait for Retry-After when present, otherwise back off exponentially, then retry the same request |
500 |
server_error |
GEN_003 |
Unexpected error — retry idempotent requests with backoff; quote request_id to support |
503 |
service_unavailable |
GEN_004 |
Temporary maintenance or dependency outage — retry with backoff |
Retry guidance
- Retry
429,500,503with exponential backoff (start at 1–2 s, cap at ~30 s). On a429that carriesRetry-After, wait that many seconds instead. - Don't retry
400,401,403,404,409,413,422unchanged — the request itself is the problem. The one exception isVALIDATION_620(409, "another request with thisIdempotency-Keyis still running"): wait for itsRetry-Afterand resend. - Send an
Idempotency-Keyheader on everyPOST,PUTandPATCHyou might retry (see Idempotent retries). Without it, check whether the record already exists (list with the filter you would have used) before re-sending after a timeout or5xx.
Idempotent retries
Add Idempotency-Key: <unique string> (1-255 characters; a UUID per logical operation works) to a POST, PUT or PATCH. The first request runs normally and its JSON answer is kept for 24 hours. Repeating the request with the same key, path, caller and body returns that answer with the same status and Idempotent-Replayed: true, and nothing runs a second time, so a retry after a timeout cannot double-post.
| Situation | Response |
|---|---|
| Repeat of a request that succeeded | The original status and body, Idempotent-Replayed: true |
| Repeat while the first is still running | 409, VALIDATION_620, Retry-After: 2: wait and send it again |
| Same key, different body or query string | 400, VALIDATION_621: use a new key for a new request |
| Key empty or longer than 255 characters | 400, VALIDATION_622 |
Only successful (2xx) JSON answers up to 256 KB are kept. A request that failed, returned a file or stream, or crashed releases its key, so retrying it runs it again. File uploads (multipart), webhooks and MCP calls are not covered; MCP tool calls are deduplicated by the server itself. A key belongs to the credentials that sent it: a rotated session token or a different API token starts a new key space.
Common Mistakes
Wrong business context: Getting 404 on a resource you know exists? Check that x-business-id matches the business that owns it.
Missing business header: 401 with code: "AUTH_008" means x-business-id was required and absent. Only GET /api/v1/businesses (and a handful of user-level endpoints) work without it.
Amounts in dollars instead of cents: All monetary values are integers in cents. $150.00 should be sent as 15000; 150.00 fails validation.
Expired token: Tokens with an expiration return 401 once expired. Generate a new one from Settings → Developers.
Read-only token on a write: A token created with the read_only scope gets 403 on every non-GET request.