Skip to content

Transactions

A transaction is one closed bill: what was ordered, what was discounted, what tax was applied, what the customer paid with, and whether the bill was later voided. This is the resource an accounting system, a BI warehouse or a daily reconciliation job spends almost all of its time on.

Two endpoints:

Both are read-only. Nothing you do here changes anything inside ScanFood.

A bill is created when a cashier closes a sale. One bill can absorb several orders (a table that ordered three times pays once), which is why orderIds is an array.

What the API returns is a fixed, hand-picked projection of the bill — a list of fields written out one by one on the server. A real bill in storage carries far more than this, including cost prices and recipes. Nothing outside the list below can appear in the response, now or after a future change.

Who uses this:

Use case What you pull
Daily reconciliation with the shop’s own report one calendar range per shop, grouped by businessDate
Continuous sync into a warehouse updatedSince, every 15 minutes
Menu / item analytics items[] across a date range
Loyalty attribution memberId on the bill, joined to Members
Channel P&L (dine-in vs delivery) channel.code + amounts

Response envelope for the list endpoint:

{
"ok": true,
"data": [ { "…": "one object per bill" } ],
"nextCursor": null,
"serverTime": "2026-09-01T06:30:00.000Z"
}

The single-bill endpoint returns the same object under data with no nextCursor.

Field Type Nullable Meaning Example
id string no Bill identifier. Stable, safe to use as a primary key, and also the value used as a cursor. "tx9Bd2LmQ7sVfR4KpN1C"
orderIds string[] no (may be []) The orders merged into this bill. ["or4Tq8ZmB2vNs6WkY9Hd"]
receiptNumber string yes The receipt number printed for the customer. "RC-001"
taxNumber string yes Full tax invoice number. Only present on bills where a tax invoice was issued. "TIV-2026-000144"
timestamp string yes When the bill was closed. ISO-8601 with the shop’s UTC offset. "2026-09-01T13:30:00.000+07:00"
businessDate string yes The trading day this bill counts towards, YYYY-MM-DD. See below. "2026-09-01"
billDate string yes The calendar day on the shop’s clock, YYYY-MM-DD. This is the field from/to filter on. "2026-09-01"
updatedAt string yes Last time the bill changed. ISO-8601 with shop offset. null on bills closed before change tracking existed. "2026-09-01T13:30:00.000+07:00"
franchiseId string yes Franchise the shop belongs to. "fr5Ns8CtJ4vHqZ2WbY7K"
shopId string no Shop that issued the bill. "sh7Kq2mVbN4tRxZ0Lp8W"
deviceId string yes Device that closed the bill. "dv6Mk1PqR8tZbN3WxL5J"
stationId string yes POS station that closed the bill. "st2Wc7YnV5qLpK8ZmR4T"
channel object no Sales channel — { "code", "name" }, see below. { "code": "dine_in", "name": "โต๊ะ 3" }
serviceType string yes "alacarte" for a normal sale, otherwise the id of the buffet package that was sold, not its display name. Not a closed set — each shop defines its own packages. "alacarte"
tableName string yes Table name for dine-in bills. "โต๊ะ 3"
memberId string yes Opaque member token (m1_…) when the bill was attached to a member; null otherwise. "m1_ZmQ3YjJkNGE1…"
items array no (may be []) Lines on the bill, see Items. —
amounts object no Bill totals, see Amounts and tax. —
payments array no (may be []) How the customer paid, see Payments. —
status "completed" | "void" no "void" means the whole bill was cancelled after it was closed. "completed"
voidReason string yes Reason recorded at cancellation; null when not voided. "กดผิดเมนู"
cashier string yes Display name of the staff member who closed the bill. Not a staff code. "สมชาย ใจดี"

channel.code tells you how the sale reached the shop:

code Meaning
"dine_in" Eaten in, ordered at a table
"take_away" Rung up at the counter to be taken away
"pickup" Customer ordered themselves (QR / self-order link) and collected
a shop-defined channel id A channel the shop created — delivery platform, phone order, marketplace
null The bill carries no table or channel reference

channel.name is the human label: the shop’s own channel name when it matches, otherwise the table name, otherwise null.

You must choose exactly one of two modes per request. Sending both, or neither, is rejected with 400 bad_query — the API will not guess which one you meant.

Mode A — calendar range Mode B — incremental
Parameters from + to updatedSince
Filters on billDate updatedAt
Format YYYYMMDD, no dashes ISO-8601 datetime
Maximum span 31 days, inclusive unlimited
Order billDate ascending updatedAt ascending
Best for backfills, month-end reconciliation continuous sync every few minutes

Common to both: shopId is required (a shop outside your key’s scope gives 403 shop_not_in_scope), limit defaults to 100, and cursor continues the previous page.

limit is corrected rather than rejected: a value above 200 is silently reduced to 200, and a value that is not a positive number (0, negative, text) falls back to 100. Check nextCursor, not the row count, to know whether you have everything.

This distinction is the single most common source of “the numbers don’t match”.

businessDate is the day the bill counts as revenue for the shop. A shop’s trading day ends at its own cut-off time, which is usually after midnight. billDate is the ordinary calendar day. For a shop that closes its day at 06:00, a bill paid at 02:00 has tomorrow’s billDate but yesterday’s businessDate.

Filtering always uses billDate. businessDate cannot be filtered on — it is returned so that you can group by it yourself after fetching.

That means a reconciliation for one trading day must fetch a slightly wider calendar range and then group:

  1. Request from = the day before, to = the day after the trading day you want (three calendar days).
  2. Group the returned rows by businessDate.
  3. Keep only the group you asked for.

Mode B only ever returns bills whose updatedAt moved. It is stamped when:

  • a bill is closed,
  • a tax invoice number is issued for it,
  • it is voided or reversed,
  • a field on it is edited,
  • a member claims the bill,
  • a note is written on it.

It is deliberately not stamped when the bill’s accounting-export status changes — that is not a change to the bill’s contents, and stamping it would make bills reappear in your sync with nothing different about them.

A voided bill is still returned, with status: "void". It is your job to exclude it from revenue. Because voiding stamps updatedAt, a bill you already imported as completed will come back through Mode B as void — upsert by id, do not append.

Each entry in items[]:

Field Type Nullable Meaning
id string yes Product id — join key to your own catalogue
rootId string yes Parent product id, for variants and items inside a set
name string yes Product name as printed on the bill
qty number yes Quantity
unitPrice number yes Price per unit recorded on the line. Where the POS recorded none it is derived as line total ÷ quantity, and is null when even that is not possible (no readable quantity, quantity 0, or no line total).
lineTotal number yes Total for the line
options array no (may be []) Chosen options: { "choiceId", "choiceName", "qty" }; qty defaults to 1
category array of string yes Category the product sat in at the time of sale, as category ids ordered from the top-level category down to the most specific one. Most lines carry one id; [] when the product had no category
cancelled boolean no true when the line was cancelled during ordering
vat boolean no Whether the line is subject to VAT
isTip boolean no true when the line is a tip, not a product

Lines with cancelled: true and lines with isTip: true are both present in the array. Filter them out before you compute item-level sales.

There is no SKU and no barcode on the line. Use items[].id as the join key against your own product catalogue.

Discounts are not distributed across lines. lineTotal is a gross line value; every discount lives at bill level in amounts. If you need discount-adjusted item revenue, allocate amounts.discountTotal across lines yourself using a rule you choose.

All values are in Thai baht (THB).

Field Type Nullable Meaning
subtotal number yes Sum of the item lines before discounts and fees, with tip lines left out — see the note below the table
discount number yes Manual discount entered by the cashier
campaign array no (may be []) Promotions applied, one row per promotion — see campaign[] and coupon[]
coupon array no (may be []) Coupons redeemed, one row per coupon — same row shape
discountTotal number yes Total discount recorded on the bill — manual + campaigns + coupons. Not capped at the amount due — see the note below the table
serviceCharge number yes Service charge
deliveryFee number yes Delivery fee charged to the customer
cardSurcharge number yes Card surcharge
credit number yes Store credit consumed on this bill
vat number yes VAT amount
vatType string yes VAT mode — see below
rounding number yes Rounding adjustment
tip number yes Tip
net number yes What the customer actually paid — tip included

Both lists share one row shape. A bill with no promotion or no coupon carries [], never null.

Field Type Nullable Meaning
id string yes Identifier of the promotion or coupon, as the shop configured it
name string yes Readable name of the promotion or coupon
amount number no Discount this row contributed, in THB. 0 when the row carried no discount amount
Value Meaning
"Include" Menu prices already contain VAT; the vat amount is extracted from the total
"Exclude" VAT is added on top at the end of the bill
"" The shop has never configured VAT
null The bill carries no VAT setting

No cost data is ever returned. Cost per line, total cost, recipes and ingredient consumption are excluded from this API by policy, at every key level. Margin cannot be computed from this data alone.

payments[] records how the bill was settled. A single bill can be split across several methods, so this is an array:

Field Type Nullable Meaning
name string yes The payment channel’s name, as the shop configured it — e.g. "เงินสด", "โอน", "บัตรเครดิต"
amount number yes Amount tendered through that channel, in THB — what the customer handed over, not what was settled
change number yes Change given back on this payment, in THB. 0 on methods that give none

The internal payment record id is not returned. Because amount is what the customer handed over, the sum of payments[].amount can exceed amounts.net when cash was paid and change given back. What actually settled on a row is amount - change, and it is those figures that add up to amounts.net.

ScanFood has no separate refund object. A refund at the counter is performed by voiding the bill. That means:

  • status has exactly two values: "completed" and "void".
  • There is no partial-refund concept and no negative bill.
  • voidReason carries whatever the staff member typed.
  • Voiding stamps updatedAt, so the change reaches an incremental sync.

Your importer should therefore:

  1. Upsert on id — never insert blindly.
  2. Recompute period totals excluding status: "void" rather than assuming a bill you imported yesterday is still valid.
  3. Keep voided rows rather than deleting them, so that a restated day can be explained.

Pull one trading day and group by businessDate.

Terminal window
# Fetch a 3-day calendar window around the trading day 2026-09-01,
# then group by businessDate on your side.
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"

Store the highest updatedAt you have imported and pass it back as updatedSince on the next run. Because the feed is ordered by updatedAt ascending, re-sending the last stored value is safe: you will re-receive the boundary bill and upsert it over itself.

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"

A bill that does not exist and a bill belonging to a shop outside your key both return the same 404 not_found — the API will not confirm that an id exists outside your scope.

{
"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"
}

By policy, and enforced by the shape of the response itself:

Category Not available
Cost cost per line, total cost, recipes, ingredient consumption, stock movements
Customer identity customer name, member name, phone, email, address, tax-invoice customer details
Evidence and notes transfer slips and payment evidence images, bill notes, uploaded images
Staff traces staff codes, the staff member who voided, shift and manager references
Shop operations delivery tracking numbers, pickup details, per-line staff assignment
Internal references payment gateway reference ids, accounting-export state, edit metadata

If a report needs any of these, it cannot be produced from this API — say so early rather than designing around a field that will never arrive.