Skip to content

Test data

The ScanFood API reads data that already exists: bills your shops have closed and members your brands have signed up. There is no separate practice copy of it, so you build and test against the same records you will use in production.

That sounds riskier than it is. Every endpoint is a GET, nothing you send can change a bill, and the only trace a request leaves behind is one line in our usage log and one tick on your daily quota. The rest of this page is about keeping that first week cheap and boring.

There is no test environment, no fake shop and no test key that talks to a parallel database. Keys handed out for production start with sf_live_ and read live records.

What protects you instead:

Safeguard What it means for you
Every endpoint is read-only There is no request you can send that edits, voids or deletes anything in ScanFood.
The key decides what exists A key sees the shops it was issued for and nothing else. A shopId outside that list is a 403, not a peek.
Sensitive fields are off by default Member phone numbers and e-mail addresses are not returned unless the key was explicitly issued with that permission.
Requests are metered, not billed by mistake A runaway loop hits the rate limit or the daily quota and stops. It cannot spend anything.

Ask the ScanFood team for two keys: one for the integration you are building, one for people to poke at by hand. Each key carries its own label, its own per-minute limit, its own daily quota and its own usage history.

Why it is worth the extra request:

  • You can revoke the scratch key the day the work is done without touching the running integration.
  • The usage log separates the two, so “who made 4,000 calls last night” has an answer.
  • Different quotas mean an experiment cannot eat the production allowance.

Five habits, in the order they matter:

  1. Start with /v1/shops. It takes no parameters, returns only the shops your key can see, and proves the key works before you write anything harder.
  2. Keep date ranges short. A single day (from=20260901&to=20260901) is enough to see the shape of a bill. The endpoint accepts up to 31 days per request; you do not need that while you are still reading JSON by eye.
  3. Keep limit small. The default is 100 and the maximum is 200. Use limit=5 while you are exploring — the response fits on a screen and the page loop is easier to reason about.
  4. Read X-RateLimit-Remaining on every response while you develop. It is the cheapest early warning you will get.
  5. Pick a quiet shop for the first runs. Any shop in your key’s scope will do, but a branch that is not mid-service makes a stable target while you compare two runs against each other.

This sequence spends four requests and shows you a real bill end to end.

Terminal window
# 0. keep the key out of your shell history
read -rs SCANFOOD_API_KEY && export SCANFOOD_API_KEY
# 1. which shops can this key see?
curl -s -H "Authorization: Bearer $SCANFOOD_API_KEY" \
"https://api.scanfood.co/ext/v1/shops"
# 2. five bills from one day (dates are YYYYMMDD, no dashes)
curl -s -H "Authorization: Bearer $SCANFOOD_API_KEY" \
"https://api.scanfood.co/ext/v1/transactions?shopId=SHOP_ID&from=20260901&to=20260901&limit=5"
# 3. one bill in full
curl -s -H "Authorization: Bearer $SCANFOOD_API_KEY" \
"https://api.scanfood.co/ext/v1/transactions/TXN_ID"
# 4. how much quota is left today?
curl -s -D - -o /dev/null -H "Authorization: Bearer $SCANFOOD_API_KEY" \
"https://api.scanfood.co/ext/v1/shops" | grep -i x-ratelimit

If step 2 comes back with an empty data array, that day had no bills at that branch — try a date you know was busy. An empty list is a valid answer, not an error.

Two limits run at the same time, and they fail differently:

Limit Default What you see when you cross it
Requests per minute, per key 60 429 with "error": "rate_limited" and a Retry-After header telling you how many seconds to wait.
Requests per day, per key 10,000 429 with "error": "quota_exceeded". The counter resets at 00:00 Thailand time (ICT).

X-RateLimit-Limit and X-RateLimit-Remaining describe the daily quota, not the per-minute burst. They appear on responses that got past authentication and the per-minute check — so a 401, or a 429 that says rate_limited, will not carry them.

Both counters count requests received, not requests that succeeded. A loop that sends 500 malformed requests spends 500 of your day. This is the single best reason to develop with limit=5 and a one-day range.

Nothing about the API changes — the same key, the same host, the same responses. What changes is your side, and the go-live checklist is the list to walk through before you widen the date range and turn the schedule on.

Two things worth doing before that day:

  • Backfill first, then switch to incremental. Page through history with from/to in 31-day chunks, and only then start polling updatedSince for bills. Members have no incremental mode yet and are always a full sync.
  • Retire the scratch key. Ask for it to be revoked once the exploration is finished. Revocation takes effect within five minutes, after which that key answers 401.