Pagination
Paging is cursor-based, and the cursor is asymmetric: it goes inside page on the way in and comes back at the top level on the way out.
The loop
- Send
page: { "limit": 50 }.limitis 1–100 (both ends inclusive) and defaults to 50 when omitted. Anything outside that range —0,2.5, or101— is a400naming the bound it crossed. In particular an over-largelimitis not clamped down: a truncated page that looks like a whole one is a silent data loss for the caller, so asking for more than 100 is an error you read once rather than rows you never find out you missed. - Read
nextCursorfrom the top level of the reply. - Send it back as
page: { "cursor": "<nextCursor>" }. - Repeat until
nextCursoris absent. Absent means you have the last page.
Why a cursor and not an offset
An offset re-counts the collection on every request, so a row that is inserted or removed mid-scan shifts the window — and the page you fetch next either skips rows or repeats them, silently. A cursor names a position rather than a count, so it cannot do either.
What a cursor does not promise
- It is opaque. Do not parse it, derive one, or store one as a durable bookmark across days.
- Iteration is not snapshot-stable: a row created while you are paging lands wherever it belongs in the ordering, which may be a page you have already read.
Ordering is a different question, and it depends on the feed. Most collection reads have a fixed order — the page says which, for example "sorted by name ascending". fetchOrders lets you choose (see below). Either way the order is total, so two calls for the same page name the same rows.
For an exact backfill, still do not lean on the cursor alone: iterate contiguous [createdAtStartDate, createdAtEndDate) windows whose upper bound is in the past — such a window cannot gain rows while you page it — and reconcile on the row's own id. That is idempotent under any interleaving of concurrent writes, which paging alone is not.
Sorting the order feed
fetchOrders takes a sort, on the order's creation time, and it defaults to createdAtDesc — newest first, which is what you want for "what has arrived since I last polled".
sort: "createdAtAsc" with no cursor is the other question worth asking: your very first order. Send page: { "limit": 1 } with it, add storeId to ask it of one storefront, and the answer is one row rather than a sweep.
Ties on createdAt break on the order's id, so the ordering is total and offset paging over it is a genuine partition — no page repeats a row and none skips one.
How deep a cursor goes
On most feeds you page to the end. On fetchOrders the cursor stops at 10,000 orders; past that you get a 400 naming the number.
That is not a retention limit — every order we hold is still readable. It is a limit on one access pattern. Reaching row 10,001 by paging means re-counting ten thousand rows the database has already handed you, and it is the shape that gets expensive for us both. The cheaper way to ask the same question is a narrower [createdAtStartDate, createdAtEndDate) window, and windows have no width limit at all — so you lose nothing and your loader gets checkpoints for free.
Time windows page too
fetchOrders bounds an order's creation time with createdAtStartDate (inclusive) and createdAtEndDate (exclusive). There is no maximum window width and no rolling retention ceiling, so a warehouse backfill can be one wide window with the cursor loop above nested inside it, or a loop over narrower windows if your loader prefers to checkpoint. Both bounds are optional; omit both for all-time.
Creation time is what makes a window loop exact: an order is created once, so it falls in exactly one window, and contiguous windows partition your history with no overlap and no gap. Do not size the windows against when an order was picked up — that is a different timestamp, typically minutes to hours later, and this filter does not read it.