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 409 conflict if it would leave zero admins on the project; the message says to promote another member to admin first.

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

Role: admin. CSRF-protected. Removing the membership takes effect on the next project authorization check, so the user's existing global browser sessions can no longer access this project. It does not revoke those sessions or sign the user out of other projects.

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": "[email protected]",
"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.

POST /api/v1/invitations/preview
Body: {"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 404 with error: "not_found" and the message invalid_or_expired, indistinguishable from an unknown token.