Time zone & business date
A restaurant’s day does not end at midnight. ScanFood therefore records two different dates on every bill, and picking the wrong one is the most common reason an export disagrees with the shop’s own report.
Time zone
Section titled “Time zone”Each shop has its own time zone, returned as an IANA name by GET /v1/shops:
{ "shopId": "aK3mP9xQ2bR7cT4dV6wY", "timezone": "Asia/Bangkok" }The value is null for a shop that never set one, which is the normal state for
older shops. Treat null as Asia/Bangkok — that is what ScanFood itself does
when it formats that shop’s dates. Do not pass the raw value to a zone parser
without that fallback, or the first old branch you meet will break the run.
Every timestamp is formatted for the reader who cares about it:
| Field | Format | Zone |
|---|---|---|
timestamp, updatedAt on a transaction |
ISO-8601 with an offset, e.g. 2026-09-01T13:30:00.000+07:00 |
The shop’s zone |
createdDate, coupons[].expireAt on a member |
ISO-8601 with an offset | The member brand’s zone |
businessDate, billDate, lastVisit |
YYYY-MM-DD |
A wall-clock date in that same zone — no time, no offset |
serverTime on every response |
ISO-8601 ending in Z |
UTC |
from / to in a request |
YYYYMMDD, no dashes |
Calendar days in the shop’s zone |
updatedSince in a request |
ISO-8601 | Any zone you like — send Z and avoid the question |
Because the offset travels with the timestamp, you never have to know a shop’s
zone to sort or compare bills — parse them as instants. You only need the zone
when you want to say “which day was that?”, and for that the API has already
done the work: read billDate or businessDate rather than re-deriving a day
from the timestamp.
Calendar date vs business date
Section titled “Calendar date vs business date”| Field | Question it answers |
|---|---|
billDate |
On which calendar day was this bill paid, on the clock on the shop’s wall? |
businessDate |
Which trading day’s takings does this bill belong to? |
For a shop that closes before midnight the two are always identical. For a bar that stops serving at 03:00 they diverge every single night — and that is the whole point of having both.
The shop cut-off
Section titled “The shop cut-off”Every shop sets a cut-off time: the moment its trading day rolls over. A shop with a 06:00 cut-off treats everything rung up between 06:00 today and 05:59 tomorrow as one trading day — which is exactly how its own end-of-day report, its cash count and its staff think about it.
businessDate applies that rule for you. You do not need to know the cut-off,
fetch it, or replicate the arithmetic.
Choosing a filter
Section titled “Choosing a filter”Two rules cover every case:
- Filter with
from/to, which matchbillDate. There is no way to filter by business date; the range parameters look at the calendar day only. - Group by
businessDatewhenever the number has to agree with the shop. Revenue, VAT, daily takings, commission — all of these are trading-day figures.
That combination has one consequence worth designing around: to be sure you hold
every bill of a trading day, you must fetch the calendar day and the
following one, then group by businessDate. A bill rung up at 01:00 on 2
September carries billDate: "2026-09-02" while belonging to the trading day of
1 September.
The safe pattern for a nightly export:
- Fetch
from= the day you are reporting,to= the day after. - Keep only the rows whose
businessDateis the day you are reporting. - Re-run the day once more after the shop’s cut-off has passed, since late voids and re-issued tax invoices change bills after the fact — the incremental mode described in Pagination & sync catches these automatically.
Worked example
Section titled “Worked example”A shop in Asia/Bangkok with a 06:00 cut-off. Two bills:
| Paid at | timestamp |
billDate |
businessDate |
|---|---|---|---|
| 1 Sep, 13:30 | 2026-09-01T13:30:00.000+07:00 |
2026-09-01 |
2026-09-01 |
| 2 Sep, 01:40 | 2026-09-02T01:40:00.000+07:00 |
2026-09-02 |
2026-09-01 |
The shop’s own report for 1 September contains both bills. So:
- Requesting
from=20260901&to=20260901returns only the first — the second has a September 2 calendar date. Your total is short. - Requesting
from=20260901&to=20260902returns both, plus everything that genuinely belongs to 2 September. Filtering that result onbusinessDate === "2026-09-01"gives you exactly the shop’s number.
If you compare against a shop’s daily report and land a few bills short, this is almost always why.