Shared

Cancel a pending invite (admin)

post/api/invites/{id}/revoke

Sets 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
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

Path Parameters

id*string

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.