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

FieldTypeDescription
idUUIDInvitation identifier
workspace_idUUIDTarget workspace
invited_emailstringNormalised (lowercase) recipient email
rolestring"member" or "admin"
statusstringpending accepted revoked expired
expires_atdatetime7 days after creation
created_atdatetimeWhen invitation was sent
invited_by_namestring|nullDisplay name of the sender

Endpoints

Send Invitation

POST/api/workspaces/{workspace_id}/invitations

Send an email invitation to join the workspace. Requires owner or admin role.

Request Body:

{
  "email": "colleague@company.com",
  "role": "member"
}
FieldTypeRequiredDescription
emailstringYesValid email address
rolestringYes"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:

CodeErrorCondition
403FORBIDDENCaller is not owner or admin
409ALREADY_MEMBEREmail belongs to an existing workspace member
409INVITATION_PENDINGA 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

GET/api/workspaces/{workspace_id}/invitations

List 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

DELETE/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:

CodeErrorCondition
400INVALID_STATEInvitation is not in pending status
403FORBIDDENCaller is not owner or admin
404NOT_FOUNDInvitation not found in this workspace

Validate Token

GET/api/invitations/{token}/validate

Check 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

POST/api/invitations/{token}/accept

Accept 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:

CodeErrorCondition
400INVITATION_EXPIREDToken is past 7-day window
400INVITATION_REVOKEDAdmin cancelled the invitation
400INVITATION_NOT_FOUNDToken does not exist
403EMAIL_MISMATCHCaller's email ≠ invited_email

Side effects on acceptance:

  • Creates a WorkspaceMember record with the invitation's role
  • Sets the user's access_status to active if it was pending or waitlisted (invitations bypass the access gate)
  • Marks invitation accepted with accepted_by_user_id and accepted_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?