Skip to content

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.

GET /api/v1/projects/{slug}/memberships

Role: viewer. Returns:

[
{
"id": "...",
"user_id": "...",
"project_id": "...",
"role": "operator",
"created_at": "...",
"updated_at": "..."
}
]
POST /api/v1/projects/{slug}/memberships

Role: 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).

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.

DELETE /api/v1/projects/{slug}/memberships/{membership_id}

Role: admin. CSRF-protected. Removing a user revokes all their sessions on the project.

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.

POST /api/v1/projects/{slug}/invitations

Role: 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.

GET /api/v1/projects/{slug}/invitations

Role: admin. Returns InvitationPublic rows for all non-accepted, non-expired invitations.

DELETE /api/v1/projects/{slug}/invitations/{invitation_id}

Role: admin. CSRF-protected.

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.

POST /api/v1/invitations/accept

No 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.