API Design¶
The API exposes the domain through the api-gateway. Public clients never call service
APIs or databases directly.
Each service carries a machine-readable version of its part of this contract in
services/<name>/openapi.yaml; this page remains the system-wide source they are
derived from.
1. Conventions¶
- Base path:
/api/v1 - JSON request and response bodies
- Consumer authentication: bearer token when signed in; anonymous scans use a stable Visitor ID managed by the client and gateway
- Brand scope comes from the authenticated Brand User; public requests never supply a
trusted
brandId - Error body:
{ "code": "...", "message": "...", "correlationId": "..." }
Identity headers (gateway → services)¶
Services never see raw tokens. The gateway validates the bearer token against the Keycloak realm's JWKS (ADR-011) and forwards the result as trusted headers:
- It always strips any inbound
X-Account-Id, then injects it from the token'ssubonly after successful validation — a client can never assert an account itself. - A present-but-invalid token (bad signature, expired, wrong issuer) is rejected with
401, never silently downgraded to anonymous. /api/v1/me/*(the consumer-only surface) requires a valid token and returns401without one.POST /api/v1/scans,/api/v1/accountsand/api/v1/sessionsare public; a valid token on a scan is honoured (account-scoped) but not required.X-Visitor-Idcarries the ADR-004 pseudonymous identity; the server-set visitor cookie is authoritative over a client-sent header.
2. Public API¶
Scanning and passports¶
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/01/{gtin}/10/{lot}/21/{serial}?17={expiry} |
None | Resolver entry: a camera app opening the QR's Digital Link URL lands here; the gateway parses the path, creates the scan, and redirects the browser to the result page |
POST |
/api/v1/scans |
Optional | Record a scan and resolve its Product Passport |
GET |
/api/v1/scans/{scanId} |
Owner | Read the progressively assembled result |
POST |
/api/v1/products/{gtin}/requests |
None | Report an unknown product |
There are two ways into a scan: the in-app scanner decodes the GS1 Digital Link
client-side and calls POST /api/v1/scans; a camera app opens the URL itself and
arrives at the Resolver route, which performs the same scan creation server-side. Lot,
serial and expiry are optional in both.
A scan belongs to the Visitor ID or Account that created it. GET /scans/{scanId}
returns 404 to anyone else — the Verdict is health-derived and must never be readable
by a third party holding the scan id.
POST /api/v1/scans
{
"gtin": "08012345678901",
"lot": "LOT-42",
"serial": "ITEM-9",
"expiry": "2026-07-15"
}
{
"scanId": "scan_123",
"catalogEntry": {
"gtin": "08012345678901",
"name": "Example Product",
"brand": "Example Brand",
"digitalLabel": {}
},
"verdict": {
"grade": "careful",
"reasons": [],
"goalFit": "Fits your primary goal"
},
"journey": { "state": "pending" },
"eco": { "state": "pending" }
}
Anonymous responses omit verdict. GET /scans/{scanId} returns updated section states:
pending, loaded, or failed.
Account, consent, and profile¶
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/v1/accounts |
None | Create an Account and link the current Visitor ID |
POST |
/api/v1/sessions |
None | Sign in |
DELETE |
/api/v1/sessions/current |
Consumer | Sign out |
PUT |
/api/v1/me/consents/health-profile |
Consumer | Grant health-profile consent |
DELETE |
/api/v1/me/consents/health-profile |
Consumer | Revoke consent and erase the Health Profile |
GET |
/api/v1/me/health-profile |
Consumer | Read the Health Profile |
PUT |
/api/v1/me/health-profile |
Consumer | Create or replace the consent-gated Health Profile |
DELETE |
/api/v1/me/health-profile |
Consumer | Delete the Health Profile without deleting the Account |
DELETE |
/api/v1/me/account |
Consumer | Delete the Account and start the full erasure process |
Fridge¶
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/api/v1/me/fridge |
Consumer | Read Fridge items with derived Freshness |
POST |
/api/v1/me/fridge/items |
Consumer | Add a physical item from a scan |
PATCH |
/api/v1/me/fridge/items/{itemId} |
Consumer | Mark an item consumed or discarded |
DELETE |
/api/v1/me/fridge/items/{itemId} |
Consumer | Remove an item |
GET |
/api/v1/me/fridge/waste-summary |
Consumer | Read the monthly waste projection |
Adding an item uses { "scanId": "scan_123" }; the Fridge service obtains the trusted
item and passport snapshot through the internal Passport API.
Brand dashboard¶
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/api/v1/brand/products |
Brand User | List the authenticated Brand's products |
POST |
/api/v1/brand/products |
Brand User | Onboard a Product Catalog Entry |
GET |
/api/v1/brand/metrics/scans |
Brand User | Read aggregate scan counts and trends |
GET |
/api/v1/brand/metrics/engagement |
Brand User | Read aggregate save and account-conversion metrics |
The gateway derives Brand scope from the authenticated user. Brand endpoints cannot request another Brand's identifier or access consumer endpoints.
3. Internal Service APIs¶
Internal APIs are not exposed through the public gateway.
| Service | Internal endpoint | Used by |
|---|---|---|
passport-service |
GET /internal/v1/scans/{scanId}/item |
Fridge obtains trusted ScannedItem and passport snapshot |
personalization-service |
POST /internal/v1/verdicts |
Passport requests an optional Verdict using Account reference + Digital Label |
identity-service |
GET /internal/v1/accounts/{accountId}/consents/health-profile |
Personalization checks consent before creating a Health Profile |
Measurement, cross-context reactions, and erasure propagation use the domain events defined in Domain Events, not public REST endpoints.
4. Endpoint Ownership¶
| Public prefix | Gateway routes to |
|---|---|
/01/… (Resolver entry) |
passport-service |
/api/v1/scans, /api/v1/products |
passport-service |
/api/v1/accounts, /api/v1/sessions, /api/v1/me/consents, /api/v1/me/account |
identity-service |
/api/v1/me/health-profile |
personalization-service |
/api/v1/me/fridge |
fridge-service |
/api/v1/brand/products |
passport-service, with Brand scope enforced |
/api/v1/brand/metrics |
brand-analytics-service, with Brand scope enforced |