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_cursorisnullon the last page.limitis 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 exceptawaiting_paymentandexpired; ask for those by name.branch_id: one branch (a branch-bound key sees only its own anyway).type:dine_in,pickuporcar.created_from/created_to: ISO 8601 times with a zone, e.g.2026-11-08T00:00:00+03:00.created_fromis included andcreated_tois excluded, so one day iscreated_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_atyou have processed: your checkpoint. - Call
GET /api/v1/orders?updated_since=<checkpoint>and follownext_cursoruntil it is null. Withupdated_since, the oldest change comes first and the value is included. - Start each run a few seconds before your checkpoint: an order's
updated_atis 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.