Security best practices
An API key reads a shop’s sales history and its member list. It cannot change anything — every endpoint is read-only — so the risk it carries is disclosure, not damage. That still means the key deserves the same handling as a database password.
Two facts shape everything on this page:
- The key is shown once. We store only a one-way hash of it, so no one at ScanFood can read it back to you. Lost means replaced, not recovered.
- The key travels in one place only. We read it from
Authorization: Bearer <key>and nowhere else — not from a query string, not from a body.
Store the key on a server
Section titled “Store the key on a server”Keep the key in a secret manager, or in an environment variable injected at deploy time. Never in the repository, never in a file you commit, never pasted into a ticket or a chat thread.
# Read it into the shell without leaving it in your history…read -rs SCANFOOD_API_KEY && export SCANFOOD_API_KEY
curl -s -H "Authorization: Bearer $SCANFOOD_API_KEY" \ "https://api.scanfood.co/ext/v1/shops"
# …and never like this: the key lands in shell history, in access logs,# and in every proxy between you and us.# curl "https://api.scanfood.co/ext/v1/shops?key=sf_live_…" ← the API ignores it anyway// Read from the environment. Do not hard-code, do not ship to the client.const KEY = process.env.SCANFOOD_API_KEY;if (!KEY) throw new Error('SCANFOOD_API_KEY is not set');
const res = await fetch('https://api.scanfood.co/ext/v1/shops', { headers: { Authorization: `Bearer ${KEY}` },});
// Redact the key before anything is logged.const safe = (s) => String(s).replace(/sf_live_[a-z0-9]+_[a-f0-9]+/g, 'sf_live_***');console.log(safe(`GET /v1/shops -> ${res.status}`));import os, re, requests
KEY = os.environ.get("SCANFOOD_API_KEY")if not KEY: raise SystemExit("SCANFOOD_API_KEY is not set")
res = requests.get( "https://api.scanfood.co/ext/v1/shops", headers={"Authorization": f"Bearer {KEY}"}, timeout=30,)
REDACT = re.compile(r"sf_live_[a-z0-9]+_[a-f0-9]+")print(REDACT.sub("sf_live_***", f"GET /v1/shops -> {res.status_code}"))Also worth knowing:
- Traffic is HTTPS only. Do not disable certificate verification “just for testing” — that is exactly the request that ends up running in production.
- We never log the key. Neither the full key nor its hash appears in our
usage records. What we do keep is the
keyId— the middle segment, safe to quote in a support conversation.
One key per integration
Section titled “One key per integration”Ask for a separate key for every system that calls us: the nightly accounting sync, the dashboard, the analyst’s notebook, the vendor doing a proof of concept.
| Why | What it buys you |
|---|---|
| Revocation is surgical | Turning off the vendor’s access does not stop your accounting sync at 2 a.m. |
| Usage is attributable | “Who sent 40,000 requests yesterday?” has one answer instead of a shrug. |
| Limits are independent | An experiment cannot spend the production key’s daily quota. |
| Blast radius is bounded | One leaked key exposes one integration’s scope, not everything you have. |
Every key carries a required label. Use it: accounting-nightly-sync,
bi-dashboard, vendor-poc-2026-09 — a name a colleague can act on a year from
now without asking anyone.
Least privilege
Section titled “Least privilege”Two dials decide how much a key can see. Ask for the smallest setting that does the job; you can always be issued a wider key later.
Scope — which shops. A key is issued either for one shop or for a whole franchise. A franchise key sees every branch under it, now and in the future, including branches that open after the key was issued. If the integration only touches two branches, a franchise key is not the right tool.
Personal data — phone numbers and e-mail. Member phone numbers and e-mail
addresses are not returned by default. An ordinary key sees hasTel and
hasEmail — true or false — which is enough to answer “is this member
reachable?” without moving personal data out of ScanFood. Keys that return the
real values exist, but they are issued deliberately, for a stated purpose, with
the data-protection obligations that come with it.
Also outside the API’s reach, by design: costs, recipes and ingredient data never leave through it, and no endpoint can write anything back into ScanFood.
Logging and monitoring
Section titled “Logging and monitoring”- Redact before you log. Log the path, the status, the row count and the
timing — never the
Authorizationheader. A helper like the one above, applied once at the boundary, is worth more than a rule people have to remember. - Log the
keyId, not the key. Insf_live_<keyId>_<secret>, thekeyIdis a public handle. Recording it with each run means a support question can be answered in minutes. - Watch for the shape of abuse. Requests you did not schedule, at hours you do not run, are the signal that a key has escaped. Your own logs will see this before anyone else does.
- Alert on
401. A working integration does not produce authentication errors. A burst of them means the key was revoked, rotated behind your back, or is being tried by someone who does not have the whole of it. - Watch the daily quota. An unexplained jump in usage is the cheapest leak
detector you own —
X-RateLimit-Remainingon every response is the number to track.
Rotating a key
Section titled “Rotating a key”Keys do not expire on their own, so rotation is something you schedule rather than something you are forced into. Because a key is only shown once, rotation is “issue new, then retire old” — there is no way to re-read the current one.
The order matters, and done in this order there is no downtime:
- Ask for a new key for the same integration and scope.
- Deploy it to your secret store and restart or reload the caller.
- Confirm the new key is the one working — its usage appears, the old one goes quiet.
- Ask us to revoke the old key. Both keys are valid until you do; there is no window where neither works.
After revocation the key answers 401 with "error": "key_revoked", and it
stays that way permanently. Revoking a key twice is harmless.
If a key leaks
Section titled “If a key leaks”A key in a public repository, a screenshot, a pasted log or a former colleague’s laptop is a leaked key. Treat it as leaked even if you are not sure.
- Tell us immediately and ask for that key to be revoked. Quote the
keyId— the middle segment of the key — not the whole key. - Issue a replacement and deploy it. If the integration must not stop, do the replacement first (see the rotation order above), then the revocation.
- Assume the window was used. Within the five-minute propagation, the key still worked. Ask us what that key read and when; our usage records are kept per key, with path, status and timestamp.
- Close the hole that let it out. A leaked key that goes back into the same commit history, the same chat channel or the same shared drive will leak again.
- Check what the scope exposed. A single-shop, no-personal-data key that leaked for five minutes is a very different conversation from a franchise key that returns phone numbers.
Because the API is read-only, a leaked key cannot void a bill, change a price or delete a member. What it can do is read — which is exactly why the scope you asked for at the beginning is the thing that limits the damage at the end.