Skip to content

List transactions for one shop

GET
/v1/transactions
curl --request GET \
--url 'https://api.scanfood.co/ext/v1/transactions?shopId=cQpATdNiK697JkaqHg5k&from=20260901&to=20260930&limit=100' \
--header 'Authorization: Bearer <token>'

Returns bills for a single shop, oldest first. Choose exactly one selection mode: from+to (calendar days) or updatedSince (incremental). Sending both, or neither, returns 400 bad_query.

Mode A — calendar range (backfill, daily reconciliation · YYYYMMDD, no dashes · at most 31 days per call):

GET /v1/transactions?shopId=cQpATdNiK697JkaqHg5k&from=20260901&to=20260930&limit=200

Mode B — incremental sync (scheduled job · pass the serverTime of your previous call · returns bills created or changed since then, voids included):

GET /v1/transactions?shopId=cQpATdNiK697JkaqHg5k&updatedSince=2026-09-09T00:00:00Z&limit=200

Both modes page the same way: when nextCursor is not null, repeat the request with &cursor=<nextCursor>. The generated sample below shows Mode A only.

shopId
required
string

Shop to read. Must be one of the shops returned by /v1/shops.

from
string
/^\d{8}$/

First calendar day to include, in the shop’s timezone (YYYYMMDD). Requires to.

to
string
/^\d{8}$/

Last calendar day to include, inclusive. The span from..to may not exceed 31 days.

updatedSince
string format: date-time

Return only bills changed at or after this instant (ISO-8601). Cannot be combined with from/to.

limit
integer
default: 100

Rows per page. Out-of-range values are never rejected, they are adjusted silently: any fraction is truncated first, then anything above 200 is reduced to 200 and anything that is left below 1 (0, negative, a fraction under 1, text, missing) falls back to 100.

cursor
string

The nextCursor from the previous page.

OK

Media typeapplication/json
object
ok
required
boolean
data
required
Array<object>

One bill (transaction) recorded by the POS. Every field below is always present in the response — a field with nothing to report comes back as null (or [] for the lists), never missing. Fields not listed here are never sent, whatever the POS stored on the bill.

object
id
required

Transaction id. Stable. Use it with /v1/transactions/{id} and as a cursor.

string
orderIds
required

Orders that were settled into this bill. Empty array when the POS recorded none.

Array<string>
receiptNumber
required

Receipt number printed for the customer.

string | null
taxNumber
required

Full tax-invoice number, when one was issued.

string | null
timestamp
required

When the bill was closed. ISO-8601 with the shop’s own UTC offset, e.g. 2026-09-09T14:03:05.120+07:00.

string | null format: date-time
businessDate
required

Accounting day the bill is counted in (YYYY-MM-DD). Taken from the trading-day stamp the bill carries, or worked out from the bill time and the shop’s own closing time when that stamp is absent. null only when neither is possible — the bill has no readable time, or the shop has no closing time configured.

string | null
billDate
required

Calendar day in the shop’s timezone (YYYY-MM-DD). This is the field from/to filter on — the request parameters use YYYYMMDD (no dashes) while the response uses dashes.

string | null
updatedAt
required

Last time the bill changed. ISO-8601 with the shop’s own UTC offset. null on bills written before change tracking existed — those never appear in updatedSince mode. Sending a bill to an accounting integration does not count as a change and does not move this field.

string | null format: date-time
franchiseId
required

Franchise the shop belongs to.

string | null
shopId
required

Shop that issued the bill.

string
deviceId
required

Device that closed the bill.

string | null
stationId
required

POS station that closed the bill.

string | null
channel
required

Where the sale came from.

object
code
required

dine_in — served at a table in the shop. take_away — rung up at the counter to be taken away. pickup — ordered for pick-up (QR / self-order link). Any other value is the shop’s own sales-channel id (delivery apps, marketplaces, call-in, and so on) exactly as the shop configured it; the readable label is in channel.name. Treat this as an open string, not a closed enum — shops add channels at any time. null when the bill carries no table/channel reference.

string | null
name
required

Readable channel name as configured by the shop (for example หน้าร้าน, Grab).

string | null
serviceType
required

alacarte on a normal sale; otherwise the id of the buffet package that was sold, not its display name. Not a fixed set — each shop defines its own packages.

string | null
tableName
required

Table name, for dine-in bills.

string | null
memberId
required

Opaque, stable member token (prefix m1_) when the bill is linked to a member; null otherwise. The same token is used by the Member API, so you can join the two. It is not the customer’s real identifier and cannot be reversed.

string | null
items
required
Array<object>

One line on the bill. Every field below is always present.

object
id
required

Product id. Join to your own catalogue with this.

string | null
rootId
required

Parent product id for variants/set members.

string | null
name
required

Product name as printed on the bill.

string | null
qty
required

Quantity sold on this line. null when the POS recorded no readable quantity.

number | null
unitPrice
required

Price per unit recorded on the line (THB). Where the POS recorded none it is worked out as line total ÷ quantity, and is null when even that is not possible (no readable quantity, quantity 0, or no line total).

number | null
lineTotal
required

Total for this line (THB).

number | null
options
required

Chosen options/add-ons for this line. Empty array when there are none.

Array<object>
object
choiceId
required

Option/choice id.

string | null
choiceName
required

Option/choice name as printed on the bill.

string | null
qty
required

How many of this option were taken on the line. Defaults to 1 when the POS recorded no quantity.

number
category
required

Category path the product sat in at sale time, as category ids ordered from the top-level category down to the most specific one. Most bills carry one id. Ids, not names — they stay stable when staff rename a category. [] when the product had no category. Turn ids into names with categories from /v1/shops (match on id). An id with no match there belongs to a category the shop has since deleted; its name cannot be recovered.

Array<string> | null
cancelled
required

true when the line was voided during the order; cancelled lines do not contribute to the totals.

boolean
vat
required

Whether this line is VAT-applicable.

boolean
isTip
required

true when the line is a tip, not a sold item.

boolean
amounts
required

Money on the bill, in THB. Every field below is always present. A money field is null when the shop does not run that feature at all, which is deliberately different from 0 (“the feature is on and the amount happened to be zero”).

object
subtotal
required

Sum of the item lines before discounts and charges, with tip lines left out. A tip still appears in items[] with isTip: true, so on a bill that carries a tip this will not equal the sum of lineTotal.

number | null
discount
required

Manual discount entered by the cashier.

number | null
campaign
required

Promotions applied to this bill. Empty array when none were used.

Array<object>

One promotion or coupon applied to the bill.

object
id
required

Identifier of the promotion/coupon as configured by the shop.

string | null
name
required

Readable name of the promotion/coupon.

string | null
amount
required

Discount this line contributed (THB). 0 when it carried no discount amount.

number
coupon
required

Coupons applied to this bill. Empty array when none were used.

Array<object>

One promotion or coupon applied to the bill.

object
id
required

Identifier of the promotion/coupon as configured by the shop.

string | null
name
required

Readable name of the promotion/coupon.

string | null
amount
required

Discount this line contributed (THB). 0 when it carried no discount amount.

number
discountTotal
required

Total discount recorded on the bill (manual + campaign + coupon), as computed by the POS. It is not capped at the amount due, so on a bill where the discounts add up to more than the bill itself, this is larger than the amount that could actually be taken off.

number | null
serviceCharge
required

Service charge.

number | null
deliveryFee
required

Delivery fee charged to the customer.

number | null
cardSurcharge
required

Card surcharge charged to the customer.

number | null
credit
required

Store credit redeemed on this bill.

number | null
vat
required

VAT amount.

number | null
vatType
required

VAT mode of the bill. The values in use are Include (prices already contain VAT, which is extracted from the total) and Exclude (VAT is added on top at the end). Shops that never configured VAT can carry an empty string. Treat it as an open string.

string | null
rounding
required

Rounding adjustment.

number | null
tip
required

Tip collected. Included in net but not shop revenue.

number | null
net
required

Amount the customer paid, including tip.

number | null
payments
required
Array<object>

One payment taken against the bill. A bill can be split across several.

object
name
required

Payment method name as configured by the shop (cash, PromptPay, card, …).

string | null
amount
required

Amount tendered through this method (THB) — what the customer handed over, not what was settled. For cash that can be more than the bill. The amount that actually settled is amount - change, and it is that figure which adds up to amounts.net across the list.

number | null
change
required

Change given back on this payment (THB). 0 on methods that give none. null only when the recorded value cannot be read as a number.

number | null
status
required

void means the whole bill was cancelled after being closed. Voided bills are still returned — exclude them from revenue yourself.

string
Allowed values: completed void
voidReason
required

Reason recorded when the bill was voided.

string | null
cashier
required

Name of the staff member who closed the bill.

string | null
nextCursor

Pass back as cursor to fetch the next page. null means no more rows.

string | null
serverTime
required

Server clock when the response was produced (UTC, ISO-8601).

string format: date-time
Example
{
"ok": true,
"data": [
{
"businessDate": "2026-08-31",
"billDate": "2026-09-01",
"channel": {
"code": "dine_in"
},
"items": [
{
"options": [
{
"qty": 1
}
],
"category": [
"9369601f-fef5-4a61-bfb8-26f25eab3dad",
"eff254c9-3d71-4d6f-9e43-1806ece6a427"
]
}
],
"amounts": {
"vatType": "Include"
},
"payments": [
{
"amount": 400,
"change": 7.35
}
],
"status": "completed"
}
],
"serverTime": "2026-09-09T13:45:12.310Z"
}
X-RateLimit-Limit
integer

Daily quota for this key — requests allowed per Thailand-time calendar day. This is not the per-minute burst limit; hitting that limit returns rate_limited with a Retry-After header, and is not reflected in these two headers. Not sent on every response. The pair is added by the daily meter, which runs after the key check and after the per-minute brake, so 401, 503, and 429 rate_limited carry neither header. Everything that reaches the meter — including 429 quota_exceeded and 4xx/5xx raised by the handler itself — carries both.

X-RateLimit-Remaining
integer

Requests left in the current Thailand-time day for this key, after this request. Sent under the same conditions as X-RateLimit-Limit.

Malformed request. error is one of: bad_query (missing/invalid parameters, both or neither selection mode, range over 31 days, a lineUserId that is not shaped like a LINE user id), bad_cursor (the cursor is not valid, or points at a row that no longer exists), not_supported (a parameter that exists in this document is not available yet — today only updatedSince on /v1/members; the message says what to do instead).

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "bad_query",
"message": "choose exactly one mode: from+to (calendar days) or updatedSince (ISO-8601)"
}

Missing or unusable key. error is invalid_key (absent, malformed, or wrong secret — the API does not reveal whether the key id exists) or key_revoked (the key was revoked).

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "invalid_key",
"message": "Invalid API key"
}

The requested scope is not yours. error is one of: shop_not_in_scope (the requested shopId is outside this key’s scope), provider_not_in_scope (the requested providerId is outside this key’s scope), lookup_not_enabled (this key is not allowed to look members up by LINE user id; that permission is granted per key and is off by default).

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "shop_not_in_scope",
"message": "this API key cannot access that shopId"
}

rate_limited — too many requests per minute for this key. The body carries retryAfterSec, limit (the per-minute ceiling that was hit) and windowSec (the window it was measured over), and the response also has a Retry-After header. X-RateLimit-* are not sent on this one, because the per-minute brake sits in front of the daily meter. quota_exceeded — the daily quota for this key is used up; it resets at 00:00 Thailand time (ICT). This one does carry X-RateLimit-*.

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "quota_exceeded",
"message": "daily quota exceeded (10000 requests/day, Thailand time). Resets at 00:00 ICT."
}
Retry-After
integer

Seconds to wait before trying again. Sent on 429 rate_limited (the per-minute brake). Not sent on 429 quota_exceeded, where the wait is until the next Thailand-time day.

internal — temporary failure on our side. Retry with backoff.

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "internal",
"message": "temporary failure, please retry"
}

auth_unavailable — the store we check keys against could not be read, so the request was refused instead of being let through. Your key is fine; this is our side. Retry with backoff. X-RateLimit-* are not sent on this response.

Media typeapplication/json
object
ok
required
boolean
error
required

Stable machine-readable code. Branch on this, not on message.

string
message

Human-readable explanation. May change without notice.

string
retryAfterSec

Present on rate_limited. Seconds to wait before retrying.

integer
limit

Present on rate_limited. The per-minute ceiling that was hit for this key.

integer
windowSec

Present on rate_limited. Length of the window limit was measured over, in seconds.

integer
Example
{
"ok": false,
"error": "auth_unavailable",
"message": "Authentication service temporarily unavailable, retry later"
}