Skip to content

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.

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

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

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 error code, 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_cursor means 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.