Skip to main content

Invitations

Add users to an organization through email invites.

Flow

[admin / owner] [invitee]
│ │
│ POST /v1/orgs/:org_id/invitations │
│ { email, role } │
│ ──────────────────────────────────► │
│ │
│ email with link │
│ {public_base_url}/ │
│ accept-invite?token=... │
│ │
│ │ click link
│ │
│ │ GET /v1/auth/invitations/preview?token=...
│ │ → { organization_name, role, ... }
│ │
│ │ POST /v1/auth/invitations/accept
│ │ { token, password?, display_name? }
│ │ → session issued

Roles you can invite

admin, billing, member, viewer. owner is intentionally NOT grantable through invitations — use the ownership-transfer flow for that.

Token storage

Raw token (32-byte hex) is delivered exactly once via email. The database stores only SHA-256(token).

Lifecycle

StateMeaning
pendingLive; can be accepted.
acceptedConsumed.
revokedManually revoked via DELETE.
expiredPast expires_at (set by application code, not enforced by DB).

TTL: 7 days.

Re-sending

POST /v1/orgs/:org_id/invitations/:id/resend mints a fresh token (overwriting the prior hash), resets the expiry, and re-emails. The old token is dead immediately.

Accepting

The accept endpoint has two modes:

  • New user: token + password (+ optional display_name). Creates the user with email_verified: true, adds the membership, returns a session.
  • Already signed in: token only. Adds membership in the invited org, returns 204. The caller's existing session continues.

If the caller is signed in but their email doesn't match the invite, 403.

Public routes

GET /v1/auth/invitations/preview and POST /v1/auth/invitations/accept must be mounted OUTSIDE the auth middleware. The SDK exports them on a separate router (routes::invitations::router_public()) for exactly this reason.

Audit

  • members.invited
  • members.invitation_revoked
  • members.invitation_resent
  • members.invitation_accepted

Limits

  • No "max pending invites per org" cap in v0.1. Hosts that want one should add their own pre-check before calling create.
  • No throttling on resend. Same — add your own rate-limit if needed.