Skip to content

Authentication

Every endpoint except GET /v1/openapi.json requires an API key. One key carries both your identity and your permissions: what it may read is fixed when it is issued and can never be widened by anything you put in a request.

Send the key as a bearer token:

Authorization: Bearer sf_live_XXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Terminal window
curl -s https://api.scanfood.co/ext/v1/shops \
-H "Authorization: Bearer $SCANFOOD_API_KEY"

The header is the only accepted channel. Keys in a query string or a request body are ignored and the call is answered as if no key had been sent — query strings end up in the access logs of every intermediary, and bodies end up in tracing systems.

A key has three parts:

Part Example Notes
Prefix sf_live_ Production keys only.
Key id 12 hexadecimal characters Identifies the key; safe to log.
Secret 32 hexadecimal characters Never log it. ScanFood keeps only a hash.

Scope is decided when the key is issued, and every request is checked against it. Nothing in a request can change it.

Scope Bound to Which shops it reads Which member brands it reads
shop One shop That shop only The member brand attached to that shop
franchise One franchise Every branch of that franchise Every member brand used by those branches

The practical consequences:

  • GET /v1/shops takes no parameters. It returns the branches the key may read, which makes it the fastest way to see what a key is allowed to do. A branch that has since been deleted drops out of this list while staying inside the key’s scope, so read the list as “what is there to read”, not as the literal definition of the scope.
  • Passing a shopId outside the scope returns 403 shop_not_in_scope, and a providerId outside the scope returns 403 provider_not_in_scope. The list endpoints answer with 403 rather than 404 because the id you sent is one you already knew.
  • Asking for a single transaction or member outside the scope returns 404 not_found instead — we do not confirm that an id exists in a shop you cannot read.

A second flag, set per key, decides whether member phone numbers and e-mail addresses are returned. It is off by default; without it you still get hasTel and hasEmail booleans, which are enough to know whether a member can be contacted at all.

Keys do not expire on their own, so rotation is something you schedule, not something the API forces on you. Because the key is only ever shown once, plan the change before you make it:

  1. Ask ScanFood for a second key with the same scope.
  2. Deploy it to your systems and confirm traffic is flowing on the new key.
  3. Ask for the old key to be revoked.

Store the key where your deployment already keeps secrets. Do not commit it, do not paste it into a ticket, and do not e-mail it. If it does leak, treat that as a rotation: issue a new key, deploy, revoke the old one.

Revocation is permanent — a revoked key can never be re-enabled, and a new one must be issued instead. Revoking a key that is already revoked is harmless.

Requests made with a revoked key are answered 401 key_revoked. That is a different code from invalid_key on purpose: only the holder of a correct secret can ever see it, so it tells you “this key was retired” rather than sending you off to hunt for a typo.

Status error When it happens What to do
401 invalid_key No header, a malformed header, a key id that does not exist, or a wrong secret — all four give the same answer Check that the header is exactly Authorization: Bearer sf_live_…, with no extra whitespace and no quotes
401 key_revoked The key was retired Move to the replacement key
503 auth_unavailable ScanFood could not verify the key right now (Authentication service temporarily unavailable, retry later). This is our side, not yours Retry with backoff; do not touch your key

401 responses carry no quota headers, because the key is checked before the quota is. See Rate limits for what the headers mean when they do appear, and Errors for every other code.