ข้ามไปยังเนื้อหา

เริ่มใช้งานใน 5 นาที

หน้านี้พาคุณจากเทอร์มินัลเปล่า ไปถึงข้อมูลยอดขายจริงหนึ่งวัน ภายในราวห้านาที เริ่มจากดูว่ากุญแจของคุณเห็นสาขาไหนบ้าง ดึงบิลของสาขานั้น แล้วแปลงเป็นรอบดึงข้อมูลประจำ

ต้องมีสามอย่าง

  • กุญแจ API จาก ScanFood ดูขั้นที่ 1 ด้านล่าง
  • ที่สำหรับรันคำขอ เซิร์ฟเวอร์ งานตั้งเวลา หรือ backend service ของคุณ ไม่ใช่เบราว์เซอร์ และไม่ใช่แอปมือถือ เพราะคำขอข้ามโดเมนถูกปฏิเสธ และกุญแจที่ไปอยู่บนเครื่องผู้ใช้คือกุญแจที่หลุดแล้ว
  • ที่เก็บกุญแจให้เป็นความลับ ตัวแปรสภาพแวดล้อม ระบบเก็บความลับ อะไรก็ได้ที่ไม่ใช่โค้ดใน repo
Terminal window
export SCANFOOD_API_KEY="sf_live_XXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

กุญแจ API ออกให้โดยทีมงาน ScanFood ติดต่อเราพร้อมข้อมูลสามข้อ

  • กุญแจใบนี้ใช้กับร้านไหน ร้านเดียว หรือทั้งแฟรนไชส์
  • คุณกำลังทำอะไร เพื่อให้เราตั้งโควตาต่อวันให้เหมาะสม
  • ต้องใช้เบอร์โทรหรืออีเมลของสมาชิกไหม สองช่องนี้ปิดเป็นค่าตั้งต้น และจะเปิดให้หลังคุยเรื่องการคุ้มครองข้อมูลส่วนบุคคลแล้วเท่านั้น

คุณจะได้สตริงหนึ่งชุด หน้าตาแบบนี้

sf_live_XXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

เริ่มที่ GET /v1/shops เส้นนี้ไม่รับพารามิเตอร์เลยสักตัว เพราะตัวกุญแจเป็นคนกำหนดว่า สาขาไหนจะถูกส่งกลับมา คำขอนี้จึงบอกคุณด้วยว่ากุญแจของคุณเห็นอะไรได้บ้าง

Terminal window
curl -s https://api.scanfood.co/ext/v1/shops \
-H "Authorization: Bearer $SCANFOOD_API_KEY"
{
"ok": true,
"data": [
{
"shopId": "aK3mP9xQ2bR7cT4dV6wY",
"shopName": "ร้านตัวอย่าง สาขาสีลม",
"franchiseId": null,
"providerId": "bL4nQ0yR3cS8dU5eW7xZ",
"timezone": "Asia/Bangkok",
"categories": [
{ "id": "9369601f-fef5-4a61-bfb8-26f25eab3dad", "name": "อาหาร", "level": 1, "parentId": null, "hidden": false }
]
}
],
"serverTime": "2026-09-09T13:45:12.310Z"
}

เก็บ shopId ไว้ เพราะทุกคำขอฝั่งบิลต้องใช้ และเก็บ providerId ไว้ด้วย เพราะนั่นคือรหัสที่เส้นฝั่งสมาชิกใช้

ต่อไปคือดึงบิลตามช่วงวัน from และ to คือวันตามปฏิทินในเขตเวลาของร้านเอง เขียนเป็น YYYYMMDD ไม่มีขีด และนับรวมทั้งวันแรกและวันสุดท้าย

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"

ตัวอย่างผลลัพธ์แบบย่อ ช่องข้อมูลครบทั้งหมดอยู่ที่ เอกสารรายการขาย

{
"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": "โต๊ะ 3" },
"memberId": "m1_7W3mBnrKtP3ieg3Zhve_Cj4OvSWX7FYAYCfZ3GDHT54n7KuhPMylFlhZE71FJb-gnMnqFgL7RL7dsPATXUZNTVN_698LdRA",
"items": [
{
"id": "iS1uX7fY0jZ5kB2lD4eG",
"name": "สปาเก็ตตี้ขี้เมา",
"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": "เงินสด", "amount": 400 }],
"status": "completed"
}
],
"nextCursor": null,
"serverTime": "2026-09-01T06:30:00.000Z"
}

มีสี่จุดที่ควรสังเกต เพราะเกือบทุกคนสะดุดอย่างน้อยหนึ่งครั้ง

ในผลลัพธ์ หมายความว่า
nextCursor เป็น null แปลว่าได้ครบแล้ว ถ้าไม่ใช่ null ให้ส่งค่านี้กลับไปเป็น cursor ของหน้าถัดไป ดู การแบ่งหน้าและการซิงก์
billDate กับ businessDate from/to กรองที่ billDate ซึ่งเป็นวันตามปฏิทินธรรมดา ส่วน businessDate คือวันที่ร้านนับยอดนั้นเข้ารายได้ สองค่านี้ต่างกันในร้านที่ปิดหลังเที่ยงคืน อ่านต่อ
amounts.net ยอดที่ลูกค้าจ่ายจริง รวมทิปแล้ว ถ้าจะเรียกว่ายอดขายของร้าน ต้องหัก amounts.tip ออกก่อน
status บิลที่ถูกยกเลิกยังถูกส่งมาด้วย เป็น "void" ถ้ารายงานของคุณไม่ควรนับ ต้องคัดออกเอง

อย่าดึงวันเดิมซ้ำไปตลอด เมื่อมีสำเนาชุดแรกครบแล้ว ให้เปลี่ยนไปถามเฉพาะสิ่งที่เปลี่ยน

  • บิล เรียกด้วย updatedSince (เวลาแบบ ISO-8601) แทน from/to โดยใช้ค่า serverTime ของรอบที่สำเร็จครั้งก่อน
  • สมาชิก ยังใช้ updatedSince ไม่ได้ ต้องอ่านทั้งแบรนด์ใหม่ด้วย cursor วันละครั้งก็เพียงพอ
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"
สิ่งที่เห็น มักเป็นเพราะ วิธีแก้
401 invalid_key ไม่มีส่วนหัว Authorization หรือรูปแบบผิด หรือไม่ใช่กุญแจของระบบจริง กุญแจที่ไม่มีอยู่กับรหัสลับผิด ตอบเหมือนกันโดยเจตนา ส่ง Authorization: Bearer sf_live_… กุญแจอยู่ในส่วนหัวเท่านั้น ไม่ใส่ใน query string และไม่ใส่ใน body
401 key_revoked กุญแจถูกยกเลิกแล้ว ขอกุญแจใบใหม่ การยกเลิกย้อนกลับไม่ได้
403 shop_not_in_scope shopId มีอยู่จริง แต่อยู่นอกขอบเขตของกุญแจใบนี้ เรียก GET /v1/shops แล้วใช้รหัสที่ได้จากที่นั่น
403 provider_not_in_scope แบบเดียวกัน แต่เป็น providerId ของเส้นฝั่งสมาชิก เอา providerId มาจาก GET /v1/shops
400 bad_query — choose exactly one mode ส่งทั้ง from/to และ updatedSince หรือไม่ส่งเลยสักโหมด เลือกโหมดเดียวต่อหนึ่งคำขอ
400 bad_query — from/to must be YYYYMMDD ส่ง 2026-09-01 มา ฝั่งคำขอใช้ YYYYMMDD ไม่มีขีด ที่มีขีดคือฝั่งผลลัพธ์
400 bad_query — range too wide ช่วง from ถึง to เกิน 31 วัน ซอยการดึงย้อนหลังเป็นช่วงละไม่เกิน 31 วัน
429 rate_limited ยิงถี่เกินไปในหนึ่งนาที รอตามส่วนหัว Retry-After แล้วลองใหม่
429 quota_exceeded โควตาของวันหมดแล้ว รีเซ็ตเที่ยงคืนเวลาไทย ลดความถี่ หรือขอเพิ่มโควตา
503 auth_unavailable ระบบตรวจกุญแจฝั่งเราไม่พร้อมชั่วคราว กุญแจของคุณไม่ได้มีปัญหา ลองใหม่แบบถอยเวลาเพิ่มขึ้น อย่าออกกุญแจใหม่
500 internal ความล้มเหลวชั่วคราวฝั่งเรา ลองใหม่แบบถอยเวลาเพิ่มขึ้น ถ้าเป็นต่อเนื่องให้แจ้งเรา
ไม่ได้อะไรเลย เมื่อเรียกจากหน้าเว็บ คำขอจากเบราว์เซอร์ถูกปฏิเสธโดยเจตนา เรียกจากเซิร์ฟเวอร์ของคุณ

รหัสข้อผิดพลาดทุกตัวและวิธีรับมือ อยู่ที่หน้า ข้อผิดพลาด