Memberships and invitations API
Memberships and invitations are project-scoped. Roles are viewer, operator, admin. There is no owner role; admin is the highest tier and the last-admin protection kicks in on demotion or removal.
Memberships
Section titled “Memberships”List members
Section titled “List members”GET /api/v1/projects/{slug}/membershipsRole: viewer. Returns:
[ { "id": "...", "user_id": "...", "project_id": "...", "role": "operator", "created_at": "...", "updated_at": "..." }]Add membership
Section titled “Add membership”POST /api/v1/projects/{slug}/membershipsRole: admin. CSRF-protected. Body adds an existing user to the project (use the invitations flow below to add a user who has not signed up yet).
Change role
Section titled “Change role”PATCH /api/v1/projects/{slug}/memberships/{membership_id}Role: admin. CSRF-protected.
{"role": "admin"}Refuses with 422 last_admin_protection if it would leave zero admins on the project.
Remove membership
Section titled “Remove membership”DELETE /api/v1/projects/{slug}/memberships/{membership_id}Role: admin. CSRF-protected. Removing a user revokes all their sessions on the project.
Invitations
Section titled “Invitations”The invitations router has two halves: an admin half scoped to the project, and a public half (no auth) for the invitee’s accept flow.
Mint an invitation
Section titled “Mint an invitation”POST /api/v1/projects/{slug}/invitationsRole: admin. CSRF-protected.
{ "email": "alice@example.com", "role": "operator", "ttl_days": 7}ttl_days defaults to 7 and is bounded by the server-side min/max (typically 1..30). Response includes the single-use accept URL and the expiry timestamp. If the project has an active email notification channel, the invitation link is auto-emailed; otherwise the dashboard / API returns the link for out-of-band delivery.
List pending invitations
Section titled “List pending invitations”GET /api/v1/projects/{slug}/invitationsRole: admin. Returns InvitationPublic rows for all non-accepted, non-expired invitations.
Revoke a pending invitation
Section titled “Revoke a pending invitation”DELETE /api/v1/projects/{slug}/invitations/{invitation_id}Role: admin. CSRF-protected.
Preview an invitation (public)
Section titled “Preview an invitation (public)”GET /api/v1/invitations/preview?token=<single-use-token>No auth. Returns the invited email, role, and project name so the invitee’s accept page can render context before they submit a password.
Accept an invitation (public)
Section titled “Accept an invitation (public)”POST /api/v1/invitations/acceptNo auth. CSRF-protected via origin check.
{ "token": "<single-use-token>", "display_name": "Alice", "password": "min-12-chars"}Password is bounded 12..200 chars and is hashed with argon2id (see password policy). The token is single-use; subsequent uses return 410 invitation_consumed_or_expired.