Invitations
Send, list, validate, accept, and revoke workspace invitations.
Overview
Workspace owners and admins can invite users by email. Invitations generate a secure single-use token delivered by email. The recipient validates and accepts the token via a public/authenticated endpoint pair.
See the Workspace Invitations guide for the full flow.
Data Model
WorkspaceInvitation
| Field | Type | Description |
|---|---|---|
id | UUID | Invitation identifier |
workspace_id | UUID | Target workspace |
invited_email | string | Normalised (lowercase) recipient email |
role | string | "member" or "admin" |
status | string | pending accepted revoked expired |
expires_at | datetime | 7 days after creation |
created_at | datetime | When invitation was sent |
invited_by_name | string|null | Display name of the sender |
Endpoints
Send Invitation
/api/workspaces/{workspace_id}/invitationsSend an email invitation to join the workspace. Requires owner or admin role.
Request Body:
{
"email": "colleague@company.com",
"role": "member"
}
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Valid email address |
role | string | Yes | "member" or "admin" |
Response (201):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "...",
"invited_email": "colleague@company.com",
"role": "member",
"status": "pending",
"expires_at": "2026-04-10T12:00:00Z",
"created_at": "2026-04-03T12:00:00Z",
"invited_by_name": "Alice Smith"
}
Errors:
| Code | Error | Condition |
|---|---|---|
| 403 | FORBIDDEN | Caller is not owner or admin |
| 409 | ALREADY_MEMBER | Email belongs to an existing workspace member |
| 409 | INVITATION_PENDING | A pending invitation already exists for this email |
The invitation email is sent asynchronously. If email delivery fails (SMTP misconfiguration, etc.) the API still returns 201 — the invitation record exists and the token can be retrieved via the list endpoint if needed.
List Pending Invitations
/api/workspaces/{workspace_id}/invitationsList all pending invitations for a workspace. Requires owner or admin role.
Response (200):
[
{
"id": "uuid",
"workspace_id": "uuid",
"invited_email": "colleague@company.com",
"role": "member",
"status": "pending",
"expires_at": "2026-04-10T12:00:00Z",
"created_at": "2026-04-03T12:00:00Z",
"invited_by_name": "Alice Smith"
}
]
Stale invitations past their expires_at are automatically marked expired when this endpoint is called and will not appear in the results.
Revoke Invitation
/api/workspaces/{workspace_id}/invitations/{invitation_id}Cancel a pending invitation before it is accepted. Requires owner or admin role.
Response (200): The revoked invitation with status: "revoked".
Errors:
| Code | Error | Condition |
|---|---|---|
| 400 | INVALID_STATE | Invitation is not in pending status |
| 403 | FORBIDDEN | Caller is not owner or admin |
| 404 | NOT_FOUND | Invitation not found in this workspace |
Validate Token
/api/invitations/{token}/validateCheck whether an invitation token is valid and retrieve workspace/inviter details. This endpoint requires no authentication and is called by the invitation landing page.
Response (200) — valid token:
{
"valid": true,
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_name": "Acme Product Team",
"inviter_name": "Alice Smith",
"inviter_email": "alice@acme.com",
"invited_email": "colleague@company.com",
"role": "member",
"error": null
}
Response (200) — invalid/expired token:
{
"valid": false,
"workspace_id": null,
"workspace_name": null,
"inviter_name": null,
"inviter_email": null,
"invited_email": null,
"role": null,
"error": "Invitation has expired"
}
This endpoint always returns HTTP 200. Use the valid field to determine invitation state. Calling validate on an expired token auto-updates its status to expired in the database.
Accept Invitation
/api/invitations/{token}/acceptAccept an invitation and join the workspace. Requires authentication. The authenticated user's email must match the invited email.
Request Body: None (token in path is sufficient).
Response (200):
{
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_name": "Acme Product Team"
}
Errors:
| Code | Error | Condition |
|---|---|---|
| 400 | INVITATION_EXPIRED | Token is past 7-day window |
| 400 | INVITATION_REVOKED | Admin cancelled the invitation |
| 400 | INVITATION_NOT_FOUND | Token does not exist |
| 403 | EMAIL_MISMATCH | Caller's email ≠ invited_email |
Side effects on acceptance:
- Creates a
WorkspaceMemberrecord with the invitation's role - Sets the user's
access_statustoactiveif it waspendingorwaitlisted(invitations bypass the access gate) - Marks invitation
acceptedwithaccepted_by_user_idandaccepted_at
Invitation Expiry
Invitations expire 7 days after creation. Expiry is lazy — the status is updated to expired the first time the record is read (validate, accept, or list) after the deadline. You do not need to run a cleanup job.
Token Security
- Tokens are UUID v4 strings (36 characters, URL-safe)
- Tokens are single-use: once accepted, the invitation cannot be accepted again
- Tokens are validated server-side; possession of a valid token plus a matching email is required to join
- Tokens are delivered only via email; they are never returned in list or create responses
Was this helpful?