Pagination
Every list endpoint in the v1 API uses cursor-based pagination. Cursors are opaque to clients -- they're HMAC-signed tokens encoding the sort key of the last record on the page. You don't need to (and shouldn't) decode or construct them yourself.
Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Max | Description |
| --------- | ------- | ------- | --- | ---------------------------------------------------------------------- |
| limit | integer | 25 | 100 | Number of records to return per page. |
| cursor | string | - | - | Opaque cursor returned by a previous page in meta.pagination.cursor. |
Response shape
Section titled “Response shape”List responses include a pagination object inside meta:
{ "data": [ /* records */ ], "meta": { "request_id": "req_8f3c1b2a", "pagination": { "cursor": "eyJpZCI6NDIsImMiOiIyMDI2LTA0LTAxVDE1OjMyOjExWiJ9.7e4f...", "has_more": true, "count": 25 } }}cursor-- Pass to the next request to get the next page.nullon the last page.has_more--trueif there are more records after this page.count-- Number of records in the current page.
Why keyset cursors?
Section titled “Why keyset cursors?”Cursors encode the sort key of the last seen record (id + createdAt) and are HMAC-signed so the server can detect tampering. Compared to offset/limit pagination, keyset cursors give you:
- Consistency under writes. New records inserted while you're paginating won't shift items between pages or cause duplicates.
- Stable performance. The server can use an indexed range scan instead of skipping rows. Pages 1 and 1000 cost the same.
- No skew on deletions. Soft-deleted records don't shift the offset.
Looping through every page
Section titled “Looping through every page”async function* listAllContacts(apiKey) { let cursor = null; do { const url = new URL('https://app.buildworkpro.com/api/v1/contacts'); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const body = await res.json();
for (const contact of body.data) yield contact;
cursor = body.meta.pagination.has_more ? body.meta.pagination.cursor : null; } while (cursor);}
for await (const contact of listAllContacts(process.env.BUILDWORKPRO_API_KEY)) { console.log(contact.email);}