# Pagination

> List endpoints page with limit and offset, and tell you whether there is more with has_more and next_offset.

Lists are returned in pages. Pass `limit` and `offset`, and read `has_more` and `next_offset` from the response; `next_offset` is `null` on the last page.

| List | `limit` |
| --- | --- |
| Journal entries, attachments, bank lines, products, invoices, contacts | 1 to 500, default 100 |
| Ledger entries | 1 to 1000, default 1000 |

A `limit` or `offset` out of range returns `400 invalid_request`, except on invoices and contacts, which bring it within range.

```json title="GET /v1/contacts?limit=2" {6-7}
{
  "contacts": [
    { "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Acme ApS" },
    { "id": "550e8400-e29b-41d4-a716-446655440002", "name": "Brdr. Hansen I/S" }
  ],
  "has_more": true,
  "next_offset": 2
}
```

Keep the same filters and pass `next_offset` as `offset` until `has_more` is `false`:

```javascript title="JavaScript"
async function* allContacts() {
  let offset = 0;
  while (offset !== null) {
    const response = await fetch(`https://api.numina.app/v1/contacts?limit=500&offset=${offset}`, {
      headers: { Authorization: `Bearer ${process.env.NUMINA_API_KEY}` },
    });
    const page = await response.json();
    yield* page.contacts;
    offset = page.has_more ? page.next_offset : null;
  }
}
```

> [!WARNING]
> Offsets are not a snapshot. A contact created or deleted while you page can shift records between pages, so de-duplicate by `id` when completeness matters.

The chart of accounts, VAT codes and accounting years are short and come back in one response, as do the balance reports.
