Pagination
Cursor pagination on every list, in both directions, and auto-pagination in the SDK.
Every list in the API is paginated with opaque cursors. Cursors stay correct while data changes underneath them: an item added or removed during your walk never makes you skip or repeat another.
A page
{
"object": "list",
"data": [{ "object": "source", "id": "src_034XqBaL2mRLTZPXyRbaUc" }],
"has_more": true,
"next_cursor": "eyJ2IjoxLCJpZCI6InNyY18wMzRYcUJhTDJtUkxUWlBYeVJiYVVjIn0"
}| Field | Meaning |
|---|---|
data | The items, at most limit of them. |
has_more | More items exist beyond this page, in the direction you are walking. |
next_cursor | Pass it back to continue in the same direction; null when nothing is left. |
Parameters
| Parameter | Meaning |
|---|---|
limit | Items per page, 1 to 100. Defaults to 20. |
starting_after | Continue after this cursor: the next page. |
ending_before | Continue before this cursor: walk the other way. |
Lists come newest first, unless the operation says otherwise (review pairs, for instance, come
most probable first). Give at most one of starting_after and ending_before.
Pass next_cursor back in the same parameter you used: starting_after to keep going
forwards, ending_before to keep going backwards. A cursor is opaque: do not build or parse one.
A cursor the list did not issue answers 400 invalid_request with an
errors[] entry whose code is invalid_cursor.
curl "https://api.furniops.com/v1/sources?limit=100" \
-H "Authorization: Bearer $FURNIOPS_API_KEY"
curl "https://api.furniops.com/v1/sources?limit=100&starting_after=$NEXT_CURSOR" \
-H "Authorization: Bearer $FURNIOPS_API_KEY"GET /v1/products takes only starting_after (it walks one way, and filters with
updated_since). GET /v1/categories answers the whole taxonomy in one response.
In the SDK
Auto-pagination
A list call returns a PagePromise. await it for the first page, or iterate it with
for await to walk every item; the next page is fetched only when the current one is used up:
// Iterating fetches each next page only when the current one is used up.
for await (const product of furniops.products.list({ updated_since: "2026-10-01T00:00:00Z" })) {
console.log(product.id);
}Breaking out of the loop stops fetching. Each page is one request, with the same retries as any call.
Page by page
let cursor: string | undefined;
do {
const page = await furniops.sources.list({ limit: 100, starting_after: cursor });
for (const source of page.data) console.log(source.id, source.name);
cursor = page.has_more ? (page.next_cursor ?? undefined) : undefined;
} while (cursor !== undefined);Backwards
// ending_before walks toward newer items; next_cursor keeps going that way.
const newer = await furniops.webhookEndpoints.list({ ending_before: "eyJ2IjoxfQ", limit: 10 });
console.log(newer.data.length, newer.next_cursor);The change feed is different
GET /v1/changes is a log, not a list: it takes cursor (not
starting_after), up to 1000 events a page, and answers caught_up instead of has_more. Its
cursor is meant to be saved and reused for weeks (up to the 30 days events are kept), where a list
cursor is for one walk.