Skip to content

Authentication

Every request to /api/payments/v1/ except enrollment carries a device credential:

Authorization: Bearer sit_pd_<hex>

A device is registered by the restaurant owner in Sitora Biz, then armed with a one-time enrollment code. The code lives for one hour and is good for one claim. Claiming it is the one anonymous operation on this surface — the code is the secret, so the endpoint is IP-throttled and unknown, spent, and expired codes all answer identically, with 400 and {"code": "enrollment_invalid"}.

The claim returns the credential exactly once. Sitora stores only a hash; there is no endpoint that returns your key again.

Issuing a fresh enrollment code is also the reset lever: it revokes every live credential for that device first, so an old key and a claimable code are never valid at the same time.

A device credential is not a user account. It has no membership, no role, and no staff permissions. It can never author an order, a transaction, or a session, and it never appears as the actor on anything.

Staff working at a device — a cashier on a smart POS — authenticate as themselves with their normal Sitora Pro session. One request is always exactly one principal: the human acts ride the staff session, the device plane rides the device credential. Do not attempt to use one for the other.

The credential resolves to exactly one device, which belongs to exactly one station in exactly one filial. You never send a filial or station identifier — Sitora derives them from the key. A credential for one till cannot see another’s work, even inside the same restaurant.

Scope Granted when the device declares Grants
device:heartbeat always Heartbeat and self snapshot
device:display qr_dynamic_present or customer_total_display The display queue
intents:read_assigned payment_app_handoff Reading the assignment
intents:report_handoff payment_app_handoff Reporting an outcome

Because scopes are derived from capabilities, changing a device’s capabilities means re-enrolling it — which is why issuing a code revokes the old key.

Authorization is not decided once at enrollment. Every request re-checks that the platform is not frozen, the device is active, its station is active, and the filial is active. A pause therefore takes effect on the very next call, mid-shift, with no cache to wait out.

A refusal names its reason:

Reason Meaning
platform_frozen Provider-driven collection is frozen platform-wide (displays only)
device_paused / device_retired The owner paused or retired this device
device_not_enrolled The device is awaiting a fresh enrollment claim
station_retired The station this device serves was retired
filial_inactive The branch is not active

The 403 itself is not machine-readable: its body is {"detail": "Device credential refused: station_retired."}, with the reason inside the sentence. To branch on the reason, read it as its own field from the device’s own snapshot:

GET /api/payments/v1/devices/me/

That endpoint answers with the same snapshot the heartbeat returns — the device’s name, kind, capabilities, status, its station’s name and status, its configured handoff target — plus blocked_reason, which carries the code from the table above, or an empty string when the device stands. It needs only device:heartbeat, so every enrolled device can ask.

Handle 401 by escalating for a new enrollment code, not by retrying: a revoked key never becomes valid again. Handle 403 by displaying the reason and continuing to heartbeat — standing can come back without any action from you.