FurniOpsDocs

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"
}
FieldMeaning
dataThe items, at most limit of them.
has_moreMore items exist beyond this page, in the direction you are walking.
next_cursorPass it back to continue in the same direction; null when nothing is left.

Parameters

ParameterMeaning
limitItems per page, 1 to 100. Defaults to 20.
starting_afterContinue after this cursor: the next page.
ending_beforeContinue 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:

all.ts
// 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

pages.ts
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.

Lists that paginate

ListDirectionsFilters
GET /v1/agreement-acceptancesbothnone
GET /v1/assistant/conversationsforwards onlynone
GET /v1/audit-eventsbothaction, actor, since, until
GET /v1/billing/credits/entriesbothkind, from, to
GET /v1/billing/credits/purchasesbothnone
GET /v1/billing/invoicesbothnone
GET /v1/brand/claimsbothnone
GET /v1/catalogue/imagesbothstatus
GET /v1/catalogue/publication-runsbothnone
GET /v1/catalogue/variantsbothq, product, availability, issue
GET /v1/changes/recentforwards onlytypes, catalogues
GET /v1/exportsbothnone
GET /v1/match-pairsbothstatus
GET /v1/mergesbothstatus, product
GET /v1/offersforwards onlyvariant, product, source, availability, updated_since
GET /v1/productsforwards onlyupdated_since, status, brand, category, availability, catalogue, source, q
GET /v1/proposalsbothstatus, agent, source
GET /v1/request-logsbothstatus, method, actor, path, since, until
GET /v1/source-probesbothnone
GET /v1/sourcesbothnone
GET /v1/sources/{id}/inbound-messagesbothnone
GET /v1/sources/{id}/mappingsbothnone
GET /v1/sources/{id}/runsbothnone
GET /v1/vendor-profilesbothq, domain, status, verified
GET /v1/webhook-endpointsbothnone
GET /v1/webhook-endpoints/{id}/deliveriesbothstatus, type, event

On this page