Manually adjust a loyalty member's points balance (admin)
/api/loyalty/members/{id}/adjustA 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.
Authorization
sessionCookie 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
Path Parameters
The book_loyalty_members.id to adjust.
uuidRequest Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X PATCH "https://example.com/api/loyalty/members/497f6eca-6276-4993-bfeb-53cbbbba6f08/adjust" \ -H "Content-Type: application/json" \ -d '{ "delta": 0 }'{ "ok": true, "member": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "pointsBalance": 0, "tier": "string", "status": "string" }, "delta": 0, "reason": "string"}Ask for free migration help (admin) POST
The button on the proactive support banner. Stamps `support_requested_at` on one of this org's data jobs and pages us (console line plus `SUPPORT_WEBHOOK_URL`, if set). Deliberately small: it does not open a ticket in another system, email the customer, or promise a response time. The durable record is the row, the superadmin queue reads it, and a human replies. Scoped with an explicit `.eq('company_id')` on top of the id, because this route holds a service-role client and so has to spell out the tenancy that RLS would otherwise enforce. A zero-row update is a PostgREST 204 rather than an error, so the returned row (not the absence of an error) is what tells a real request from one naming somebody else's job.
Issue an API key (admin) POST
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.