Skip to content

Quickstart

This page takes you from an empty terminal to a day of real sales data in about five minutes: list the shops your key can see, pull their transactions, then turn that into a repeating sync.

You need three things:

  • An API key from ScanFood — see step 1.
  • Somewhere to run the calls from. A server, a scheduled job or a backend service. Not a browser, and not a mobile app: cross-origin requests are refused, and a key that reaches a user’s device has leaked.
  • A way to keep the key secret — an environment variable, a secrets manager, anything but source control.
Terminal window
export SCANFOOD_API_KEY="sf_live_XXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

API keys are issued by the ScanFood team. Contact us with:

  • Which shops the key is for — a single shop, or a whole franchise.
  • What you are building, so we can set a sensible daily quota.
  • Whether you need member phone numbers or e-mail addresses. These are off by default and are only enabled after a conversation about data protection.

You will receive one string that looks like this:

sf_live_XXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Start with GET /v1/shops. It takes no parameters at all: the key itself decides which branches come back, so this call also tells you exactly what your key can see.

Terminal window
curl -s https://api.scanfood.co/ext/v1/shops \
-H "Authorization: Bearer $SCANFOOD_API_KEY"
{
"ok": true,
"data": [
{
"shopId": "aK3mP9xQ2bR7cT4dV6wY",
"shopName": "Example Restaurant — Silom",
"franchiseId": null,
"providerId": "bL4nQ0yR3cS8dU5eW7xZ",
"timezone": "Asia/Bangkok",
"categories": [
{ "id": "9369601f-fef5-4a61-bfb8-26f25eab3dad", "name": "Food", "level": 1, "parentId": null, "hidden": false }
]
}
],
"serverTime": "2026-09-09T13:45:12.310Z"
}

Keep the shopId — every transaction call needs one. Keep the providerId too: that is the identifier the member endpoints use.

Now pull the bills for a date range. from and to are calendar days in the shop’s own time zone, written as YYYYMMDD with no dashes, and both ends are included.

Terminal window
curl -s -G https://api.scanfood.co/ext/v1/transactions \
-H "Authorization: Bearer $SCANFOOD_API_KEY" \
--data-urlencode "shopId=aK3mP9xQ2bR7cT4dV6wY" \
--data-urlencode "from=20260901" \
--data-urlencode "to=20260901" \
--data-urlencode "limit=200"

A shortened response — the full field list is in the transactions reference:

{
"ok": true,
"data": [
{
"id": "dN6pS2aT5eU0fW7gY9zB",
"receiptNumber": "RC-001",
"taxNumber": null,
"timestamp": "2026-09-01T13:30:00.000+07:00",
"businessDate": "2026-09-01",
"billDate": "2026-09-01",
"updatedAt": "2026-09-01T13:30:00.000+07:00",
"shopId": "aK3mP9xQ2bR7cT4dV6wY",
"channel": { "code": "dine_in", "name": "Table 3" },
"memberId": "m1_7W3mBnrKtP3ieg3Zhve_Cj4OvSWX7FYAYCfZ3GDHT54n7KuhPMylFlhZE71FJb-gnMnqFgL7RL7dsPATXUZNTVN_698LdRA",
"items": [
{
"id": "iS1uX7fY0jZ5kB2lD4eG",
"name": "Drunken noodles",
"qty": 3,
"unitPrice": 137.55,
"lineTotal": 412.65,
"cancelled": false
}
],
"amounts": {
"subtotal": 412.65,
"discountTotal": 20,
"vat": 0,
"vatType": "Include",
"tip": 0,
"net": 392.65
},
"payments": [{ "name": "Cash", "amount": 400 }],
"status": "completed"
}
],
"nextCursor": null,
"serverTime": "2026-09-01T06:30:00.000Z"
}

Four things to notice, because they catch almost everyone once:

In the response What it means
nextCursor null means you have the whole result. Anything else is the cursor for the next page — see Pagination & sync.
billDate vs businessDate from/to filter on billDate, the plain calendar day. businessDate is the day the shop counts the money in. They differ for shops that close after midnight — details.
amounts.net What the customer actually paid, tip included. Subtract amounts.tip before you call it shop revenue.
status Voided bills are still returned, as "void". Exclude them yourself if your report should not count them.

Do not re-download the same days forever. Once you hold a first full copy, switch to asking only for what changed:

  • Transactions — call with updatedSince (an ISO-8601 timestamp) instead of from/to, and pass the serverTime of your previous successful run.
  • Members — updatedSince is not available yet, so re-read the brand in full with the cursor. Once a day is plenty.
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-01T06:30:00.000Z" \
--data-urlencode "limit=200"
What you see What it usually is Fix
401 invalid_key Missing or malformed Authorization header, or a key that is not a production key. The same answer is given for an unknown key and a wrong secret, on purpose. Send Authorization: Bearer sf_live_…. The key goes in the header only — never in the query string or the body.
401 key_revoked The key was revoked. Ask for a new one; revocation cannot be undone.
403 shop_not_in_scope The shopId is real but outside this key’s reach. Call GET /v1/shops and use one of the ids it returns.
403 provider_not_in_scope Same, for providerId on the member endpoints. Take providerId from GET /v1/shops.
400 bad_query — choose exactly one mode You sent both from/to and updatedSince, or neither. Pick one mode per request.
400 bad_query — from/to must be YYYYMMDD You sent 2026-09-01. Requests use YYYYMMDD with no dashes; only responses use dashes.
400 bad_query — range too wide More than 31 days between from and to. Split the backfill into windows of 31 days or fewer.
429 rate_limited Too many requests in one minute. Wait for the Retry-After header, then retry.
429 quota_exceeded The key’s daily quota is used up; it resets at midnight Thailand time. Poll less often, or ask for a higher quota.
503 auth_unavailable Our key check is temporarily unavailable. Your key is fine. Retry with backoff; do not re-issue the key.
500 internal A transient failure on our side. Retry with backoff; report it if it persists.
Nothing at all, from a web page Browser calls are refused on purpose. Call from your server.

Every code we return, and what to do about it, is on the Errors page.