# Limits and pagination

How collections page and sort, and every size and batch limit the BeeL. API enforces, in one place.

Most integrations hit a limit before they hit a bug: a list that stops at 20 items, a body
that is too large, a batch one row too long. This page gathers the ceilings you can run
into and how to walk a collection to its end.

For how many **requests** you can make per minute, see [Rate limits](/guides/rate-limits).
For how many **emails** you can send, see [Sending email](/guides/sending-email). Neither is
repeated here.

## Pagination

Every collection travels under a named key inside `data`, next to its `pagination` block:

```json
{
  "success": true,
  "data": {
    "invoices": [ /* … */ ],
    "pagination": {
      "current_page": 2,
      "total_pages": 5,
      "total_items": 87,
      "items_per_page": 20,
      "has_next": true,
      "has_previous": true
    }
  },
  "meta": { "request_id": "…", "timestamp": "…" }
}
```

| Parameter | Default | Range |
|---|---|---|
| `page` | `1` | from `1` |
| `limit` | `20` | `1`–`100` |

The response echoes what it applied: `page` as `pagination.current_page` and `limit` as
`pagination.items_per_page`. A value outside the range is not clamped: a `limit` above `100` or
below `1`, or a `page` below `1`, answers `422` [`VALIDATION_ERROR`](/errors/VALIDATION_ERROR), with the parameter in `error.details`.
A page past the end is not an error: it comes back empty, with `has_next: false`.

<Callout type="warn">
  **Without `limit` you get 20 items, not all of them.** A list that "only returns 20" is
  a list you have not paged through yet. Read `has_next`, not the length of the array.
</Callout>

An empty collection has **one** page, the first one and empty: `total_pages` is `1` and
`total_items` is `0`. To know whether anything came back, read `total_items`.

### Walking every page

Ask for the largest page and keep going while `has_next` is `true`:

```ts
async function listAllCustomers(companyId: string, apiKey: string) {
  const customers = [];
  let page = 1;

  while (true) {
    const res = await fetch(
      `https://app.beel.es/api/v1/companies/${companyId}/customers?page=${page}&limit=100`,
      { headers: { Authorization: `Bearer ${apiKey}` } },
    );
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const { data } = await res.json();
    customers.push(...data.customers);

    if (!data.pagination.has_next) break;
    page += 1;
  }

  return customers;
}
```

A full walk of a large collection is many requests. Pace it against the
[rate limits](/guides/rate-limits) and handle a `429` by waiting `Retry-After`, then
retrying the same page.

### Sorting

Lists that can be ordered take `sort_by` and `sort_order` (`asc` or `desc`). The accepted
fields depend on the collection:

| Collection | `sort_by` values | Default |
|---|---|---|
| [Customers](/customers/listCompanyCustomers) | `legal_name`, `nif`, `email`, `phone`, `city`, `province`, `active`, `created_at` | `legal_name`, `asc` |
| [Products](/products/listCompanyProducts) | `name`, `code`, `category`, `default_price`, `created_at` | `name`, `asc` |
| [Recurring invoices](/recurring-invoices/listCompanyRecurringInvoices) | `name`, `next_generation`, `status`, `created_at` | `created_at`, `desc` |
| [Email deliveries](/emails/listAccountEmailDeliveries) | `sent_at`, `status`, `email_type` | `sent_at`, `desc` |
| [Invoices](/invoices/listCompanyInvoices) | `issue_date`, `operation_date`, `due_date`, `invoice_number`, `series_code`, `status`, `invoice_total`, `taxable_base`, `total_vat`, `total_equivalence_surcharge`, `total_discounts`, `recipient_name`, `recipient_nif`, `created_at`, `updated_at` | `issue_date`, `desc` |

Results are tie-broken by a stable key, so paging through a collection sorted on a field
with repeated values never repeats or skips an item.

<Callout type="warn">
  A `sort_by` outside the collection's vocabulary is rejected with
  `400` [`VALIDATION_ERROR`](/errors/VALIDATION_ERROR), and `error.details` names `sort_by` and lists
  the accepted fields. A `sort_order` other than `asc` or `desc` is rejected the same way. This
  holds for invoices too: an unknown `sort_by` there used to come back silently in the default
  order.
</Callout>

### Filters that take a list

A filter that accepts several values takes them comma-separated (`status=DRAFT,ISSUED`) or
repeated (`status=DRAFT&status=ISSUED`). An empty element in the list (`status=ISSUED,`,
`status=DRAFT,,ISSUED` or `status=ISSUED&status=`) is rejected with
`400` [`VALIDATION_ERROR`](/errors/VALIDATION_ERROR), and `error.details` names the parameter. The
parameter empty on its own (`status=`) still means no filter.

### Collections that do not page

Two kinds of collection carry no `pagination` block, and each operation says which it is:

- **Closed catalogues** are fixed, bounded lists with nothing to page through — for example
  [customisation options](/invoice-customization/listInvoiceCustomizationOptions),
  [payment connections](/payment-connections/listCompanyPaymentConnections) or the
  [VeriFactu records of an invoice](/invoices/listCompanyInvoiceVerifactuRecords). You get
  the whole list in one response.
- **Cursor collections** page with an opaque cursor instead of a page number.
  [`GET /v1/accounts`](/accounts/listAccounts) returns `data.accounts` and `data.next_cursor`
  (`limit` defaults to 50, up to 200); send `next_cursor` back as `cursor` until it comes
  back `null`. [Request logs](/request-logs/listAccountRequestLogs) also page by cursor:
  `data.request_logs` comes with a `pagination` block that carries `next_cursor` /
  `prev_cursor` and `has_next` / `has_previous` instead of page numbers. There is no jump to
  page N on either.

[Invoice series](/invoice-series/listCompanySeries) are the one opt-in case: omit `page`
and `limit` and you get every series and no `pagination`; send either and you get a page
with its `pagination` block.

```bash
# Next page of managed accounts
curl "https://app.beel.es/api/v1/accounts?limit=200&cursor=eyJpZCI6Ij..." \
  -H "Authorization: Bearer $BEEL_API_KEY"
```

Send the cursor exactly as you received it. A hand-built or truncated cursor is rejected.

## Request size

| Limit | Value | What you get |
|---|---|---|
| JSON request body | 2 MB | `413` [`REQUEST_BODY_TOO_LARGE`](/errors/REQUEST_BODY_TOO_LARGE) |
| Any request body, at the network edge | 8 MB | `413` `PAYLOAD_TOO_LARGE` |

[`REQUEST_BODY_TOO_LARGE`](/errors/REQUEST_BODY_TOO_LARGE) carries the limit and the size you declared in `error.details`
(`max_size_bytes`, `max_size_formatted`, `request_size_bytes`). The byte counts arrive as
strings (for example `"2097152"`), so parse them before comparing. The 8 MB rejection happens
before the request reaches the API, which is why it has its own code.

The largest body the API expects is a bulk creation at its maximum number of items, which
stays well under 2 MB. If you are close, split the batch.

There is **no cap on the number of lines of an invoice**: the only bound is the 2 MB body. An
invoice with 900 lines is accepted.

### File uploads

File uploads (`multipart/form-data`) are not bound by the 2 MB JSON limit. Each upload has
its own limit instead:

| Upload | Format | Size | Rows |
|---|---|---|---|
| [Company logo](/companies/uploadCompanyLogoById) | JPEG or PNG | 1 MB | — |
| [Customer import](/customers/createCompanyCustomerImport), `source: csv` | CSV from the import template | 5 MB | 1,000 |
| [Customer import](/customers/createCompanyCustomerImport), `source: holded` | Holded contacts `.xlsx` | 10 MB | 5,000 |

A file over either limit of its `source` — CSV or Holded — is rejected as a whole, before any
row is read: nothing is imported and no row is reported. The reason is in `error.code` —
`400` [`CSV_FILE_TOO_LARGE`](/errors/CSV_FILE_TOO_LARGE) with `file_size_mb` and
`max_file_size_mb`, or `400` [`TOO_MANY_RECORDS`](/errors/TOO_MANY_RECORDS) with `record_count`
and `max_records`. An oversized logo answers
`422` [`FILE_TOO_LARGE`](/errors/FILE_TOO_LARGE). Compare `error.code`, not the status, to tell the
cases apart.

The 8 MB edge limit applies to uploads too, so a Holded export larger than 8 MB is refused with
`413` `PAYLOAD_TOO_LARGE` before its own 10 MB limit is checked. Split larger exports.

## Batch sizes

| Operation | Maximum per request | Atomic? |
|---|---|---|
| [Invoice batch](/invoices/createCompanyInvoiceBatch) (`ISSUE`, `STATUS`) | 50 invoices | No |
| [Invoice delivery](/invoices/createCompanyInvoiceDelivery) (one email, many PDFs) | 200 invoices | No |
| [PDF archive](/invoices/createCompanyInvoicePdfArchive) (one ZIP) | 500 invoices | No |
| [Invoice export](/invoices/createCompanyInvoiceExport) | 50,000 invoices | — |
| [Create customers](/customers/createCompanyCustomersBulk) | 500 customers | **Yes** |
| [Create products](/products/createCompanyProductsBulk) | 100 products | No |
| [Delete customers](/customers/deleteCompanyCustomersBulk) | 100 IDs | No |
| [Delete products](/products/deleteCompanyProductsBulk) | 100 IDs | No |

Going over the maximum rejects the request before anything is processed. An export over
50,000 invoices is rejected with `422` [`EXPORT_LIMIT_EXCEEDED`](/errors/EXPORT_LIMIT_EXCEEDED)
and is never silently truncated: narrow the date range or split the selection.

How each batch reports its rows, and what its status code means, is covered in
[Bulk operations and exports](/guides/bulk-operations-and-exports).

## Email

Sending is limited per account, separately from request rate limits, on several axes
(emails per hour and per day, distinct recipients, sends of the same invoice). The numbers,
how the windows count and how a bulk delivery counts are in
[Sending email](/guides/sending-email#send-quotas).

## Related

<Related>

- [Rate limits](/guides/rate-limits) — how many requests per window
- [Bulk operations and exports](/guides/bulk-operations-and-exports) — working within the batch sizes
- [Sending email](/guides/sending-email) — the email quotas in detail

</Related>

---

Full OpenAPI spec: https://docs.beel.es/api/openapi