Skip to main content

Members and Invitations

Workspaces are collaborative. Owners and admins add people by sending email invitations; once accepted, the invitee becomes a member with the role chosen at invitation time. All endpoints here use JWT bearer authentication. The /workspaces/current/* routes additionally require the X-Posta-Workspace-Id header.

Members

List members

GET /api/v1/workspaces/current/members
curl -X GET http://localhost:9000/api/v1/workspaces/current/members \
-H "Authorization: Bearer <jwt>" \
-H "X-Posta-Workspace-Id: 1"

Response:

{
"data": [
{
"id": 5,
"user_id": 42,
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "owner",
"created_at": "2026-05-31T10:00:00Z"
}
]
}

Update a member's role

PUT /api/v1/workspaces/current/members/{member_id}

member_id is the user ID of the member (the user_id field returned by the list endpoint).

{
"role": "editor"
}

Valid roles are admin, editor, and viewer. The owner role cannot be assigned (400), and an existing owner's role cannot be changed (400). The member receives a role-change notification email. Returns a message payload on success.

Remove a member

DELETE /api/v1/workspaces/current/members/{member_id}

Removes the member identified by their user ID and returns 204. The workspace owner cannot be removed (400).

Invitations (workspace side)

Owners and admins manage outgoing invitations under /workspaces/current/invitations.

Invite a member

POST /api/v1/workspaces/current/invitations
{
"email": "newuser@example.com",
"role": "editor"
}

Both fields are required. role may be admin, editor, or viewer — inviting as owner is rejected (400). If the email already belongs to a member, the request returns 409. The inviter's email must be verified.

A pending invitation is created with a unique token, valid for 7 days, and an invitation email is sent to the address with an accept link of the form /<app>/invitations?token=<token>.

Response (201):

{
"data": {
"id": 12,
"workspace_id": 1,
"workspace": "",
"email": "newuser@example.com",
"role": "editor",
"status": "pending",
"expires_at": "2026-06-07T10:00:00Z",
"created_at": "2026-05-31T10:00:00Z"
}
}
curl -X POST http://localhost:9000/api/v1/workspaces/current/invitations \
-H "Authorization: Bearer <jwt>" \
-H "X-Posta-Workspace-Id: 1" \
-H "Content-Type: application/json" \
-d '{"email": "newuser@example.com", "role": "editor"}'

List pending invitations

GET /api/v1/workspaces/current/invitations

Returns the workspace's pending invitations as an array of the object shown above.

Cancel an invitation

DELETE /api/v1/workspaces/current/invitations/{id}

Deletes the pending invitation by its ID and returns 204.

Invitations (invitee side)

These routes are for the recipient of an invitation. They use JWT auth only and do not require the workspace header — the invitee is not yet a member.

List my invitations

GET /api/v1/invitations

Lists all pending invitations addressed to the authenticated user's email. Each entry includes the originating workspace name in the workspace field:

{
"data": [
{
"id": 12,
"workspace_id": 1,
"workspace": "Acme Inc",
"email": "newuser@example.com",
"role": "editor",
"status": "pending",
"expires_at": "2026-06-07T10:00:00Z",
"created_at": "2026-05-31T10:00:00Z"
}
]
}

Accept or decline by token

Used by the link in the invitation email.

POST /api/v1/invitations/accept
POST /api/v1/invitations/decline
{
"token": "<invitation_token>"
}

On accept, the invitee is added to the workspace with the invited role and the invitation is marked accepted. The invitation's email must match the authenticated user (403 otherwise). Accepting a non-pending or expired invitation returns 400; if the user is already a member the invitation is marked accepted and 409 is returned.

A successful accept returns the joined workspace:

{
"data": {
"message": "joined workspace \"Acme Inc\"",
"workspace_id": 1
}
}

Accept or decline by ID

Used by the dashboard when the user is viewing their pending invitations.

POST /api/v1/invitations/{id}/accept
POST /api/v1/invitations/{id}/decline

These take no body. The same validation applies: the invitation must be pending, not expired, and addressed to the authenticated user's email.

curl -X POST http://localhost:9000/api/v1/invitations/12/accept \
-H "Authorization: Bearer <jwt>"