# Errors

> Envelope, typed codes, rate limits, and versioning policy.

Source: https://invoia.io/developers/api/errors

The API uses HTTP-native status codes so `resp.ok` and standard
handling work correctly. Failure bodies always follow one shape:

```json
{
  "error": {
    "type": "validation_failed",
    "code": "VALIDATION_FAILED",
    "message": "Body failed schema validation.",
    "field_errors": {
      "name": ["Required"],
      "email": ["Invalid email address"]
    },
    "request_id": "req_01H…"
  }
}
```

Branch on `code`, not on `message` — `code` is stable; `message` is
free-form and may improve over time.

## Status code map [#status-code-map]

| Status | Family            | Typical code                               |
| ------ | ----------------- | ------------------------------------------ |
| `400`  | Bad request       | Malformed JSON                             |
| `401`  | Auth              | `UNAUTHENTICATED`                          |
| `403`  | Scope             | `INSUFFICIENT_SCOPE`                       |
| `404`  | Not found         | `RESOURCE_NOT_FOUND` / `LINE_NOT_FOUND`    |
| `409`  | Business conflict | see below                                  |
| `422`  | Validation        | `VALIDATION_FAILED` (with `field_errors`)  |
| `429`  | Rate limit        | `RATE_LIMITED` (with `Retry-After` header) |
| `500`  | Internal          | `INTERNAL_ERROR`                           |

## Typed 409 codes [#typed-409-codes]

Codes you can branch on to distinguish business-rule conflicts:

* `INVOICE_ALREADY_ISSUED` — you tried to edit a frozen invoice; raise a
  credit note.
* `INVOICE_NOT_ISSUED` — you tried to credit-note an unissued invoice.
* `SERIES_ENDED` — the series' end date is final (it has passed, or a credit note already reversed part of it) and cannot be moved.
* `OCCURRENCE_NOT_FOUND` — no current occurrence (past horizon,
  paused).
* `OCCURRENCE_SKIPPED` — the occurrence was explicitly skipped.
* `DELETE_BLOCKED_BY_BOOKINGS` — a customer delete blocked because
  Dinero has bookings against them.
* `DELETE_BLOCKED_BY_REFERENCES` — a customer or product delete blocked
  because Invoia still holds the record: invoices, recurring series or
  credit notes for a customer; invoice lines, series lines or one-time
  lines for a product. The `message` names which. Nothing was deleted on
  either side — the reference is permanent, so retrying will not help.

## request\_id [#request_id]

Every response — success or failure — includes a `request_id`. Log it
alongside your integration's own trace id. If you open a support
ticket, the `request_id` lets us find your exact call in seconds.

## Rate limiting [#rate-limiting]

Every response carries three rate-limit headers:

```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1717000000
```

On `429`, the response adds:

```
Retry-After: 12
```

Two buckets:

* **General** — reads and non-Dinero writes. A generous default.
* **Dinero-writes** — a separate, stricter bucket for endpoints that
  hit Dinero (`issue`, `credit-note`). This protects the expensive
  operations without throttling your reads.

Retry with exponential backoff on `Retry-After`. Do **not** retry on
`400/401/403/404/422` — those will not succeed on retry with the same
payload.

## Versioning [#versioning]

The URL major (`/v1`) is additive-only. Within a major, we may:

* Add new endpoints.
* Add new optional fields on request bodies.
* Add new fields on response bodies.
* Add new event types to webhooks.
* Add new error codes (branch defensively: an unknown `code` should
  fall through to a generic handler).

We will NOT, within `/v1`:

* Remove endpoints.
* Remove or rename fields.
* Change the meaning of an existing `code`.
* Tighten validation on an existing endpoint in a way that would
  reject payloads that used to succeed.

A breaking change ships as `/v2`. Both majors are supported in parallel
during a deprecation window announced in the changelog.
