Skip to content

Pagination & sync

List endpoints return one page at a time and hand you a cursor for the next. This page covers both halves of the job: reading a result set to the end, and keeping your own copy in step afterwards without downloading everything again.

Field Direction What it is
limit Request Rows per page. Default 100, maximum 200
cursor Request The nextCursor from the previous page. Omit it for the first page
nextCursor Response Cursor for the next page, or null when the result is complete

Rules that follow from that:

  • limit is corrected, not rejected. A value above 200 is reduced to 200, and a value that is not a positive number falls back to 100. You will not get a 400 for it, so check what you actually received.
  • A cursor is opaque. Store it as a string and send it back unchanged; never parse it or build one yourself. On the member endpoints the cursor is a m1_… token, and a raw id is rejected with 400 bad_cursor.
  • “Full page” means “ask again”. The next page is offered whenever a page came back completely full. If the last page happens to be exactly full you will get one more request that returns "data": [] with "nextCursor": null. That is normal, not a bug.
  • There is no total. No hasMore, no row count, no page or offset. The only stopping condition is nextCursor === null.
  • GET /v1/shops is not paginated at all — it returns every branch the key can read in a single response.

The same loop works for transactions and for members; only the parameters differ.

Terminal window
# First page
curl -s -G https://api.scanfood.co/ext/v1/transactions \
-H "Authorization: Bearer $SCANFOOD_API_KEY" \
--data-urlencode "shopId=aK3mP9xQ2bR7cT4dV6wY" \
--data-urlencode "from=20260801" --data-urlencode "to=20260831" \
--data-urlencode "limit=200"
# Next page: pass the nextCursor you just received
curl -s -G https://api.scanfood.co/ext/v1/transactions \
-H "Authorization: Bearer $SCANFOOD_API_KEY" \
--data-urlencode "shopId=aK3mP9xQ2bR7cT4dV6wY" \
--data-urlencode "from=20260801" --data-urlencode "to=20260831" \
--data-urlencode "limit=200" \
--data-urlencode "cursor=dN6pS2aT5eU0fW7gY9zB"

Keep every parameter identical across the pages of one result set. Changing the range or the limit halfway through gives you a cursor that belongs to a different query.

Once you hold a full copy, ask only for what changed.

Transactions accept updatedSince, an ISO-8601 timestamp. Rows come back ordered by the moment they last changed, so re-issued tax invoices, voided bills and amended fields all reappear.

Terminal window
curl -s -G https://api.scanfood.co/ext/v1/transactions \
-H "Authorization: Bearer $SCANFOOD_API_KEY" \
--data-urlencode "shopId=aK3mP9xQ2bR7cT4dV6wY" \
--data-urlencode "updatedSince=2026-09-09T04:00:00.000Z" \
--data-urlencode "limit=200"

Use the serverTime of your last successful run as the next updatedSince, and only advance it once every page of the run has been stored. Overlapping by a minute costs nothing — every row carries a stable id, so re-reading one is an update, not a duplicate.

from/to and updatedSince are two modes of the same endpoint, and you must choose exactly one per request. Sending both, or neither, is 400 bad_query.

Do this once per shop, then switch to incremental.

  1. List the shops. GET /v1/shops gives you every shopId the key can read.
  2. Walk the calendar in windows of 31 days or fewer. A wider range is rejected with 400 bad_query (range too wide). Month-sized windows are the easiest to reason about.
  3. Page each window to the end with limit=200, following nextCursor.
  4. Record the serverTime of the final successful window. That timestamp is the starting point for your first incremental run.
  5. Members separately. Page GET /v1/members per providerId — one brand may be shared by several branches, so de-duplicate the provider ids you got from step 1 before you start.

The API guarantees nothing changes when you read; it is your storage that has to tolerate the same row arriving twice.

  • Upsert on id for transactions, and on memberId for members. Both are stable and never reused.
  • Handle status: "void" as an update, not a delete. A bill you already stored can come back voided; keep the row and mark it, so your totals and the shop’s agree.
  • Store updatedAt alongside each bill. If a row arrives with an older updatedAt than the one you hold, it is a replay — ignore it.
  • Advance your watermark only after a successful run. Storing rows and the new updatedSince in the same transaction avoids a gap if the job dies halfway.
  • Expect null values, not missing fields. Fields that do not apply come back as null, so a column that is suddenly empty is data, not a schema change.