/api/scan
The staff scanner app — 9 routes, any role.
Session-authenticated from a paired device. Unlike the dashboard, every role is admitted: in a
one-person shop the owner is the barista. /api/scan/pair and /api/scan/device-login are the
two ways in and are themselves unguarded.
If a device is unpaired while a session is still warm, the next call is a 401 (not 403) with
this device has been unpaired; scan a QR from the dashboard — so the app wipes its stored token
and re-pairs rather than showing a permissions error.
Pairing
| Method | Path | |
|---|---|---|
POST | /api/scan/pair | Exchange the dashboard's QR for a durable device token |
POST | /api/scan/device-login | Open a session with a stored device token |
POST | /api/scan/logout | 204 |
GET | /api/scan/me | The signed-in staff member, role and current branch |
GET | /api/scan/branches | The merchant's open branches |
PUT | /api/scan/branch | Switch the device's branch |
At the till
| Method | Path | |
|---|---|---|
GET | /api/scan/passes/{serialOrCode} | Look up a pass. Read-only — writes no ledger row |
POST | /api/scan/stamp | Earn. Takes idempotencyKey, deviceId, count, optional amount |
POST | /api/scan/redeem | Claim, spend, or admit — the program type decides which |
{serialOrCode} accepts either the UUID from the QR or the short typeable code printed under it.
The short code is a 40-bit prefix of the serial, so two passes can in principle share one: that
answers 409 two cards start with this code rather than guessing.
Both writes require an idempotencyKey you generate — a double tap awards one stamp.
What redeem means
| Program type | |
|---|---|
STAMPS | The card restarts: cycle advances, balance returns to zero |
POINTS | The balance draws down by the amount applied. Partial is normal |
COUPON | One use is spent; the last one leaves the pass terminally REDEEMED |
TICKET | The holder is admitted. The first scan is terminal — no cooldown, and a second scan is a loud 409 |
POST /api/scan/stamp against a ticket is a 400: a ticket is admitted at the door, not stamped. A ticket scanned past its derived end is a 409 naming when the event ended.