# Errors

> Every error has the same JSON shape with a stable machine-readable code. Branch on the code, show the description, and read details for field errors.

Numina uses HTTP status codes for the class of failure and a stable `error` code for the exact reason. Branch on `error`; `error_description` is for people and may change, and can be in Danish.

```json title="400 Bad Request"
{
  "error": "validation_error",
  "error_description": "Journal entries do not balance",
  "details": [
    {
      "field": "lines",
      "message": "Sum of amounts must equal zero."
    }
  ]
}
```

| Field | |
| --- | --- |
| `error` | Machine-readable code, listed below |
| `error_description` | A sentence for logs and people |
| `details` | For field errors: one entry per field, with a `field` path such as `lines.0.account_number` |
| `scope` | On `insufficient_scope`: the scopes the route asks for, separated by spaces |

## Codes

| Status | `error` | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | The body or query is malformed, or names an account, VAT code or contact the company does not have |
| 400 | `validation_error` | Well-formed, but breaks an accounting rule: unbalanced lines, a closed accounting year, an incomplete invoice |
| 400 | `payment_details_required` | An invoice cannot be booked without a bank account to be paid into. Pass `bank_account_id` when you create it, or have the company choose a default bank account for invoices |
| 400 | `missing_file`, `invalid_file`, `file_too_large`, `upload_error` | An upload has no file, one that is not a PDF, PNG or JPEG, one over 10 MB, or more than 10 files |
| 400 | `range_crosses_accounting_year` | A balance period spans two accounting years |
| 401 | `unauthorized` | No token, or an `Authorization` header that is not `Bearer` |
| 401 | `invalid_token` | A token that is wrong, expired or revoked |
| 403 | `insufficient_scope` | The token lacks a scope this endpoint needs; `scope` names them |
| 403 | `booking_not_available` | Booking is not available to Fuld Service customers |
| 403 | `company_archived` | The company is archived |
| 403 | `forbidden` | A sandbox company cannot send invoices by email |
| 404 | `not_found` | The resource does not exist in this company |
| 404 | `accounting_year_not_found` | No accounting year covers the requested period |
| 409 | `conflict` | The request clashes with the resource's current state: the entry is booked, Numina's bookkeeping agent is working on it, or the reference or product number is taken |
| 409 | `attachment_exists`, `attachment_not_empty` | The entry already has a document, or the document already has files |
| 409 | `attachment_required` | A journal entry needs a document before it can be booked |
| 409 | `idempotency_key_in_use` | A request with this `Idempotency-Key` is still running |
| 422 | `idempotency_key_reused` | This `Idempotency-Key` was used with a different body or endpoint |
| 429 | `rate_limit_exceeded` | Too many requests; see [Rate limits](https://developers.numina.app/rate-limits.md) |
| 500 | `internal_error` | Something failed on our side |

A `404 not_found` with the description `Not found` on any route means the OAuth user who connected your app is no longer a member of the company. Someone who is a member has to connect it again.

A few failures are not JSON: a body that is not valid JSON returns `400` with the text `Bad request`, a JSON body over 4 MB returns `500`, and an unknown path returns an HTML `404`.

## Retrying

> [!TIP]
> Retry `429` and `5xx` responses with exponential backoff. Do not retry other `4xx` responses unchanged: they fail the same way until the request is fixed.

To retry a create safely after a timeout, send an [`Idempotency-Key`](https://developers.numina.app/idempotency.md).
