Billabex public API list endpoints use cursor-based pagination.
This guide explains how the model works, why it is designed this way, and what
your client should implement to paginate safely and efficiently in production.
Overview
Pagination relies on two query parameters:
first– number of items to returnafter– opaque cursor returned by the previous page
Each paginated response includes:
nodes– items for the current pagepageInfo.endCursor– cursor to request the next page
There is no page number, offset, or total count.
Why Cursor-Based Pagination?
Cursor pagination is designed for consistency and scalability.
It avoids common problems of offset-based pagination when data changes between requests.
Key benefits:
- Stable pagination when items are created or deleted
- No expensive offset scans at scale
- Predictable performance on large datasets
Design Choices and Integration Impact
| Design choice | Why this choice | What you should do |
|---|---|---|
Cursor pagination (first + after) | Consistent results when data changes | Always chain requests using the latest endCursor |
| No offset or page number | Avoids expensive database scans | Do not build UX around page indexes |
No totalCount or hasNextPage | Smaller, faster responses | Stop pagination when nodes.length === 0 |
| Endpoint-defined ordering | Ordering depends on storage/index | Do not assume a global sort unless documented |
Request Parameters
Core pagination parameters:
| Parameter | Type | Required | Notes |
|---|---|---|---|
first | integer | No | Must be >= 1 |
after | string | No | Opaque cursor from pageInfo.endCursor |
Additional required filters depend on the endpoint:
- Most list endpoints require
organizationId - Account-scoped endpoints include
accountIdin the path GET /organizationsdoes not requireorganizationId
Always refer to the API Reference for endpoint-specific requirements.
Response Contract
Example response:
{
"nodes": [{ "id": "..." }, { "id": "..." }],
"pageInfo": {
"endCursor": "eyJpZCI6IjEyM2U0NTY3LWU4OWItMTJkMy1hNDU2LTQyNjYxNDE3NDAwMCJ9"
}
}
Important behaviors to understand:
endCursoris a continuation token, not a signal that more data exists- The last non-empty page may still return an
endCursor - An empty page (
nodes.length === 0) is the only reliable stop condition
Basic Pagination Flow
First request:
GET [baseURL]/api/public/v1/accounts?organizationId=YOUR_ORG_ID&first=50
Authorization: Bearer YOUR_ACCESS_TOKEN
Next request (using the previous cursor):
GET [baseURL]/api/public/v1/accounts?organizationId=YOUR_ORG_ID&first=50&after=PREVIOUS_END_CURSOR
Authorization: Bearer YOUR_ACCESS_TOKEN
Repeat until an empty page is returned.
Recommended Client Pattern
The example below demonstrates a safe production-ready pagination loop.
async function fetchAllAccounts({ baseUrl, accessToken, organizationId }) {
const all = [];
let after;
while (true) {
const url = new URL(`${baseUrl}/api/public/v1/accounts`);
url.searchParams.set('organizationId', organizationId);
url.searchParams.set('first', '50');
if (after) url.searchParams.set('after', after);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// Optional: integrate with rate-limiting behavior
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') || '1');
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
continue;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
all.push(...data.nodes);
if (!Array.isArray(data.nodes) || data.nodes.length === 0) {
break;
}
after = data.pageInfo?.endCursor;
if (!after) {
break;
}
}
return all;
}
Choosing a Page Size (first)
There is no universal optimal value.
first directly impacts:
- Response size
- Latency
- Client memory usage
- Rate-limit consumption
Recommended starting values:
20–50for UI-driven flows50–100for backend batch jobs
Adjust based on payload size and real traffic patterns.
Interaction with Rate Limiting
Pagination can increase request volume quickly.
Best practices:
- Prefer fewer, larger pages over many small ones
- Combine pagination with a per-token request queue
- Watch
X-RateLimit-Remainingheaders - Avoid parallel pagination with the same access token
See the Rate Limiting guide for details.
Common Mistakes
- Treating cursors as readable or stable identifiers
- Reusing a cursor after changing filters
- Mixing cursors from different endpoints
- Assuming a missing
endCursormeans “no more data” - Requesting very large page sizes by default
Paginated Endpoints (Public API)
Common paginated endpoints include:
GET /organizationsGET /accountsGET /invoicesGET /credit-notesGET /emailsGET /incoming-emailsGET /outgoing-emailsGET /incoming-email-communicationsGET /outgoing-email-communicationsGET /customer-outstanding-balancesGET /accounts/:accountId/incoming-email-communicationsGET /accounts/:accountId/outgoing-email-communications
Always consult the API Reference for the definitive list.
Next Steps
- Getting Started – End-to-end integration flow
- OAuth Authentication – Token lifecycle and scopes
- Rate Limiting – Token quota and retry behavior
- API Reference – Endpoint schemas and examples
Support
Questions about pagination behavior?
Contact us via the website contact form.