Skip to content

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.

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.

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.

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.

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 on hidden yourself if you only want what is on sale now.
  • A deleted category is gone. An id on a bill that is not in categories belongs to a category the shop has since deleted; its name cannot be recovered. The same goes for a parentId that 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. categories is 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/shops
const 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.

Fetch the directory, then fan out over it.

Terminal window
curl -s https://api.scanfood.co/ext/v1/shops \
-H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718"

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.