Pagination, filtering & sync

Pages

GET /api/v1/orders and GET /api/v1/cards/{id}/events answer one page at a time:

{ "data": [ … ], "next_cursor": "eyJ0cyI6…" }
  • For the next page, send cursor=<next_cursor> with exactly the same filters. A cursor only works with the filters that produced it.
  • next_cursor is null on the last page.
  • limit is 1–100 (default 25).
  • Cursors are opaque: don't build or change them.

Filtering orders

  • status: a comma list, e.g. status=new,preparing,ready. Without it you get every status except awaiting_payment and expired; ask for those by name.
  • branch_id: one branch (a branch-bound key sees only its own anyway).
  • type: dine_in, pickup or car.
  • created_from / created_to: ISO 8601 times with a zone, e.g. 2026-11-08T00:00:00+03:00. created_from is included and created_to is excluded, so one day is created_from=2026-11-08T00:00:00+03:00&created_to=2026-11-09T00:00:00+03:00.

Without updated_since, orders come newest first.

Syncing orders

To keep your own copy of orders current (for accounting, say):

  • Store the newest updated_at you have processed: your checkpoint.
  • Call GET /api/v1/orders?updated_since=<checkpoint> and follow next_cursor until it is null. With updated_since, the oldest change comes first and the value is included.
  • Start each run a few seconds before your checkpoint: an order's updated_at is the time its change started, so a change that was still being saved can land just behind one you've already seen.
  • An order comes back again every time it changes, and the overlap repeats a few: upsert by id.

An order that is still awaiting_payment doesn't move its updated_at when a payment attempt fails or a new attempt starts. If you sync waiting orders, read them with GET /orders/{id} until they leave awaiting_payment.

const base = "https://serveu-psi.vercel.app/api/v1";
const checkpoint = await loadCheckpoint(); // e.g. "2026-11-08T00:00:00.000Z"
// Overlap the last run by a few seconds; upserts make the repeats harmless.
const since = new Date(Date.parse(checkpoint) - 5000).toISOString();
let cursor = null;
let newest = checkpoint;
do {
  const url = new URL(`${base}/orders`);
  url.searchParams.set("updated_since", since); // the same filter on every page
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SERVEU_API_KEY}` } });
  if (!res.ok) throw new Error((await res.json()).error.code);
  const page = await res.json();
  for (const order of page.data) {
    await upsertOrder(order); // by order.id
    if (order.updated_at > newest) newest = order.updated_at;
  }
  cursor = page.next_cursor;
} while (cursor);
await saveCheckpoint(newest);
Times in answers are ISO 8601 UTC with milliseconds (2026-11-08T09:41:12.482Z), so they compare correctly as strings.

Next: Idempotency →