Skip to content

Rate limits

Two limits protect the platform, and they are counted separately: a burst limit per minute and a quota per day. Both are counted per key, so keys issued to different systems never eat each other’s budget.

Limit Default Window Applies to
Burst 60 requests per minute Rolling 60 seconds Each key
Daily quota 10,000 requests per day Calendar day in Thailand time, resetting at 00:00 ICT Each key

These are the defaults every key starts with. Either number can be raised or lowered for an individual key when it is issued. The daily quota that actually applies to your key is returned on every successful request in X-RateLimit-Limit — read it there instead of hard-coding it — and the per-minute ceiling is reported as limit in the body of a 429 rate_limited response.

Two details that decide whether your budget is enough:

  • Requests are counted as they arrive, not as they succeed. A run of failing calls still spends the quota, so fix a 400 loop rather than letting it run.
  • The daily counter resets at midnight Thailand time (UTC+7), not at midnight in your own zone. A job that starts at 23:00 in Europe is already running in tomorrow’s quota.
Header On which responses Value
X-RateLimit-Limit Every request that gets past the key check and the burst limit Your key’s daily quota
X-RateLimit-Remaining Same Requests left in today’s quota after this one
Retry-After Only on 429 rate_limited Seconds to wait before retrying

The headers above describe the request you just made, and only reach you on the requests that carry them. GET /v1/usage answers the same question outright, whenever you ask: the ceilings your key actually runs under, what it has spent today, and the six days before that.

The endpoint takes no parameters. It reports on the key in the Authorization header and on nothing else — there is no way to ask about another key.

Terminal window
curl -s -H "Authorization: Bearer $SCANFOOD_API_KEY" \
"https://api.scanfood.co/ext/v1/usage"
{
"ok": true,
"data": {
"keyId": "9f3a1c7d5b2e",
"label": "Nightly warehouse sync",
"scope": "shop",
"rate": { "perMin": 60, "perDay": 10000 },
"today": { "date": "20260910", "count": 127, "remaining": 9873 },
"days": [
{ "date": "20260904", "count": 96 },
{ "date": "20260905", "count": 101 },
{ "date": "20260906", "count": 0 },
{ "date": "20260907", "count": 88 },
{ "date": "20260908", "count": 143 },
{ "date": "20260909", "count": 132 },
{ "date": "20260910", "count": 127 }
]
},
"serverTime": "2026-09-10T06:45:12.310Z"
}
Field Meaning
keyId The key you authenticated with — the part of the key before the secret
label The label recorded when the key was issued, or an empty string
scope shop for a single branch, franchise for every branch under one franchise
rate.perMin The per-minute ceiling this key runs under
rate.perDay The daily quota this key runs under — the same number as X-RateLimit-Limit
today.date Today in Thailand time, YYYYMMDD
today.count Requests counted today, including this one
today.remaining perDay minus count, never below zero
days[] The last seven Thailand days, oldest first and today last, each with date and count

Three things worth knowing before you build on the numbers:

  • today.count includes the call you just made. A request is counted as it arrives, so today.remaining is exactly the X-RateLimit-Remaining header on that same response — and asking for your usage spends one request of the quota like any other call.
  • days always holds seven entries. A day with no traffic comes back as 0 rather than being left out, so a chart or a report needs no gap filling.
  • This is the one place that reports perMin without tripping it. No header carries the per-minute ceiling; otherwise you only learn it from the limit field of a 429 rate_limited body.

Two different situations share the 429 status, and they need different reactions. Branch on the error field:

error Meaning What to do
rate_limited Too many requests in the last minute Wait Retry-After seconds and retry. The body also carries retryAfterSec, limit and windowSec
quota_exceeded The daily quota is gone Stop for the day, or ask for a larger quota. Retrying will not help before 00:00 ICT
{
"ok": false,
"error": "rate_limited",
"message": "Too many requests, retry in about 37 seconds",
"retryAfterSec": 37,
"limit": 60,
"windowSec": 60
}

A retry helper that honours both:

Terminal window
# --retry only understands the header, so let curl wait for you.
curl -s --retry 5 --retry-delay 5 --retry-all-errors \
-H "Authorization: Bearer $SCANFOOD_API_KEY" \
"https://api.scanfood.co/ext/v1/shops"

The limits are generous for a sync, and tight for a crawl. Some arithmetic before you write the scheduler:

  • Ask for full pages. limit=200 is the maximum; a day with 600 bills is 4 requests at 200 and 7 at 100 — 3 (or 6) full pages, plus one closing call, because a page that comes back exactly full always needs one more request to see nextCursor: null.
  • One shop per request. A franchise of 20 branches polled every 15 minutes is 20 × 4 × 24 = 1,920 requests a day before paging — comfortable inside 10,000, but worth doing the sum for larger groups.
  • Poll incrementally. updatedSince returns only what changed since your last run, so a quiet quarter of an hour costs one request per shop.
  • Backfill deliberately. History is fetched in windows of at most 31 days; a year per shop is at least 12 requests plus paging. Run it once, off-peak, not on every deploy.
  • Spread the branches out. Twenty shops fired at the same second is a burst; the same twenty spread across the minute is not.

If a legitimate workload does not fit, ask for a higher quota rather than splitting one system across several keys — separate keys make usage impossible to read back.