Skip to content

Invitations

Klaxon supports two invite shapes that share a single invitations table and handler. Both are admin-initiated and both land at /invite/<code> in the web UI; the difference is how they're constrained.

ShapeTargeted atmax_usesEmail lock
Email inviteone address you type inforced to 1yes — only that email can accept
Link invitenobody specificnull (unlimited) or an integerno — anyone with the URL can accept

Both shapes carry a pre-assigned role (and optionally a team). The invitee signs in with any IdP, then accepts.

Create an invitation

bash
# Email invite
curl -X POST https://api.klaxon.sh/api/admin/invitations \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "email":"new@example.com",
    "role":"member",
    "team_id":"optional-team-uuid"
  }'

# Link invite (max 5 uses over 7 days)
curl -X POST https://api.klaxon.sh/api/admin/invitations \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "role":"member",
    "max_uses":5,
    "expires_in_hours":168
  }'

The response includes invite_code (32 chars of URL-safe base64, ~192 bits of entropy) and invite_url (fully-qualified, ready to share).

Permission required: users.invite. Only an Owner may invite someone as Owner. The caller's role must include every permission they're granting — i.e. you can't invite a custom role with org.delete unless you yourself have it.

Look up an invitation (public)

bash
curl https://api.klaxon.sh/auth/invitations/$CODE

No auth required. Returns an InvitationPreview with org_name, role, team_name (if any), inviter_name, and email if the invite is email-locked. Missing / revoked / expired / fully-used invites all return 404 — the server doesn't leak which it is.

Accept an invitation

bash
curl -X POST https://api.klaxon.sh/auth/invitations/accept \
  -H "Authorization: Bearer $USER_TOKEN" \
  -d '{"invite_code":"…"}'

The caller must be authenticated. On success:

  1. Creates an org_memberships row with the invited role.
  2. If the invite carried a team_id and that team still exists, creates a team_memberships row.
  3. Bumps use_count; email invites (max_uses = 1) are now exhausted.
  4. Mints a fresh 7-day session token scoped to the newly joined org and returns it.

Return shape:

json
{ "org_id": "…", "user_id": "…", "role": "member", "token": "…" }

The UI uses the returned token directly so the user lands inside the new workspace without a switch-org round trip.

Error responses

StatusErrorWhen
403invitation is for a different emailEmail-lock mismatch
410invitation revokedAdmin hit revoke
410invitation expiredexpires_at <= now()
410invitation fully useduse_count >= max_uses
402plan_limit_exceeded (key max_seats)Accept would exceed the plan's seat count
404invitation not foundBad code (or any of the above surfaced via preview)

The plan gate fires inside the same transaction that locks the invitation row, so two parallel accepts can't both sneak past the seat limit.

Revoke an invitation

bash
curl -X POST https://api.klaxon.sh/api/admin/invitations/$INVITE_ID/revoke \
  -H "Authorization: Bearer $TOKEN"

Sets revoked_at to now(). Subsequent previews 404; accepts return 410.

Email delivery (Resend)

Email invitations are sent via Resend.

Env varRequiredPurpose
RESEND_API_KEYyes, for email deliveryResend API key
KLAXON_INVITE_FROM_ADDRESSdefault invites@klaxon.shFrom: address

If RESEND_API_KEY is unset or the API call fails, the request still succeeds — the invite row is created and the admin can recover the URL from the POST response or via GET /api/admin/invitations. A tracing::error! logs the send failure so you'll see it in the collector.

The From address must have its domain verified in the Resend dashboard.

List open invitations

bash
curl https://api.klaxon.sh/api/admin/invitations \
  -H "Authorization: Bearer $TOKEN"

Returns active invitations only — revoked, expired, and exhausted rows are filtered out. Use this for the admin-panel "Pending invitations" view.

Web UI flow

  1. Admin clicks Invite in the admin panel, fills the form (email or link, role, optional team, optional expiry).
  2. Server creates the row and — if email — sends the Resend mail.
  3. Invitee clicks the link → lands at /invite/<code> → sees the preview, signs in via their preferred IdP (or magic link).
  4. POST /auth/invitations/accept runs automatically once authenticated.
  5. The UI swaps to the new token and navigates into the joined org.