Shared

Request a new invite link for the signed-in account (session-gated)

post/api/invites/resend-mine

The onboarding page's "you already have an invitation" dialog (PendingInviteDialog), for someone who signed up fresh instead of using their invite email. Session-gated rather than token-keyed, unlike POST /api/invites/resend: this caller never held a token to begin with (migration 0142 stopped book_invites from storing one anyone but the emailed recipient could ever reconstruct), only a live session proving their own email address. Looks up the pending invite by the CALLER'S OWN verified email, then does exactly what the token-keyed resend does: mint a fresh token, rotate it in, email it. Not gated by requireMember(): the caller has no company_id claim yet, which is exactly what accepting the invite is about to create.

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

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/api/invites/resend-mine"
{  "ok": true,  "email": "user@example.com"}

Request a new invite link (public) POST

The self-service "send me a new link" button on an expired `/invite/{token}` page, no session required. Deliberately keyed on the TOKEN rather than an email address: unlike an email-lookup endpoint, which would let anyone probe arbitrary addresses for "does this person have a pending invite, and where", possessing a specific 256-bit token already proves access to the original message, so nothing new is disclosed by honouring it. That is also why this can return specific errors instead of a deliberately generic response; "invite not found" leaks nothing an attacker didn't already need to have. Issues a fresh token and expiry and re-sends the invite email to the address on file, which is echoed back so the page can say where it went.

Create an invited account (public) POST

The "create account and join" half of the `/invite/{token}` form, no session required, because the visitor is about to establish their first one. Creates the account with its email already confirmed, then the browser signs in with the same password and goes on to POST `/api/invites/accept`. Skipping the confirmation email is the reason this route exists rather than a shortcut taken around one: holding the token means having opened the message sent to the invite's address, so it is already proven, on the same bearer-capability reasoning that lets `/invite/{token}` be viewed with no auth and lets resend be honoured on a token alone. A second confirmation round-trip proves nothing the token did not. The address is read off the invite row and never off the body; a token is authority over exactly one address, and honouring a caller-supplied one would turn a leaked invite into a create-an-account-anywhere primitive. Replaced a browser-side `supabase.auth.signUp()`, which could not work here: with confirmations enabled GoTrue answers an already-registered address with a session-less 200 and no error at all (enumeration protection) and sends no mail, so the form's only remaining branch was to strand a returning invitee on "check your email" for a message that was never coming. The admin API does not obfuscate, which is what makes the 409 below possible.