API keys

Issue an API key (admin)

post/api/keys

Mints a key for a programmatic caller. The raw secret is returned in this response and nowhere else, ever: only its SHA-256 digest is stored. There is no way to recover it afterwards; the only remedy for a lost key is to revoke it and issue another.

Requested scopes are validated against this org's own vertical, which is where "a key can never exceed what the org itself can do" is enforced: a hospitality org asking for services:write is a 400, not a key quietly holding a scope no route would ever honour for it. The route itself takes no vertical argument, because key management is shared by both engines; it is the scope list that is filtered, not the endpoint that is gated.

There is no GET /api/keys. The /developer dashboard page lists keys through a server component reading the table with service-role after requireMember() has established tenancy.

Authorization

sessionCookie
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.

Every request re-validates it against the Auth server (getUser()), never by decoding the cookie locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified app_metadata.company_id claim and enforced by row-level security; it is never read from request input, on any route, ever.

role (admin / staff) is deliberately not in RLS. It gates specific actions in route code, the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's own check is the enforcement.

In: cookie

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/api/keys" \  -H "Content-Type: application/json" \  -d '{    "name": "string",    "scopes": [      "string"    ]  }'
{  "ok": true,  "key": {    "id": "string",    "name": "string",    "scopes": [      "string"    ],    "last_four": "string",    "created_at": "2019-08-24T14:15:22Z",    "expires_at": "2019-08-24T14:15:22Z",    "revoked_at": "2019-08-24T14:15:22Z",    "rate_limit_per_minute": 0  },  "secret": "string"}

Manually adjust a loyalty member's points balance (admin) PATCH

A birthday bonus, a correction, a chargeback clawback: the ONE trigger the loyalty wallet-card push engine has today (migration 0161). There is no automatic points-accrual formula for completed bookings anywhere in this product yet, so nothing else calls this. `delta` is a signed, non-zero integer added to `points_balance` via `book_loyalty_adjust_points`, a `security definer` RPC rather than a plain PostgREST update: PostgREST cannot express `points_balance = points_balance + delta` atomically, and a read-then-write from this route would race under concurrent adjustments. The RPC leans on `book_loyalty_members.points_balance >= 0` (migration 0160) as the single source of truth for the floor, so a delta that would take the balance negative is rejected by the database, not by application logic re-implementing the same rule. Every adjustment is recorded in `book_loyalty_point_adjustments` (company, member, delta, an optional `reason`, and who made it) for a durable audit trail: redeemable points are worth being able to explain. **No `Idempotency-Key`, unlike `/api/payments/refunds`.** That machinery exists there because a retry calls an external payment processor that could double-charge with no way to detect the duplicate. A double-submitted delta has no such blast radius: it is one UPDATE against this database, its result is returned in this same response, and it is trivially correctable with the opposite delta. On success, enqueues `loyalty_pass_push` so both wallets pick up the new balance: Apple via a silent APNs wake-up that makes the device re-fetch its pass, Google by re-patching the `LoyaltyObject` directly. That enqueue is best-effort and never fails this request: a queue hiccup must not turn a successful points adjustment into a client-visible error. Not engine-gated: a loyalty member hangs off `book_customers`, a table both engines already share, and carries no engine column at all.

Revoke an API key (admin) DELETE

A soft delete: `revoked_at` is stamped and the row stays, so the record of what existed, who issued it and what it could reach survives revoking it. That matters precisely when a key is being revoked in a hurry because it leaked, which is the worst possible moment to discard the evidence. Revocation is immediate, with no cache to wait out; the gate reads `revoked_at` on every request.