> ## Documentation Index
> Fetch the complete documentation index at: https://docs.envoi.no/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors, idempotency and paging

> Rules that apply to every endpoint in the Envoi API.

## Format

* JSON with camelCase field names.
* Dates as `yyyy-MM-dd`, timestamps as ISO 8601 in UTC.
* Amounts in kroner as decimal numbers with two decimals. `currency` is always `NOK` in v1.
* VAT codes are the SAF-T standard codes. For sales: `3` = 25 %, `31` = 15 %, `33` = 12 %, `5` = exempt, `6` = outside the VAT Act, `7` = no VAT treatment, `52` = export.

## Errors

Every error from `/v1` has the same shape:

```json theme={null}
{ "code": "not-found", "message": "Not found." }
```

`code` is stable and safe to build logic on. `message` is an English explanation for developers and may change. Some errors also include `details` with more information.

Validation errors list the fields that are wrong:

```json theme={null}
{
  "code": "validation-failed",
  "message": "The request is not valid.",
  "errors": [ { "field": "lines[0].quantity", "code": "greater-than" } ]
}
```

### Errors any endpoint can return

| Status | `code` | When |
| - | - | - |
| 401 | `unauthorized` | No token, or the token is invalid, expired or refused |
| 403 | `insufficient_scope` | The client lacks the permission the endpoint needs |
| 404 | `not-found` | Does not exist in this company and environment |
| 409 | `account-closing` | The company is being closed |
| 409 | `api-client-owner-missing` | The user who created the client no longer exists. Create a new client |
| 413 | `payload-too-large` | A POST body larger than 1 MB |
| 429 | `rate_limited` | More than 600 calls a minute from one client. `Retry-After` says when to try again |
| 500 | `server-error` | Something unexpected went wrong. Retrying with the same `Idempotency-Key` is safe |

<Note>
  An ID from another company, or from the other environment, always returns `404`, never `403`. The API does not reveal that something exists elsewhere.
</Note>

## Idempotency

Every `POST` requires the `Idempotency-Key` header. Use a new UUID for each action. If the network fails and you don't know whether a call went through, send the same call again with the same key. Nothing happens twice.

```bash theme={null}
curl https://api.envoi.no/v1/invoices/$INVOICE_ID/send \
  -X POST \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: 6f1c2d0e-7b1a-4c55-9e0f-2a8d4b3c9e71"
```

| Situation | Answer |
| - | - |
| No header | `400 idempotency-key-required` |
| Key longer than 255 characters | `400 idempotency-key-too-long` |
| First use of the key | The call runs, and its answer is stored |
| Same key, same call | The stored answer again, with the header `Idempotency-Replayed: true` |
| Same key, different body or path | `422 idempotency-key-reused` |
| Same key while the first call is still running | `409 idempotency-key-in-progress`, with `Retry-After: 1` |
| The first call ended in a 5xx error | Nothing is stored, and you can retry with the same key |

Keys belong to one client in one environment and are kept for **7 days**. After that, the same key counts as a new call. The exception is payments: a payment with the same key is never recorded twice, however old the key is.

<Warning>
  Answers in the 4xx range are stored too. If you get `409 company-not-verified` when sending an invoice, for example, use a **new** key when you retry after the company has been verified.
</Warning>

## Paging

Lists are split into pages. Use `page` (from 1) and `pageSize` (1 to 100, default 20, 50 for journal entries).

```json theme={null}
{ "items": [ … ], "page": 1, "pageSize": 20, "totalCount": 57, "hasMore": true }
```

Keep going until `hasMore` is `false`. If new rows arrive while you page, later pages can shift by one. To keep another system in sync, use `updatedSince` on invoices instead and fetch what has changed since last time.
