NewProvince is only required for addresses in Spain
BeeL
Get startedMulti-NIFVeriFactuRulesStripeAPI referenceChangelog

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": "…" }
}
ParameterDefaultRange
page1from 1
limit201–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:

Collectionsort_by valuesDefault
Customerslegal_name, nif, email, phone, city, province, active, created_atlegal_name, asc
Productsname, code, category, default_price, created_atname, asc
Recurring invoicesname, next_generation, status, created_atcreated_at, desc
Email deliveriessent_at, status, email_typesent_at, desc
Invoicesissue_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_atissue_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/accounts 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 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 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

LimitValueWhat you get
JSON request body2 MB413 REQUEST_BODY_TOO_LARGE
Any request body, at the network edge8 MB413 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:

UploadFormatSizeRows
Company logoJPEG or PNG1 MB—
Customer import, source: csvCSV from the import template5 MB1,000
Customer import, source: holdedHolded contacts .xlsx10 MB5,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

OperationMaximum per requestAtomic?
Invoice batch (ISSUE, STATUS)50 invoicesNo
Invoice delivery (one email, many PDFs)200 invoicesNo
PDF archive (one ZIP)500 invoicesNo
Invoice export50,000 invoices—
Create customers500 customersYes
Create products100 productsNo
Delete customers100 IDsNo
Delete products100 IDsNo

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.

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.