Request a new invite link (public)
/api/invites/resendThe 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.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/invites/resend" \ -H "Content-Type: application/json" \ -d '{ "token": "string" }'{ "ok": true, "email": "user@example.com"}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.
Request a new invite link for the signed-in account (session-gated) POST
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.