Shops
A shop is one physical branch that records sales in ScanFood. Every bill and
every membership record in this API hangs off a shop, so the shop list is where
an integration starts: it tells you which branches your key can read, and it
hands you the two identifiers — shopId and providerId — that the other
endpoints require. It also carries each branch’s category list, which is how you
turn the category ids on a bill into names.
There is exactly one endpoint: GET /v1/shops.
What a shop is
Section titled “What a shop is”A shop record here is deliberately thin. It is a directory entry, not a profile: enough to identify a branch, place it inside a franchise, point at its membership database, tell you which wall clock its calendar dates are on, and name the categories its bills refer to.
| You want to… | Use |
|---|---|
| Know which branches this key can read | shopId of every row |
| Pull sales for a branch | shopId → GET /v1/transactions |
| Pull the membership base a branch sells to | providerId → GET /v1/members |
| Group branches by business | franchiseId |
Interpret from / to and billDate |
timezone |
Turn items[].category on a bill into category names |
categories |
Everything else a branch holds — staff accounts, messaging credentials, payment
gateway settings, addresses, contact numbers — never leaves ScanFood. The
server reads only these fields out of storage — the branch name (either of the two
spellings a shop record may use), franchiseId, providerId, timezone and the
category list — so the rest cannot leak even by accident. Even inside the category
list, only the five fields described under Categories are passed on. shopId is not one of them: it is the
record’s own identifier, not a field stored inside it.
Shop scope
Section titled “Shop scope”Your key decides which shops you see. A request cannot widen it. There is no
shopId parameter on this endpoint, and no way to ask for a branch that was not
granted to the key.
| Key scope | Bound to | Shops returned | Membership bases reachable |
|---|---|---|---|
shop |
one branch | that one shop | the providerId of that shop |
franchise |
one franchise | every branch of that franchise | every distinct providerId under it |
If a branch was granted to the key but its record has since been removed, the row is simply skipped. You get a shorter list, not an error.
Fields
Section titled “Fields”Response envelope:
{ "ok": true, "data": [ { "…": "one object per shop" } ], "serverTime": "2026-09-09T13:45:12.310Z"}serverTime is the server’s clock in UTC (Z suffix) and appears on every
successful response. There is no nextCursor on this endpoint — the shop
list is never paginated, you always receive every branch in scope in one
response.
Each object in data:
| Field | Type | Nullable | Meaning | Example |
|---|---|---|---|---|
shopId |
string | no | The branch identifier. This is the value you pass as shopId to the transactions endpoint. |
"sh7Kq2mVbN4tRxZ0Lp8W" |
shopName |
string | yes | Display name of the branch, as the owner typed it. | "ร้านตัวอย่าง สาขาสีลม" |
franchiseId |
string | yes | The franchise this branch belongs to. null for an independent shop. |
"fr5Ns8CtJ4vHqZ2WbY7K" |
providerId |
string | yes | The membership base this branch sells to. null if the branch runs no membership programme. |
"pv3Yh9DkQ2sLmT6RwX1B" |
timezone |
string | yes | IANA time zone of the branch. Calendar dates (from, to, billDate) are days on this clock. |
"Asia/Bangkok" |
categories |
array | no (may be []) |
The branch’s product categories as a flat list — see Categories. [] when the branch has none. |
[{ "id": "9369…", "name": "อาหาร", "level": 1, "parentId": null, "hidden": false }] |
Fields that are deliberately absent, so you do not go looking for them: addresses and phone numbers, tax registration details, opening hours, staff lists, device inventories, payment or messaging credentials, and anything about cost or recipes. None of these are available through this API at any key level.
Categories
Section titled “Categories”Each line on a bill carries items[].category: the category ids the product
sat in at the time of sale, top-level first (see
Transactions). categories is where those ids get their
names. It is a flat list, ordered by level (top level first) and then in the
order the shop arranged its categories.
| Field | Type | Nullable | Meaning | Example |
|---|---|---|---|---|
id |
string | no | Category id — the same value that appears in items[].category. |
"eff254c9-3d71-4d6f-9e43-1806ece6a427" |
name |
string | yes | Category name as the shop set it, in the shop’s own language. null when left empty. |
"ก๋วยเตี๋ยว" |
level |
integer | yes | Depth: 1 is a top-level category, 2 sits directly under one, and so on. |
2 |
parentId |
string | yes | Id of the category directly above. null for a top-level category. |
"9369601f-fef5-4a61-bfb8-26f25eab3dad" |
hidden |
boolean | no | true when the shop has hidden the category from every sales channel. |
false |
Things to know when you use it:
- Every category is listed, hidden ones included. A shop can hide a
category today that older bills still refer to, so hidden categories stay in
the list with
hidden: true. Filter onhiddenyourself if you only want what is on sale now. - A deleted category is gone. An id on a bill that is not in
categoriesbelongs to a category the shop has since deleted; its name cannot be recovered. The same goes for aparentIdthat points at an id not in the list. Report these as “uncategorised” (or keep the raw id) rather than failing. - Names are current, ids are historical.
categoriesis today’s list, so a renamed category shows its new name even on old bills. That is usually what you want for reporting; store the name alongside the bill yourself if you need the name as it was on the day. - Promotional groupings are not categories. “Best sellers”, “promotions” and
“recommended” shelves in the shop’s menu never appear in
items[].category, and are not listed here. - Only the name the shop typed is returned; translated names and category images are not.
To turn a bill line into names, look each id up in the branch’s list. The last
id in items[].category is the most specific category; the whole array is the
path from the top.
// shops = data from GET /v1/shopsconst categoryName = new Map();for (const shop of shops) { for (const c of shop.categories) categoryName.set(`${shop.shopId}:${c.id}`, c.name);}
function categoryPath(bill, item) { // e.g. ["อาหาร", "ก๋วยเตี๋ยว"] — null for a category that has been deleted return (item.category || []).map((id) => categoryName.get(`${bill.shopId}:${id}`) ?? null);}Category ids are unique inside a shop, but look them up per shopId as above —
franchise branches keep their own category lists.
Typical use
Section titled “Typical use”Fetch the directory, then fan out over it.
curl -s https://api.scanfood.co/ext/v1/shops \ -H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718"const BASE = 'https://api.scanfood.co/ext/v1';const KEY = process.env.SCANFOOD_API_KEY; // sf_live_…
async function listShops() { const res = await fetch(`${BASE}/shops`, { headers: { Authorization: `Bearer ${KEY}` }, }); if (!res.ok) { const err = await res.json(); throw new Error(`${res.status} ${err.error}: ${err.message}`); } const body = await res.json(); return body.data;}
const shops = await listShops();
// The two lists an integration usually needs.const shopIds = shops.map((s) => s.shopId);const providerIds = [...new Set(shops.map((s) => s.providerId).filter(Boolean))];
console.log(shopIds.length, 'shops,', providerIds.length, 'membership bases');import osimport requests
BASE = "https://api.scanfood.co/ext/v1"KEY = os.environ["SCANFOOD_API_KEY"] # sf_live_…HEADERS = {"Authorization": f"Bearer {KEY}"}
def list_shops(): res = requests.get(f"{BASE}/shops", headers=HEADERS, timeout=30) if res.status_code != 200: err = res.json() raise RuntimeError(f"{res.status_code} {err['error']}: {err['message']}") return res.json()["data"]
shops = list_shops()
shop_ids = [s["shopId"] for s in shops]provider_ids = sorted({s["providerId"] for s in shops if s["providerId"]})
print(len(shop_ids), "shops,", len(provider_ids), "membership bases")A typical response for a two-branch franchise key:
{ "ok": true, "data": [ { "shopId": "sh7Kq2mVbN4tRxZ0Lp8W", "shopName": "ร้านตัวอย่าง สาขาสีลม", "franchiseId": "fr5Ns8CtJ4vHqZ2WbY7K", "providerId": "pv3Yh9DkQ2sLmT6RwX1B", "timezone": "Asia/Bangkok", "categories": [ { "id": "9369601f-fef5-4a61-bfb8-26f25eab3dad", "name": "อาหาร", "level": 1, "parentId": null, "hidden": false }, { "id": "3c1d7e52-8a0b-4f6e-9d21-5b7a0c4e9f13", "name": "เครื่องดื่ม", "level": 1, "parentId": null, "hidden": true }, { "id": "eff254c9-3d71-4d6f-9e43-1806ece6a427", "name": "ก๋วยเตี๋ยว", "level": 2, "parentId": "9369601f-fef5-4a61-bfb8-26f25eab3dad", "hidden": false } ] }, { "shopId": "shB3xW9pL2knT6ZqR4Vd", "shopName": "ร้านตัวอย่าง สาขาอโศก", "franchiseId": "fr5Ns8CtJ4vHqZ2WbY7K", "providerId": "pv3Yh9DkQ2sLmT6RwX1B", "timezone": "Asia/Bangkok", "categories": [] } ], "serverTime": "2026-09-09T13:45:12.310Z"}Both branches point at the same providerId — sync that membership base once,
not twice.
Where to go next
Section titled “Where to go next”- Transactions — pull the bills for each
shopId. - Members — pull the membership base for each
providerId. - Time zones and business date — why
timezonedecides what “yesterday” means. - Authentication — how key scope is granted.