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:

ListRangeOrderCursor is bound to
Deliveriesfrom and to, mandatory, at most 366 daysNewest firstThe range you asked for
Collection recordsnone — you walk the whole collectionOldest change firstRestaurant, 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…
ParameterRequiredRules
fromyesEpoch milliseconds, as a string of digits. Inclusive.
toyesEpoch milliseconds, as a string of digits. Inclusive.
limitno1 to 100. Defaults to 50.
cursornoThe 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.

Last updated 2026-09-19.