Cancel a pending invite (admin)
/api/invites/{id}/revokeSets revoked_at rather than deleting the row: a revoked invite's token must stop working (checked by both accept and resend), but the row staying around is the only record that an invite existed and was cancelled.
Same lookup as resend; only a still-pending invite in the caller's own company can be revoked; RLS proves the tenancy, and an already-accepted or already-revoked row reads as not found.
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_invites row id.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/invites/string/revoke"{ "ok": true}Resend a pending invite (admin) POST
Admin-triggered resend from the teammate roster; distinct from `POST /api/invites/resend`, which is public and token-keyed for someone acting on their own expired invite link. This one is id-keyed and admin-gated, for "they still haven't accepted, send it again." RLS's tenant-scoped select already proves the id belongs to the caller's own company, so an id from a different company is simply not found, the same reasoning `DELETE /api/members/{id}` relies on. An invite that has already been accepted or already revoked is likewise not found: this route only ever acts on a still-pending one. Issues a fresh token and a fresh expiry (the old link stops working the moment this succeeds) and re-sends the invite email.
Accept an invite POST
Called by the embedded auth step on `/invite/{token}` right after a session exists (fresh signup or sign-in), never before. `requireMember()` is deliberately NOT the gate here; it requires an existing `company_id` claim, which is exactly the thing this route is about to create. The token proves someone had access to the original email; matching it against the signed-in user's own address proves the person accepting right now is that same person; without it, a forwarded or leaked token could be redeemed by whoever is signed in when they click it. An account already carrying a `company_id` claim from a different organization is refused: moving an existing account between organizations is not supported yet, checked here against the account actually accepting rather than only at invite-send time against an email that may not have had one. On success, three writes happen in order: the `company_id` claim, then the `book_org_members` row with the invite's role, then the invite is marked accepted.