Authentication
There is no single “API key”. Each credential class grants a bounded set of capabilities and nothing else, and the server derives the tenant and the acting user from the credential — never from the request body.
| Credential | How it is sent | Who holds it | What it can do |
|---|---|---|---|
Publishable key pk_… |
x-api-key + x-tenant-id |
Storefronts, browser bundles | Browse offerings, read availability, create a booking intent, make a customer-present purchase |
Operator API key sk_op_… |
x-api-key + x-tenant-id |
Back-office integrations, scripts, agents | Whatever scopes it was minted with, intersected with the live membership of the staff member who minted it |
| Staff session (JWT) | Authorization: Bearer … |
The operator app, the console, a person at a keyboard | Everything that member’s role allows, including minting operator keys (Admin/Owner) |
| Customer session (JWT) | Authorization: Bearer … |
The customer portal | /account/* only — a customer’s own bookings, quotes and readiness. Carries zero operator authority |
Content keys pk_… / sk_… |
x-api-key |
A tenant’s website build / CMS tooling | pk_ reads published content and media metadata; sk_ writes content types, entries, publications and media |
| Capability token | In the URL (/g/{token}, /b/{token}, /p/{token}, /w/{token}) |
A guest who received a link | Exactly the one action the link was issued for |
Publishable keys
Section titled “Publishable keys”A publishable key is safe to embed in a browser. Pair it with the tenant id on every request:
x-api-key: pk_live_…x-tenant-id: 00000000-0000-0000-0000-000000000001It is verified against that tenant; a key presented with another tenant’s id is rejected 401 invalid_key. Missing both headers is 401 unauthenticated. Operator-scoped routes do not accept a publishable key at all (403), so a leaked storefront key cannot read bookings or customers.
Operator API keys
Section titled “Operator API keys”An operator key is minted by an Admin or Owner from a staff session and shown once:
curl -s "$QS_BASE/operator-keys" \ -H "Authorization: Bearer $STAFF_SESSION" \ -H "content-type: application/json" \ -d '{ "label": "nightly-reconciliation", "scopes": ["bookings.read", "manifest.read"] }'The response carries the raw key (sk_op_…) and its operator_key_id; only a digest is stored. Use it like a publishable key, with the tenant id:
x-api-key: sk_op_…x-tenant-id: 00000000-0000-0000-0000-000000000001Two rules shape what a key can do:
- Scopes are an upper bound. The key’s effective authority is its scopes ∩ the current membership of the member who minted it. If that member is removed, the key stops working.
- Some things need a human. Minting keys, administering the tenant, and writes that carry permission gates (for example
commerce.price_override) require a staff session, not a key.
Revoke with POST /operator-keys/{key_id}/revocation. Revocation is immediate and idempotent.
Scopes a key can be minted with: bookings.read, manifest.read, participants.read, capacity.read, conditions.read (reads) and bookings.write, participants.write, proposals.write, conditions.write, policies.write (writes; underwritten only by an Admin/Owner membership). Unknown scopes are refused 422.
Staff sessions (magic link)
Section titled “Staff sessions (magic link)”Quikstik has no passwords. A staff member signs in by email:
POST /login/requestswithx-tenant-idand{"email": "…"}. Always202, whether or not the email belongs to an account — existence is never disclosed.- The member receives a single-use link valid for a few minutes.
POST /login/sessionswith{"token": "…"}→201 {"session_token": …, "scopes": […], "role": …}.
Sessions are short-lived JWTs. Send one as Authorization: Bearer …. An expired session is the one failure the server names (401 session_expired) so a client knows to re-authenticate; every other session failure is 401 invalid_session.
Device sessions
Section titled “Device sessions”A client that must stay signed in (the operator app) passes a device object on POST /login/sessions. The response then also includes a 90-day refresh_token, shown once. POST /login/refresh trades it for a fresh session JWT; the refresh token is the credential, so that call is unauthenticated by design and rate-limited. GET /login/devices lists the caller’s devices and POST /login/devices/{device_id}/revoke stops a device from refreshing — a JWT already minted lives out its short TTL.
GET /me returns who the session is and the scopes and role the membership authoriser grants it now.
Customer sessions
Section titled “Customer sessions”Customers use the same magic-link flow at /customer-login/requests and /customer-login/sessions. The resulting session’s subject is a person, not a staff member, and it is accepted only on /account/*. A customer session at a staff route, or a staff session at a customer route, fails as if the route did not exist.
Content keys
Section titled “Content keys”Content endpoints under /merchants/{merchant_slug}/… take a tenant-scoped key in x-api-key and resolve the tenant from the merchant slug in the path — no x-tenant-id header. pk_ keys read; sk_ keys write. The two are distinct credential classes behind distinct verifiers: an sk_ key is refused on a read (401), and vice versa. See Content.
Capability tokens
Section titled “Capability tokens”Guest links put the authorisation in the URL. GET /w/{token} (readiness / waiver), /g/{token} (group share), /b/{token} (balance), /p/{token} (party / riders) each resolve to exactly one booking and one action; the token is stored as a digest, and an unknown, expired or consumed token is a 404 indistinguishable from a wrong one. No header is needed. See Booking lifecycle → guest links.