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

รายการขาย

รายการขาย คือบิลที่ปิดแล้ว 1 ใบ — สั่งอะไรบ้าง · ลดไปเท่าไหร่ · คิดภาษียังไง · ลูกค้าจ่ายด้วยอะไร และบิลนั้นถูกยกเลิกทีหลังหรือเปล่า · นี่คือทรัพยากรที่ระบบบัญชี · ระบบ BI หรืองานกระทบยอดรายวัน ใช้เวลาอยู่กับมันเกือบทั้งหมด

มี 2 เส้น:

ทั้ง 2 เส้นเป็นการอ่านอย่างเดียว — สิ่งที่คุณทำที่นี่ไม่เปลี่ยนอะไรในระบบ ScanFood เลย

บิลถูกสร้างตอนแคชเชียร์ปิดการขาย · บิลใบเดียวรวมได้หลายออเดอร์ (โต๊ะที่สั่ง 3 รอบ จ่ายครั้งเดียว) นี่คือเหตุผลที่ orderIds เป็น array

สิ่งที่ API คืนกลับมา คือชุดฟิลด์ที่ถูกคัดไว้ตายตัว — เขียนชื่อไว้ทีละช่องบนเซิร์ฟเวอร์ · บิลจริงในฐานข้อมูลถือข้อมูลมากกว่านี้มาก รวมถึงต้นทุนและสูตรวัตถุดิบ ⇒ ของที่ไม่ได้อยู่ ในรายการข้างล่างนี้ โผล่ออกมาไม่ได้ ทั้งวันนี้และหลังการแก้ไขในอนาคต

ใครใช้ข้อมูลนี้:

งาน ดึงอะไร
กระทบยอดรายวันกับรายงานของร้าน ช่วงวันปฏิทินของร้านนั้น แล้วจัดกลุ่มด้วย businessDate
ซิงก์ต่อเนื่องเข้าคลังข้อมูล updatedSince ทุก 15 นาที
วิเคราะห์เมนู/สินค้า items[] ตลอดช่วงวัน
เชื่อมยอดกับสมาชิก memberId บนบิล แล้ว join กับ สมาชิก
กำไรขาดทุนแยกช่องทาง (หน้าร้าน vs เดลิเวอรี) channel.code + amounts

รูป response ของเส้นลิสต์:

{
"ok": true,
"data": [ { "…": "หนึ่งก้อนต่อหนึ่งบิล" } ],
"nextCursor": null,
"serverTime": "2026-09-01T06:30:00.000Z"
}

เส้นบิลใบเดียวคืนก้อนเดียวกันใต้ data และไม่มี nextCursor

ฟิลด์ ชนิด เป็น null ได้ไหม ความหมาย ตัวอย่าง
id string ไม่ รหัสบิล · เสถียร ใช้เป็นคีย์หลักได้ และเป็นค่าที่ใช้เป็น cursor ด้วย "tx9Bd2LmQ7sVfR4KpN1C"
orderIds string[] ไม่ (เป็น [] ได้) ออเดอร์ที่ถูกรวมมาปิดเป็นบิลใบนี้ ["or4Tq8ZmB2vNs6WkY9Hd"]
receiptNumber string ได้ เลขใบเสร็จที่พิมพ์ให้ลูกค้า "RC-001"
taxNumber string ได้ เลขใบกำกับภาษีเต็มรูป · มีเฉพาะบิลที่ออกใบกำกับภาษี "TIV-2026-000144"
timestamp string ได้ เวลาที่ปิดบิล · ISO-8601 พร้อม offset ของโซนเวลาร้าน "2026-09-01T13:30:00.000+07:00"
businessDate string ได้ วันธุรกิจที่บิลนี้ถูกนับ รูป YYYY-MM-DD (ดูด้านล่าง) "2026-09-01"
billDate string ได้ วันปฏิทินบนนาฬิกาของร้าน รูป YYYY-MM-DD — นี่คือช่องที่ from/to กรอง "2026-09-01"
updatedAt string ได้ ครั้งล่าสุดที่บิลเปลี่ยน · ISO-8601 พร้อม offset ร้าน · null บนบิลที่ปิดก่อนมีระบบติดตามการแก้ไข "2026-09-01T13:30:00.000+07:00"
franchiseId string ได้ แฟรนไชส์ที่ร้านสังกัด "fr5Ns8CtJ4vHqZ2WbY7K"
shopId string ไม่ ร้านที่ออกบิล "sh7Kq2mVbN4tRxZ0Lp8W"
deviceId string ได้ เครื่องที่ปิดบิล "dv6Mk1PqR8tZbN3WxL5J"
stationId string ได้ สถานี POS ที่ปิดบิล "st2Wc7YnV5qLpK8ZmR4T"
channel object ไม่ ช่องทางการขาย — { "code", "name" } (ดูด้านล่าง) { "code": "dine_in", "name": "โต๊ะ 3" }
serviceType string ได้ "alacarte" สำหรับการขายปกติ · นอกนั้นคือ รหัส ของแพ็กเกจบุฟเฟ่ต์ที่ขาย ไม่ใช่ชื่อที่แสดง · ไม่ใช่ชุดค่าปิด เพราะแต่ละร้านตั้งแพ็กเกจเองได้ "alacarte"
tableName string ได้ ชื่อโต๊ะ สำหรับบิลที่กินที่ร้าน "โต๊ะ 3"
memberId string ได้ โทเคนสมาชิกแบบทึบ (m1_…) เมื่อบิลผูกกับสมาชิก · ไม่ผูก = null "m1_ZmQ3YjJkNGE1…"
items array ไม่ (เป็น [] ได้) รายการบนบิล (ดูหัวข้อ “รายการสินค้า”) —
amounts object ไม่ ยอดเงินท้ายบิล (ดูหัวข้อ “ยอดเงินและภาษี”) —
payments array ไม่ (เป็น [] ได้) ลูกค้าจ่ายด้วยอะไร (ดูหัวข้อ “การชำระเงิน”) —
status "completed" | "void" ไม่ "void" = บิลถูกยกเลิกทั้งใบหลังปิดบิลแล้ว "completed"
voidReason string ได้ เหตุผลที่บันทึกตอนยกเลิก · ไม่ได้ยกเลิก = null "กดผิดเมนู"
cashier string ได้ ชื่อพนักงานที่ปิดบิล · ไม่ใช่รหัสพนักงาน "สมชาย ใจดี"

channel.code บอกว่าการขายเข้ามาทางไหน:

code ความหมาย
"dine_in" นั่งกินที่ร้าน สั่งที่โต๊ะ
"take_away" สั่งที่เคาน์เตอร์แล้วซื้อกลับบ้าน
"pickup" ลูกค้าสั่งเอง (QR / ลิงก์สั่งอาหาร) แล้วมารับ
รหัสช่องทางที่ร้านตั้งเอง ช่องทางที่ร้านสร้างขึ้น — แพลตฟอร์มเดลิเวอรี · โทรสั่ง · มาร์เกตเพลส
null บิลไม่มีข้อมูลโต๊ะหรือช่องทางติดมา

channel.name คือชื่อที่คนอ่าน: ชื่อช่องทางที่ร้านตั้งไว้ถ้าจับคู่ได้ · ถ้าไม่ได้ก็เป็นชื่อโต๊ะ · ถ้าไม่มีทั้งคู่ = null

คุณต้องเลือก 1 ใน 2 โหมดต่อคำขอ · ส่งทั้ง 2 โหมด หรือไม่ส่งเลย = 400 bad_query เพราะ API จะไม่เดาว่าคุณหมายถึงอันไหน

โหมด A — ช่วงวันปฏิทิน โหมด B — ส่วนที่เปลี่ยน
พารามิเตอร์ from + to updatedSince
กรองที่ช่อง billDate updatedAt
รูปแบบ YYYYMMDD ไม่มีขีด ISO-8601 datetime
ช่วงสูงสุด 31 วัน (นับรวมหัวท้าย) ไม่จำกัด
การเรียง billDate จากน้อยไปมาก updatedAt จากน้อยไปมาก
เหมาะกับ ดึงย้อนหลัง · ปิดยอดสิ้นเดือน ซิงก์ต่อเนื่องทุกไม่กี่นาที

ที่เหมือนกันทั้ง 2 โหมด: shopId บังคับ (ร้านนอกขอบเขตกุญแจ = 403 shop_not_in_scope) · limit ค่าตั้งต้น 100 · cursor ใช้อ่านหน้าถัดไป

limit เป็นค่าที่ระบบ แก้ให้ ไม่ใช่ปฏิเสธ: ค่าเกิน 200 จะถูกลดเหลือ 200 เงียบ ๆ และค่าที่ไม่ใช่จำนวนบวก (0 · ติดลบ · ตัวอักษร) จะตกกลับไปที่ 100 ⇒ ให้ดูที่ nextCursor ไม่ใช่จำนวนแถว เพื่อรู้ว่าดึงครบหรือยัง

ความต่างข้อนี้คือสาเหตุอันดับหนึ่งของอาการ “ยอดไม่ตรง”

businessDate คือวันที่บิลถูกนับเป็น รายได้ ของร้าน · วันธุรกิจของร้านจบตามเวลา ปิดยอดที่ร้านตั้งเอง ซึ่งมักเลยเที่ยงคืนไปแล้ว · billDate คือวันตามปฏิทินธรรมดา · ร้านที่ปิดยอดตอน 06:00 บิลที่จ่ายตอนตี 2 จะมี billDate เป็น วันพรุ่งนี้ แต่ businessDate เป็น เมื่อวาน

การกรองใช้ billDate เสมอ · businessDate กรองไม่ได้ — มันถูกส่งมาให้คุณ จัดกลุ่มเอง หลังดึงข้อมูลมาแล้ว

⇒ การกระทบยอด 1 วันธุรกิจ ต้องดึงช่วงวันปฏิทินที่กว้างกว่า แล้วค่อยจัดกลุ่ม:

  1. ขอ from = วันก่อนหน้า · to = วันถัดไป ของวันธุรกิจที่ต้องการ (รวม 3 วันปฏิทิน)
  2. จัดกลุ่มแถวที่ได้ด้วย businessDate
  3. เก็บเฉพาะกลุ่มที่ต้องการ

โหมด B คืนเฉพาะบิลที่ updatedAt ขยับ · ตราประทับลงเมื่อ:

  • ปิดบิล
  • ออกเลขใบกำกับภาษีให้บิลนั้น
  • ยกเลิก / กลับรายการบิล
  • แก้ไขข้อมูลบนบิล
  • สมาชิกเคลมบิล
  • เขียนโน้ตบนบิล

และ จงใจไม่ประทับ ตอนที่สถานะการส่งออกไปยังระบบบัญชีเปลี่ยน — เพราะนั่นไม่ใช่ การเปลี่ยนเนื้อบิล · ถ้าประทับ บิลจะโผล่ซ้ำในรอบซิงก์ของคุณทั้งที่ไม่มีอะไรต่างเลย

บิลที่ยกเลิกยังถูกคืนมา พร้อม status: "void" ⇒ หน้าที่ตัดออกจากยอดขายเป็นของคุณ · และเพราะการยกเลิกประทับ updatedAt บิลที่คุณเคยนำเข้าไปแล้วเป็น completed จะกลับมา อีกครั้งในโหมด B เป็น void ⇒ ให้ upsert ด้วย id ห้าม append

แต่ละแถวใน items[]:

ฟิลด์ ชนิด เป็น null ได้ไหม ความหมาย
id string ได้ รหัสสินค้า — คีย์สำหรับ join กับแคตตาล็อกของคุณเอง
rootId string ได้ รหัสสินค้าแม่ สำหรับตัวเลือกย่อยและสินค้าในชุด
name string ได้ ชื่อสินค้าตามที่พิมพ์บนบิล
qty number ได้ จำนวน
unitPrice number ได้ ราคาต่อหน่วยที่บันทึกไว้บนบรรทัด · ถ้า POS ไม่ได้บันทึกไว้จะคิดจากยอดรวมบรรทัด ÷ จำนวน · เป็น null เมื่อคิดแบบนั้นไม่ได้ (อ่านจำนวนไม่ได้ · จำนวนเป็น 0 · ไม่มียอดรวมบรรทัด)
lineTotal number ได้ ยอดรวมของบรรทัดนั้น
options array ไม่ (เป็น [] ได้) ตัวเลือกที่ลูกค้าเลือก: { "choiceId", "choiceName", "qty" } · qty ค่าตั้งต้น 1
category array ของ string ได้ หมวดที่สินค้าอยู่ ณ เวลาที่ขาย เป็น รหัสหมวด เรียงจากหมวดบนสุดลงไปหมวดย่อยสุด · ส่วนใหญ่มีรหัสเดียว · สินค้าไม่มีหมวด = []
cancelled boolean ไม่ true เมื่อบรรทัดนี้ถูกยกเลิกระหว่างสั่ง
vat boolean ไม่ บรรทัดนี้คิด VAT ไหม
isTip boolean ไม่ true เมื่อบรรทัดนี้เป็นทิป ไม่ใช่สินค้า

แถวที่ cancelled: true และแถวที่ isTip: true อยู่ใน array ทั้งคู่ ⇒ กรองออกก่อน คำนวณยอดขายรายสินค้า

บิล ไม่มี SKU และไม่มีบาร์โค้ด ⇒ ใช้ items[].id เป็นคีย์ join กับแคตตาล็อกสินค้าของคุณ

ส่วนลดไม่ถูกกระจายลงรายบรรทัด · lineTotal เป็นยอดก่อนหักส่วนลด · ส่วนลดทุกตัว อยู่ที่ระดับบิลใน amounts ⇒ ถ้าต้องการยอดรายสินค้าหลังหักส่วนลด ให้เฉลี่ย amounts.discountTotal ลงแต่ละบรรทัดเองตามกติกาที่คุณเลือก

ทุกค่าเป็น เงินบาท (THB)

ฟิลด์ ชนิด เป็น null ได้ไหม ความหมาย
subtotal number ได้ ผลรวมรายการก่อนหักส่วนลดและค่าธรรมเนียม โดยไม่นับบรรทัดทิป — ดูหมายเหตุใต้ตาราง
discount number ได้ ส่วนลดที่แคชเชียร์กดเอง
campaign array ไม่ (เป็น [] ได้) โปรโมชันที่ใช้ แถวละ 1 โปรโมชัน — ดู campaign[] และ coupon[]
coupon array ไม่ (เป็น [] ได้) คูปองที่ใช้ แถวละ 1 ใบ — รูปแถวเดียวกัน
discountTotal number ได้ ส่วนลดรวม ที่บันทึกไว้บนบิล — มือ + แคมเปญ + คูปอง · ไม่ถูกจำกัดด้วยยอดที่ต้องจ่าย — ดูหมายเหตุใต้ตาราง
serviceCharge number ได้ ค่าบริการ
deliveryFee number ได้ ค่าส่งที่เก็บจากลูกค้า
cardSurcharge number ได้ ค่าธรรมเนียมบัตร
credit number ได้ เครดิตร้านที่ใช้ตัดในบิลนี้
vat number ได้ ยอด VAT
vatType string ได้ โหมด VAT (ดูด้านล่าง)
rounding number ได้ ปัดเศษ
tip number ได้ ทิป
net number ได้ ยอดที่ลูกค้าจ่ายจริง — รวมทิปแล้ว

ทั้งสองลิสต์ใช้รูปแถวเดียวกัน · บิลที่ไม่มีโปรโมชันหรือไม่มีคูปองจะได้ [] ไม่ใช่ null

ฟิลด์ ชนิด เป็น null ได้ไหม ความหมาย
id string ได้ รหัสของโปรโมชันหรือคูปอง ตามที่ร้านตั้งค่าไว้
name string ได้ ชื่อของโปรโมชันหรือคูปองที่คนอ่านรู้เรื่อง
amount number ไม่ ยอดส่วนลดที่แถวนี้ให้ หน่วยบาท · เป็น 0 เมื่อแถวนั้นไม่มียอดลด
ค่า ความหมาย
"Include" ราคาบนเมนูรวม VAT แล้ว · ยอด vat ถูกถอดออกจากยอดรวม
"Exclude" VAT ถูกบวกเพิ่มท้ายบิล
"" ร้านยังไม่เคยตั้งค่า VAT
null บิลไม่มีข้อมูลการตั้งค่า VAT ติดมา

ข้อมูลต้นทุนไม่ถูกส่งออกเลย — ต้นทุนรายบรรทัด · ต้นทุนรวม · สูตรวัตถุดิบ และการตัดสต็อก ถูกกันออกจาก API นี้โดยนโยบาย ทุกระดับกุญแจ ⇒ คำนวณกำไรขั้นต้น จากข้อมูลชุดนี้อย่างเดียวไม่ได้

payments[] บันทึกว่าบิลถูกชำระด้วยอะไร · บิลใบเดียวแยกจ่ายหลายช่องทางได้ จึงเป็น array:

ฟิลด์ ชนิด เป็น null ได้ ความหมาย
name string ได้ ชื่อช่องทางรับเงินตามที่ร้านตั้งไว้ — เช่น "เงินสด" · "โอน" · "บัตรเครดิต"
amount number ได้ ยอดที่ลูกค้า ยื่นให้ ผ่านช่องทางนั้น หน่วยบาท — คือเงินที่ยื่นมา ไม่ใช่ยอดที่ตัดจริง
change number ได้ เงินทอนของรายการชำระแถวนี้ หน่วยบาท · ช่องทางที่ไม่มีเงินทอน = 0

รหัสอ้างอิงภายในของรายการชำระ ไม่ถูกส่งออก · และเพราะ amount คือเงินที่ลูกค้ายื่นมา ผลรวมของ payments[].amount จึงมากกว่า amounts.net ได้ เมื่อจ่ายเงินสดแล้วรับเงินทอน ⇒ ยอดที่ตัดจริงของแต่ละแถวคือ amount - change และตัวเลขชุดนั้นคือตัวที่รวมได้เท่ากับ amounts.net

ScanFood ไม่มีรายการคืนเงินแยกต่างหาก — การคืนเงินหน้าร้านทำโดยการยกเลิกบิล ⇒

  • status มีแค่ 2 ค่า: "completed" และ "void"
  • ไม่มีแนวคิดคืนเงินบางส่วน และไม่มีบิลติดลบ
  • voidReason เก็บสิ่งที่พนักงานพิมพ์ไว้
  • การยกเลิกประทับ updatedAt ⇒ การเปลี่ยนแปลงไปถึงรอบซิงก์แบบต่อเนื่อง

ระบบนำเข้าข้อมูลของคุณจึงควร:

  1. upsert ด้วย id ไม่ใช่ insert ตรง ๆ
  2. คำนวณยอดรวมของงวดใหม่โดยตัด status: "void" ออก แทนที่จะสมมติว่าบิลที่นำเข้าไป เมื่อวานยังใช้ได้อยู่
  3. เก็บแถวที่ยกเลิกไว้ อย่าลบทิ้ง จะได้อธิบายได้ว่าทำไมยอดของวันนั้นถูกแก้

ดึง 1 วันธุรกิจ แล้วจัดกลุ่มด้วย businessDate

Terminal window
# ดึงหน้าต่างวันปฏิทิน 3 วันคร่อมวันธุรกิจ 2026-09-01
# แล้วค่อยจัดกลุ่มด้วย businessDate ที่ฝั่งคุณ
curl -s -G https://api.scanfood.co/ext/v1/transactions \
-H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718" \
--data-urlencode "shopId=sh7Kq2mVbN4tRxZ0Lp8W" \
--data-urlencode "from=20260831" \
--data-urlencode "to=20260902" \
--data-urlencode "limit=200"

เก็บค่า updatedAt ที่สูงที่สุดที่นำเข้าไปแล้ว แล้วส่งกลับมาเป็น updatedSince ในรอบถัดไป · เพราะข้อมูลเรียงตาม updatedAt จากน้อยไปมาก การส่งค่าเดิมซ้ำจึงปลอดภัย: คุณจะได้บิลที่คาบเส้นกลับมาอีกใบแล้ว upsert ทับตัวมันเอง

Terminal window
curl -s -G https://api.scanfood.co/ext/v1/transactions \
-H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718" \
--data-urlencode "shopId=sh7Kq2mVbN4tRxZ0Lp8W" \
--data-urlencode "updatedSince=2026-09-01T06:15:00.000Z" \
--data-urlencode "limit=200"
Terminal window
curl -s https://api.scanfood.co/ext/v1/transactions/tx9Bd2LmQ7sVfR4KpN1C \
-H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718"

บิลที่ไม่มีอยู่จริง กับบิลของร้านที่อยู่นอกขอบเขตกุญแจ คืน 404 not_found เหมือนกัน — API จะไม่ยืนยันว่า id นั้นมีอยู่จริงนอกขอบเขตของคุณ

{
"ok": true,
"data": [
{
"id": "tx9Bd2LmQ7sVfR4KpN1C",
"orderIds": ["or4Tq8ZmB2vNs6WkY9Hd", "orH5nP2wQ8tLcZ3RvK7M"],
"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",
"franchiseId": null,
"shopId": "sh7Kq2mVbN4tRxZ0Lp8W",
"deviceId": "dv6Mk1PqR8tZbN3WxL5J",
"stationId": "st2Wc7YnV5qLpK8ZmR4T",
"channel": { "code": "ch4Rn9WsT2kLqZ6BvY8M", "name": "แกร็บฟู้ด" },
"serviceType": "alacarte",
"tableName": null,
"memberId": "m1_ZmQ3YjJkNGE1ZTZmNzg5MGExYjJjM2Q0ZTVmNjc4OTBhYmNkZWYxMjM",
"items": [
{
"id": "pd8Zt3QvL6mNc1KwB9Ys",
"rootId": "rt1Kp5MwZ9bQn7VxL3Cd",
"name": "สปาเก็ตตี้ขี้เมา",
"qty": 3,
"unitPrice": 137.55,
"lineTotal": 412.65,
"options": [],
"category": ["9369601f-fef5-4a61-bfb8-26f25eab3dad"],
"cancelled": false,
"vat": true,
"isTip": false
}
],
"amounts": {
"subtotal": 412.65,
"discount": 20,
"campaign": [],
"coupon": [],
"discountTotal": 20,
"serviceCharge": 0,
"deliveryFee": 0,
"cardSurcharge": 0,
"credit": 0,
"vat": 0,
"vatType": "Include",
"rounding": 0,
"tip": 0,
"net": 392.65
},
"payments": [{ "name": "เงินสด", "amount": 400, "change": 7.35 }],
"status": "completed",
"voidReason": null,
"cashier": "สมชาย ใจดี"
}
],
"nextCursor": null,
"serverTime": "2026-09-01T06:30:00.000Z"
}

เป็นนโยบาย และถูกบังคับด้วยรูปร่างของ response เอง:

กลุ่ม ไม่มีให้
ต้นทุน ต้นทุนรายบรรทัด · ต้นทุนรวม · สูตรวัตถุดิบ · การตัดสต็อก
ตัวตนลูกค้า ชื่อลูกค้า · ชื่อสมาชิก · เบอร์โทร · อีเมล · ที่อยู่ · ข้อมูลผู้ขอใบกำกับภาษี
หลักฐานและโน้ต สลิป/ภาพหลักฐานการโอน · โน้ตบนบิล · รูปที่อัปโหลด
ร่องรอยพนักงาน รหัสพนักงาน · ผู้ยกเลิกบิล · รหัสกะและผู้จัดการ
ปฏิบัติการหน้าร้าน เลขติดตามพัสดุ/เดลิเวอรี · รายละเอียดการรับของ · พนักงานที่ผูกรายสินค้า
อ้างอิงภายใน รหัสอ้างอิงเกตเวย์รับชำระ · สถานะการส่งออกไประบบบัญชี · ข้อมูลการแก้ไข

ถ้ารายงานที่ต้องทำจำเป็นต้องใช้ของพวกนี้ = ทำจาก API นี้ไม่ได้ ⇒ บอกกันตั้งแต่ต้น ดีกว่าออกแบบระบบรอบ ๆ ช่องที่จะไม่มีวันมา