Skip to content

Changelog

Every change to the API that a caller can observe is recorded here, newest first, in the Keep a Changelog format. If something you rely on changes, this is the page it will be announced on.

The version lives in the path. Today every endpoint sits under /v1/, and the OpenAPI document reports its own version in info.version — fetch https://api.scanfood.co/ext/v1/openapi.json if you want to check programmatically which contract you are working against.

Changes that can arrive in /v1/ at any time:

  • a new endpoint;
  • a new optional query parameter;
  • a new field in a response object;
  • a new error code for a situation that previously returned a more general one.

These are additive: an integration that ignores what it does not recognise keeps working. That is also the requirement they place on you — parse leniently. A client that rejects an unknown field will break on a release that breaks nobody else.

Changes that would not arrive in /v1/ in place: removing a field, renaming one, narrowing what an endpoint accepts, or changing the meaning of a value already in use. The version sits in the path precisely so that a contract this different arrives as a new path rather than underneath an existing integration.

A breaking change is announced at least 30 days in advance, on this page and through your ScanFood contact, before the new path goes live. The existing /v1/ path is not changed underneath you during that period.

What you can rely on:

  • Breaking changes are not made in place under /v1/.
  • Anything observable that does change is recorded here with a date.
  • Your key keeps working across releases — there is no version pinned to a key, and nothing expires on its own.

Added

  • categories on every shop returned by GET /v1/shops — the branch’s product categories as a flat list of { id, name, level, parentId, hidden }. The id is the same value that appears in a bill line’s items[].category, so category ids on bills can now be turned into names and full paths. Every category is listed, including ones the shop has hidden (hidden: true), because older bills can still refer to them. An id on a bill that is not in the list belongs to a category the shop has deleted; its name cannot be recovered. See Shops.

Nothing was removed, renamed or narrowed in this release. The category list is the only catalogue data exposed; products, menus, stock, costs and recipes are still not available through any endpoint.

Fixed

  • rankLevel on members is now always a whole number or null, as the reference has always declared. It was previously passed through as stored, so most members came back with a string — usually "", sometimes a number in quotes such as "11". A number in quotes now arrives as that number ("11" → 11); an empty string, or a value that is not a valid tier position (such as "01"), arrives as null. If your code already treated "" as “no tier”, it keeps working; if it parsed the string, it can now read the number directly.
  • rank on members is now always a string or null. A handful of old member records held a number there; those now arrive as null.

Documentation

  • items[].category on bills is documented as what it has always been: an array of category ids, from the top-level category down to the most specific one (usually a single id; [] when the product had no category). The reference and examples previously described it as a category name. The data itself has not changed.

Added

  • GET /v1/members/lookup — the member token behind a LINE user id you already hold. Give it a providerId and a lineUserId, and it answers with that person’s memberId — the same token that appears on bills and in member listings — so a chat bot can go from “someone just messaged us” to their tier, points and purchase history. The exchange runs one way only: an id goes in, a token comes out, and no response carries a LINE user id back. See Members.
  • A per-key permission for that lookup, allowLineLookup, off by default and granted the same way tel and email are. It gates only the new endpoint; nothing about existing responses changes when it is on or off.
  • 403 lookup_not_enabled — the answer when a key without that permission calls the lookup.

Nothing was removed, renamed or narrowed in this release. An integration that does not use the new endpoint needs no change.

Fixed

  • Take-away bills now report channel.code as "take_away". They were previously reported as "dine_in", with channel.name showing the shop’s price-tier name instead of a channel name, so take-away sales were counted as eat-in. Eat-in, pick-up and shop-defined channels are unchanged. If you have already stored bills from this API, take-away sales inside them are on the dine_in line and can be moved by re-pulling that date range.

Added

  • GET /v1/usage — where your key stands on its quota, read straight from the counters that enforce it: the per-minute and per-day ceilings the key actually runs under, how many requests it has spent today and how many are left, and the last seven Thailand-time days. It takes no parameters and reports only on the key that calls it. See Rate limits.

Changed

  • Usage records are no longer kept indefinitely. The record of an individual request is kept for 90 days, and the daily totals that /v1/usage reads are kept for 400 days — long enough to compare a month against the same month last year. Nothing else about a key or its data is affected.

First public release.

Added

  • GET /v1/shops — the shops a key can see, with shopId, shopName, franchiseId, providerId and the shop’s timezone. Not paginated.
  • GET /v1/transactions — bills for one shop, in two modes: a calendar range (from/to, up to 31 days per request) or an incremental pull (updatedSince). Cursor paging with limit up to 200, default 100.
  • GET /v1/transactions/{id} — one bill in full: line items, amounts, payments, channel, cashier and void status.
  • businessDate on every bill, alongside billDate, so shops that trade past midnight can be reconciled on their own trading day.
  • GET /v1/members — loyalty members for one brand (providerId), with points, credit, rank, coupons, labels and visit history. Cursor paging.
  • GET /v1/members/{memberToken} — one member, addressed by the opaque token returned in listings.
  • GET /v1/openapi.json — the full OpenAPI 3.1 document, public and unauthenticated.
  • API keys in the form sf_live_<keyId>_<secret>, sent as Authorization: Bearer …, each scoped to a single shop or to a whole franchise, each with its own label, per-minute limit and daily quota.
  • Rate limiting and quota reporting: X-RateLimit-Limit and X-RateLimit-Remaining for the daily quota, Retry-After on a per-minute 429, and a daily counter that resets at 00:00 Thailand time.
  • Key revocation, effective within five minutes, after which the key answers 401 key_revoked.
  • api.scanfood.co as the single host for the API and for this documentation.
  • This documentation site, including machine-readable copies of every guide.

Known limitations at 1.0.0

  • updatedSince is not available on /v1/members; it returns 400 not_supported. Members are synchronised in full, page by page.
  • Bills closed before change tracking existed carry no updatedAt and therefore never appear in updatedSince results — backfill with from/to first.
  • No webhooks and no push: everything is pulled on your schedule.
  • No sandbox and no test dataset — see Test data.
  • Cross-origin requests are refused; the API is server-to-server only.
  • Products, menus, stock, costs and recipes are not exposed by any endpoint. (Since 1.3.0, the shop’s category list is available on /v1/shops; the rest of this still holds.)