Errors
Every failure comes back as JSON with the same shape, whatever went wrong. The
HTTP status tells you the class of problem; the error field tells you exactly
which one.
Error shape
Section titled “Error shape”{ "ok": false, "error": "shop_not_in_scope", "message": "this API key cannot access that shopId"}| Field | Always present | Notes |
|---|---|---|
ok |
Yes | Always false on an error. Successful responses carry "ok": true |
error |
Yes | A stable, machine-readable code. Branch on this |
message |
In practice, yes | Human-readable, in English. May be reworded at any time |
retryAfterSec |
Only on rate_limited |
Seconds to wait. That response also carries limit and windowSec |
Status codes
Section titled “Status codes”| Status | Meaning | Is it worth retrying? |
|---|---|---|
200 |
Success. An empty data array is still a success |
— |
400 |
Your request is malformed: a missing or contradictory parameter, or a dead cursor | No — fix the request |
401 |
The key is missing, wrong or revoked | No — fix the key |
403 |
The key is valid, but the shop or member brand you asked for is outside its scope — or the call needs a per-key permission the key was not granted | No — ask for an id the key can read, or for the permission |
404 |
The transaction or member does not exist, or is outside your scope | No |
429 |
Too many requests this minute, or the daily quota is gone | Yes, after waiting |
500 |
Something failed on our side | Yes, with backoff |
503 |
Key verification is temporarily unavailable on our side | Yes, with backoff |
Note the deliberate asymmetry between 403 and 404. Listing endpoints answer
403 because you already knew the shopId or providerId you sent. Single-item
endpoints answer 404 for both “no such bill” and “a bill in a shop you cannot
read”, so that the response never confirms whether an id exists somewhere else
in ScanFood.
Error codes
Section titled “Error codes”| Status | error |
When it happens |
|---|---|---|
400 |
bad_query |
shopId is required — the transactions list needs one |
400 |
bad_query |
providerId is required — the members list needs one |
400 |
bad_query |
choose exactly one mode: from+to (calendar days) or updatedSince (ISO-8601) — you sent both, or neither |
400 |
bad_query |
updatedSince must be an ISO-8601 datetime — the value is parsed leniently today, so a loose format such as 2026/09/01 may slip through. Send real ISO-8601 anyway; that is the format we support |
400 |
bad_query |
from/to must be YYYYMMDD — requests use no dashes |
400 |
bad_query |
from must be <= to |
400 |
bad_query |
range too wide: N days (max 31) |
400 |
bad_query |
id is required — no transaction id in the path |
400 |
bad_query |
The member lookup was called without a providerId, or with a lineUserId that is not U followed by 32 hexadecimal characters |
400 |
bad_cursor |
cursor no longer exists — the row it pointed at is gone |
400 |
bad_cursor |
cursor is not a valid member token — you sent something that is not a m1_… token |
400 |
not_supported |
updatedSince on the members list. Incremental member sync is not open yet; page through with cursor instead |
401 |
invalid_key |
Invalid API key — missing, malformed, unknown or wrong. All four answer identically |
401 |
key_revoked |
API key has been revoked |
403 |
shop_not_in_scope |
this API key cannot access that shopId |
403 |
provider_not_in_scope |
this API key cannot access that providerId |
403 |
lookup_not_enabled |
Member lookup is not switched on for this key. It is granted per key, like tel and email — see Members |
404 |
not_found |
transaction not found — unknown id, or a shop outside your scope |
404 |
not_found |
member not found — unknown or unreadable token, or a brand outside your scope |
404 |
not_found |
No member matched a LINE user id you looked up. One answer for three situations: not a member of this brand, a member of another brand, or nobody at all |
429 |
rate_limited |
Too many requests, retry in about N seconds — more than the per-minute burst. Body carries retryAfterSec, limit, windowSec; the header Retry-After says the same |
429 |
quota_exceeded |
daily quota exceeded (N requests/day, Thailand time). Resets at 00:00 ICT. |
500 |
internal |
temporary failure, please retry — our side |
503 |
auth_unavailable |
Authentication service temporarily unavailable, retry later — key verification is temporarily unavailable. We answer 503 rather than 401 so you do not go hunting through a key that is perfectly fine |
Retrying safely
Section titled “Retrying safely”Every endpoint is a GET that changes nothing a shop owns, so a retry can
never duplicate a bill or double-apply anything. The trace a repeated call
leaves is one more tick on your quota and one more row in our request log,
which we keep for 90 days.
A retry policy that works:
| Situation | Policy |
|---|---|
429 rate_limited |
Wait Retry-After seconds, then retry. Do not shorten the wait |
429 quota_exceeded |
Stop until 00:00 ICT, or ask for a bigger quota. Retrying sooner just burns requests |
500, 503, connection reset, timeout |
Exponential backoff with jitter — for example 1s, 2s, 4s, 8s, up to five attempts |
400, 401, 403, 404 |
Do not retry. Nothing about the same request will succeed a second time |
Two more habits worth having:
- Log the
errorcode, the status and the request parameters — but never the key. The 12-character key id is safe to log; the secret half is not. - Handle a dead cursor as a restart, not as a failure.
bad_cursormeans the row your cursor pointed at is gone. Drop the cursor and re-request that page from the start of the range; see Pagination & sync.