ร้านค้า
ร้าน คือสาขาหนึ่งสาขาที่บันทึกการขายไว้ในระบบ ScanFood · บิลทุกใบและสมาชิกทุกคน
ใน API นี้ผูกอยู่กับร้านเสมอ ⇒ รายชื่อร้านจึงเป็นจุดเริ่มต้นของทุกการเชื่อมต่อ:
มันบอกว่ากุญแจของคุณอ่านสาขาไหนได้บ้าง และให้รหัส 2 ตัวที่เส้นอื่นต้องใช้ คือ
shopId กับ providerId · และยังมีรายชื่อหมวดของแต่ละสาขา ซึ่งเป็นตัวแปลงรหัสหมวดบนบิลให้เป็นชื่อ
เส้นที่ใช้มีเส้นเดียว: GET /v1/shops
ร้านคืออะไร
หัวข้อที่มีชื่อว่า “ร้านคืออะไร”ข้อมูลร้านในเส้นนี้ตั้งใจให้บางที่สุด — มันคือ สารบัญ ไม่ใช่โปรไฟล์ร้าน: มีแค่พอให้ระบุสาขา · รู้ว่าอยู่ใต้แฟรนไชส์ไหน · ชี้ไปที่ฐานสมาชิกของสาขานั้น บอกว่าวันที่ของสาขานี้นับตามนาฬิกาเรือนไหน และบอกชื่อหมวดที่บิลของสาขานี้อ้างถึง
| ถ้าคุณต้องการ… | ใช้ |
|---|---|
| รู้ว่ากุญแจใบนี้อ่านสาขาไหนได้บ้าง | shopId ของทุกแถว |
| ดึงยอดขายของสาขา | shopId → GET /v1/transactions |
| ดึงฐานสมาชิกที่สาขานั้นใช้ | providerId → GET /v1/members |
| จัดกลุ่มสาขาตามกิจการ | franchiseId |
ตีความ from / to และ billDate |
timezone |
แปลง items[].category บนบิลเป็นชื่อหมวด |
categories |
ข้อมูลอื่นที่สาขาถืออยู่ — บัญชีพนักงาน · กุญแจระบบส่งข้อความ · การตั้งค่าเกตเวย์รับชำระ ·
ที่อยู่ · เบอร์ติดต่อ — ไม่ออกจากระบบ ScanFood เลย เพราะเซิร์ฟเวอร์อ่านขึ้นมาเฉพาะช่องเหล่านี้
จากฐานข้อมูล คือชื่อสาขา (2 รูปที่เอกสารร้านใช้เรียกได้) · franchiseId · providerId ·
timezone · รายชื่อหมวด ⇒ ที่เหลือหลุดออกไปไม่ได้แม้โดยอุบัติเหตุ · และแม้ในรายชื่อหมวด
ก็ส่งต่อแค่ 5 ช่องตามหัวข้อ หมวดสินค้า · ส่วน shopId ไม่ได้เป็นช่องที่อ่านขึ้นมา
เพราะมันคือรหัสของตัวเอกสารเอง ไม่ใช่ช่องที่เก็บอยู่ข้างใน
ขอบเขตของร้าน
หัวข้อที่มีชื่อว่า “ขอบเขตของร้าน”กุญแจของคุณเป็นตัวกำหนดว่าเห็นร้านไหน · คำขอขยายขอบเขตเองไม่ได้ —
เส้นนี้ไม่มีพารามิเตอร์ shopId ให้ส่ง และไม่มีวิธีขอสาขาที่กุญแจไม่ได้รับสิทธิ์
| ระดับกุญแจ | ผูกกับ | ร้านที่คืนกลับมา | ฐานสมาชิกที่เข้าถึงได้ |
|---|---|---|---|
shop |
สาขาเดียว | สาขานั้นสาขาเดียว | providerId ของสาขานั้น |
franchise |
แฟรนไชส์ | ทุกสาขาของแฟรนไชส์นั้น | ทุก providerId ที่ไม่ซ้ำใต้แฟรนไชส์ |
ถ้าสาขาถูกให้สิทธิ์กับกุญแจไว้แต่ข้อมูลสาขานั้นถูกลบไปแล้ว แถวนั้นจะถูกข้ามเฉย ๆ คุณจะได้รายการที่สั้นลง ไม่ใช่ error
รูปของ response:
{ "ok": true, "data": [ { "…": "หนึ่งก้อนต่อหนึ่งร้าน" } ], "serverTime": "2026-09-09T13:45:12.310Z"}serverTime คือนาฬิกาของเซิร์ฟเวอร์ในรูป UTC (ลงท้าย Z) และมีอยู่ในทุก response
ที่สำเร็จ · เส้นนี้ ไม่มี nextCursor — รายชื่อร้านไม่แบ่งหน้า คุณได้ทุกสาขา
ในขอบเขตกุญแจครบในคำขอเดียวเสมอ
แต่ละก้อนใน data:
| ฟิลด์ | ชนิด | เป็น null ได้ไหม | ความหมาย | ตัวอย่าง |
|---|---|---|---|---|
shopId |
string | ไม่ | รหัสสาขา · ค่านี้คือค่าที่ส่งเป็น shopId ให้เส้นรายการขาย |
"sh7Kq2mVbN4tRxZ0Lp8W" |
shopName |
string | ได้ | ชื่อสาขาตามที่เจ้าของร้านพิมพ์ไว้ | "ร้านตัวอย่าง สาขาสีลม" |
franchiseId |
string | ได้ | แฟรนไชส์ที่สาขานี้สังกัด · ร้านเดี่ยว = null |
"fr5Ns8CtJ4vHqZ2WbY7K" |
providerId |
string | ได้ | ฐานสมาชิกที่สาขานี้ใช้ · ไม่ได้ทำระบบสมาชิก = null |
"pv3Yh9DkQ2sLmT6RwX1B" |
timezone |
string | ได้ | โซนเวลาแบบ IANA ของสาขา · วันแบบปฏิทิน (from · to · billDate) คือวันบนนาฬิกาเรือนนี้ |
"Asia/Bangkok" |
categories |
array | ไม่ (เป็น [] ได้) |
หมวดสินค้าของสาขาแบบรายการแบน — ดู หมวดสินค้า · สาขาที่ไม่มีหมวด = [] |
[{ "id": "9369…", "name": "อาหาร", "level": 1, "parentId": null, "hidden": false }] |
ช่องที่ จงใจไม่มี (จะได้ไม่ต้องไปตามหา): ที่อยู่ · เบอร์โทร · ข้อมูลจดทะเบียนภาษี · เวลาเปิด-ปิด · รายชื่อพนักงาน · ทะเบียนเครื่อง · กุญแจระบบชำระเงินหรือระบบส่งข้อความ และทุกอย่างที่เกี่ยวกับต้นทุนหรือสูตรวัตถุดิบ — ทั้งหมดนี้ไม่มีให้ผ่าน API นี้ ไม่ว่ากุญแจระดับไหน
หมวดสินค้า
หัวข้อที่มีชื่อว่า “หมวดสินค้า”บรรทัดสินค้าบนบิลมี items[].category คือรหัสหมวดที่สินค้าอยู่ ณ เวลาที่ขาย เรียงจากหมวดบนสุดลงไป
(ดู รายการขาย) · categories คือที่ที่รหัสเหล่านั้นได้ชื่อ ·
เป็นรายการแบน เรียงตาม level (หมวดบนสุดก่อน) แล้วตามลำดับที่ร้านจัดหมวดไว้
| ฟิลด์ | ชนิด | เป็น null ได้ไหม | ความหมาย | ตัวอย่าง |
|---|---|---|---|---|
id |
string | ไม่ | รหัสหมวด — ค่าเดียวกับที่อยู่ใน items[].category |
"eff254c9-3d71-4d6f-9e43-1806ece6a427" |
name |
string | ได้ | ชื่อหมวดตามที่ร้านตั้ง (ภาษาของร้านเอง) · ร้านเว้นว่าง = null |
"ก๋วยเตี๋ยว" |
level |
integer | ได้ | ความลึก: 1 = หมวดบนสุด · 2 = อยู่ใต้หมวดบนสุดโดยตรง ไล่ลงไปเรื่อย ๆ |
2 |
parentId |
string | ได้ | รหัสหมวดที่อยู่เหนือขึ้นไปหนึ่งชั้น · หมวดบนสุด = null |
"9369601f-fef5-4a61-bfb8-26f25eab3dad" |
hidden |
boolean | ไม่ | true เมื่อร้านซ่อนหมวดนี้จากทุกช่องทางขาย |
false |
สิ่งที่ต้องรู้ตอนใช้:
- ส่งทุกหมวด รวมหมวดที่ซ่อน — ร้านซ่อนหมวดวันนี้ได้ แต่บิลเก่ายังอ้างรหัสนั้นอยู่ ⇒ หมวดที่ซ่อน
ยังอยู่ในรายการพร้อม
hidden: true· ถ้าต้องการเฉพาะหมวดที่ขายอยู่ตอนนี้ ให้กรองhiddenเอง - หมวดที่ถูกลบแล้วคือหายไปแล้ว — รหัสบนบิลที่ไม่มีใน
categoriesคือหมวดที่ร้านลบไปแล้ว และกู้ชื่อคืนไม่ได้ ·parentIdที่ชี้ไปรหัสที่ไม่อยู่ในรายการก็เช่นกัน ⇒ รายงานเป็น “ไม่ระบุหมวด” (หรือเก็บรหัสดิบไว้) อย่าให้ระบบล้ม - ชื่อเป็นของปัจจุบัน รหัสเป็นของ ณ วันขาย —
categoriesคือรายการของวันนี้ ⇒ หมวดที่เปลี่ยนชื่อ จะโชว์ชื่อใหม่แม้บนบิลเก่า · ซึ่งมักเป็นสิ่งที่ต้องการสำหรับรายงาน · ถ้าต้องการชื่อตามวันที่ขาย ให้เก็บชื่อไว้คู่กับบิลเอง - ชั้นโปรโมชันไม่ใช่หมวด — ชั้น “ขายดี” “โปรโมชัน” “แนะนำ” บนเมนูของร้านไม่เคยอยู่ใน
items[].categoryและไม่อยู่ในรายการนี้ - ส่งเฉพาะชื่อที่ร้านพิมพ์ไว้ · ไม่ส่งชื่อแปลภาษาอื่นและรูปของหมวด
การแปลงบรรทัดบิลเป็นชื่อ = เอาแต่ละรหัสไปหาในรายการของสาขานั้น · รหัสตัวท้ายใน items[].category
คือหมวดที่เจาะจงที่สุด · ทั้ง array คือเส้นทางจากหมวดบนสุดลงมา
// shops = data จาก 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) { // เช่น ["อาหาร", "ก๋วยเตี๋ยว"] — หมวดที่ถูกลบไปแล้วได้ null return (item.category || []).map((id) => categoryName.get(`${bill.shopId}:${id}`) ?? null);}รหัสหมวดไม่ซ้ำกันภายในร้าน แต่ให้หาแยกตาม shopId แบบข้างบน — สาขาในแฟรนไชส์ถือรายชื่อหมวดของตัวเอง
การใช้งานทั่วไป
หัวข้อที่มีชื่อว่า “การใช้งานทั่วไป”ดึงสารบัญมาก่อน แล้วค่อยกระจายงานต่อ
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();
// 2 ลิสต์ที่ระบบเชื่อมต่อมักต้องใช้const shopIds = shops.map((s) => s.shopId);const providerIds = [...new Set(shops.map((s) => s.providerId).filter(Boolean))];
console.log(shopIds.length, 'สาขา ·', providerIds.length, 'ฐานสมาชิก');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), "สาขา ·", len(provider_ids), "ฐานสมาชิก")ตัวอย่าง response ของกุญแจแฟรนไชส์ที่มี 2 สาขา:
{ "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"}2 สาขานี้ชี้ไปที่ providerId เดียวกัน ⇒ ซิงก์ฐานสมาชิกนั้น ครั้งเดียว ไม่ใช่ 2 รอบ
อ่านต่อ
หัวข้อที่มีชื่อว่า “อ่านต่อ”- รายการขาย — ดึงบิลของแต่ละ
shopId - สมาชิก — ดึงฐานสมาชิกของแต่ละ
providerId - โซนเวลาและวันธุรกิจ — ทำไม
timezoneเป็นตัวตัดสินว่า “เมื่อวาน” แปลว่าวันไหน - การยืนยันตัวตน — ขอบเขตของกุญแจถูกกำหนดยังไง