Pagination
All list endpoints return a page at a time, with a cursor that points at the next page. Cursors are stable across inserts — a cursor returned to you yesterday still works today (within the retention window).
Request
GET /v1/customers?limit=50&cursor=eyJpZCI6Ij…| Param | Default | Max | Notes |
|---|---|---|---|
limit | 25 | 100 | Page size. |
cursor | — | — | Opaque token from the previous response’s next_cursor. |
Response
{ "data": [ … 50 items … ], "has_more": true, "next_cursor": "eyJpZCI6IjAxSDJIRzM…"}When has_more is false, you’ve reached the end; next_cursor is
null.
Walking all pages
cursor = Nonewhile True: res = api.get("/v1/customers", params={"limit": 100, "cursor": cursor}) for c in res["data"]: process(c) if not res["has_more"]: break cursor = res["next_cursor"]Sorting
The default sort is created_at descending (newest first). To change:
GET /v1/customers?sort=created_at:ascAllowed fields per resource — typically created_at, updated_at, and
the resource’s natural key (code, email). The Resources docs list
each one.
Filtering
Most list endpoints support filters. Filters are passed as query parameters; their names match the field name on the resource:
GET /v1/subscriptions?status=active,past_dueGET /v1/invoices?customer_id=cus_…&status=openGET /v1/payments?created_at[gte]=2026-05-01T00:00:00Z| Operator | Syntax | Example |
|---|---|---|
| equals | field=v | status=active |
| in | field=v1,v2 | status=active,past_due |
| greater-equal | field[gte]=v | created_at[gte]=… |
| less-than | field[lt]=v | total[lt]=100 |
| matches prefix | field[starts]=v | email[starts]=ada@ |
Filters compose with AND. There’s no OR across fields; for that, fall back to multiple list calls and union client-side.
Cursor stability
A cursor encodes the position of an item in the current sort. As long as that item still exists and the sort hasn’t been changed, the cursor is stable.
- New items inserted after the cursor: appear on the next page.
- Items deleted between cursor and current page: skipped silently.
- Sort changed between calls: cursor invalidated; you’ll get
pagination.cursor_invalid.
Cursors don’t expire on a clock — they’re valid as long as the underlying data is queryable. In practice, that’s at least 30 days.
Limits and total counts
Paylera does not return a total count on list responses. Counting all
records of a resource is O(n) on a large tenant; we don’t pay that
cost on every request. If you need a count, query a report endpoint
(GET /v1/reports/*) which is purpose-built for aggregation.
Streaming exports
For very large queries (full customer export, full invoice history), use the export endpoints:
POST /v1/exports{ "resource": "invoices", "filters": { "created_at[gte]": "2026-01-01T00:00:00Z" }, "format": "csv"}The response includes an export_id. Poll
GET /v1/exports/{id} until status: completed, then download from
download_url (signed, single-use, 24-hour TTL).
Exports stream, don’t buffer; multi-million-row queries are routine.
Common pitfalls
- Storing an absolute index (“page 5”). Pages don’t have stable numbers; cursors do.
- Walking with overlapping filters and treating “no more” as “done
for the day.” A cursor that returned
has_more: falselast hour may have new items now. Restart fromnullfor fresh data. - Page-walking in parallel from the same cursor. Cursors are unique to a position, but racing two consumers from the same cursor doesn’t get you parallelism — each gets the same next page. Shard by filter (date range, status) instead.