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. For how many emails you can send, see Sending email. Neither is repeated here.
Pagination
Every collection travels under a named key inside data, next to its pagination block:
{
"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, with the parameter in error.details.
A page past the end is not an error: it comes back empty, with has_next: false.
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.
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:
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 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 | legal_name, nif, email, phone, city, province, active, created_at | legal_name, asc |
| Products | name, code, category, default_price, created_at | name, asc |
| Recurring invoices | name, next_generation, status, created_at | created_at, desc |
| Email deliveries | sent_at, status, email_type | sent_at, desc |
| Invoices | 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.
A sort_by outside the collection's vocabulary is rejected with
400 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.
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, 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, payment connections or the VeriFactu records of an invoice. You get the whole list in one response.
- Cursor collections page with an opaque cursor instead of a page number.
GET /v1/accountsreturnsdata.accountsanddata.next_cursor(limitdefaults to 50, up to 200); sendnext_cursorback ascursoruntil it comes backnull. Request logs also page by cursor:data.request_logscomes with apaginationblock that carriesnext_cursor/prev_cursorandhas_next/has_previousinstead of page numbers. There is no jump to page N on either.
Invoice series 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.
# 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 |
| Any request body, at the network edge | 8 MB | 413 PAYLOAD_TOO_LARGE |
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 | JPEG or PNG | 1 MB | — |
Customer import, source: csv | CSV from the import template | 5 MB | 1,000 |
Customer import, 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 with file_size_mb and
max_file_size_mb, or 400 TOO_MANY_RECORDS with record_count
and max_records. An oversized logo answers
422 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 (ISSUE, STATUS) | 50 invoices | No |
| Invoice delivery (one email, many PDFs) | 200 invoices | No |
| PDF archive (one ZIP) | 500 invoices | No |
| Invoice export | 50,000 invoices | — |
| Create customers | 500 customers | Yes |
| Create products | 100 products | No |
| Delete customers | 100 IDs | No |
| Delete products | 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
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.
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.