# Error Handling

> Understand DayZero API error responses and status codes.

- **Category:** Getting Started
- HTML version: https://www.ondayzero.com/docs/guides/error-handling
- All guides: https://www.ondayzero.com/docs/guides/index.md
- Endpoint catalog: https://www.ondayzero.com/docs/reference/index.md

---

# 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

```json
{
  "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 |

Validation errors name the offending fields:

```json
{
  "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. Errors are only rendered as HTML pages for browser-style `text/html` requests, but stating JSON removes the ambiguity.

## Status Codes

| Status | `error` | Typical `code` | When |
|--------|---------|----------------|------|
| `200` | — | — | Request completed |
| `201` | — | — | Resource created |
| `400` | `validation_error` | `GEN_002`, `VALIDATION_*` | Invalid parameters 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` | Token valid but its scope or role 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. voiding an invoice that has payments) |
| `422` | `validation_error` | `GEN_002` | Request body or parameters failed schema validation; `errors` names the fields |
| `429` | `rate_limited` | `GEN_005` | Too many requests — back off exponentially and 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`, `503` with exponential backoff (start at 1–2 s, cap at ~30 s).
- **Don't retry** `400`, `401`, `403`, `404`, `409`, `422` unchanged — the request itself is the problem.
- `POST` requests that create records have no idempotency key yet. Before re-sending one after a timeout or `5xx`, check whether the record already exists (list with the filter you would have used) to avoid duplicates.

## 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.
