Pagination & time ranges
How to walk deliveries and collection records with an opaque cursor, and why one takes a time range and the other does not.
Two endpoints paginate, and they do it differently on purpose:
| List | Range | Order | Cursor is bound to |
|---|---|---|---|
| Deliveries | from and to, mandatory, at most 366 days | Newest first | The range you asked for |
| Collection records | none — you walk the whole collection | Oldest change first | Restaurant, collection and limit |
Both answer { "data": [...], "nextCursor": string | null }, both cap limit at
100 with a default of 50, and both want the cursor handed back untouched. The
rest of this page is the delivery list; the
collections page covers the other.
Deliveries are immutable and interesting by date, so they take a range. Collection records change in place, so a range over them would be a lie — you walk the collection and reconcile.
The delivery list
It takes a mandatory, explicit time range and returns a page plus an opaque cursor.
GET /v1/restaurants/{restaurantId}/deliveries
?from=1787846400000
&to=1788451200000
&limit=50
&cursor=eyJpZCI6…
| Parameter | Required | Rules |
|---|---|---|
from | yes | Epoch milliseconds, as a string of digits. Inclusive. |
to | yes | Epoch milliseconds, as a string of digits. Inclusive. |
limit | no | 1 to 100. Defaults to 50. |
cursor | no | The nextCursor of the previous page, verbatim. |
to must be greater than or equal to from, and the span must be at most
366 days. Anything else answers 400. The range is mandatory rather than
defaulted because a defaulted one is how an integration silently starts
re-reading a decade of records every night.
Ordering
Newest first: descending by occurredAt, then descending by id to break a
tie. Two deliveries recorded in the same millisecond therefore have a stable,
repeatable order, which is what makes the cursor sound.
Walking the pages
nextCursor is null on the last page and a string otherwise. Pass it back
untouched — it is a base64url-encoded position, not a document to parse, and its
internals will change without notice.
async function* deliveries({ apiKey, restaurantId, from, to }) {
const base = `https://api.backresto.com/v1/restaurants/${restaurantId}/deliveries`;
let cursor = null;
do {
const url = new URL(base);
url.searchParams.set('from', from);
url.searchParams.set('to', to);
url.searchParams.set('limit', '100');
if (cursor !== null) {
url.searchParams.set('cursor', cursor);
}
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!response.ok) {
throw new Error(`BackResto ${response.status}`);
}
const page = await response.json();
yield* page.data;
cursor = page.nextCursor;
} while (cursor !== null);
}
Keep from and to identical across every page of a walk. The cursor encodes a
position inside the range you asked for; changing the range mid-walk is not a
supported request and will not give you what you expect.
Polling incrementally
Deliveries are immutable once recorded, so an incremental poll is
straightforward: keep the occurredAt of the newest record you have stored and
use it as the next from.
Two details worth building in:
- Overlap deliberately. Start the next window a little before the last
record you saw — an hour is plenty — and deduplicate on
id. A device that was offline during service uploads when it reconnects, so a delivery can become visible after a poll that already covered its timestamp. - Never widen past 366 days. If your job has not run for a long time, walk the gap in year-long windows rather than asking for all of it.
An empty page is not proof that nothing happened — see the note on completeness.