Team and invitations API
Workspace membership is how sharing actually works in OpenDocs — if a
post is workspace visible, every active member of that workspace can
read it. These endpoints cover everything about team members and
invitations.
Every endpoint on this page requires a browser session cookie. API
keys return 401 unauthorized — teams are a dashboard surface. See
Authentication.
All routes fall under the general 120 req/min rate bucket.
Roles
Every member has one of three roles:
| Role | What it grants |
|---|---|
owner | Every permission, plus the right to transfer ownership. One per workspace. |
admin | Full admin: rename workspace, change slug, manage branding, invite / remove / re-role members. Can't remove the owner. |
member | Read/write posts. Cannot change workspace settings or manage members. |
The user who creates the workspace becomes its owner. Admins and members are added through the invitation flow below.
Member caps
Member counts are capped per workspace on Free and Pro:
| Plan | Members per workspace |
|---|---|
| Free | 3 |
| Pro | 5 |
| Team | unlimited |
Hitting the cap returns 403 member_limit from POST /workspace/team/invite. See
Plans and limits for the full
breakdown.
Team seats are account-scoped. The app does not enforce a per-workspace Team cap; instead, when an invited user accepts, OpenDocs counts unique active members across every workspace owned by the Team subscriber and updates the Paddle seat quantity before activating the new membership.
workspaceId is required on all team endpointsEvery route on this page is workspace-scoped and requires an explicit
workspaceId — as a ?workspaceId= query param on GET routes and as a
body field on mutations. Omitting it returns 400 missing_workspace_id
(GET) or 400 invalid_request (mutations). Callers who aren't active
members of the target workspace get 403 forbidden.
GET /api/v1/workspace/team?workspaceId={id}
List the target workspace's members.
Response
{
"members": [
{
"id": "mem_1",
"userId": "usr_1",
"email": "ada@acme.com",
"name": "Ada Lovelace",
"image": "https://cdn.opendocs.cc/avatars/usr_1.png",
"role": "owner",
"joinedAt": "2026-01-10T12:00:00Z"
},
{
"id": "mem_2",
"userId": "usr_2",
"email": "grace@acme.com",
"name": "Grace Hopper",
"image": null,
"role": "admin",
"joinedAt": "2026-02-14T09:30:00Z"
}
]
}
Errors
| Status | error | When |
|---|---|---|
400 | missing_workspace_id | workspaceId query param absent |
403 | forbidden | Caller isn't an active member of workspaceId |
GET /api/v1/workspace/team/stats?workspaceId={id}
Compact numeric summary used by the dashboard badge.
{
"totalMembers": 3,
"pendingInvites": 1,
"admins": 1
}
Same error shape as GET /workspace/team above.
GET /api/v1/workspace/team/invitations?workspaceId={id}
List pending invitations. Owner/admin only — members get 403 forbidden.
{
"invitations": [
{
"id": "inv_1",
"email": "mike@acme.com",
"role": "member",
"invitedAt": "2026-04-01T10:15:00Z",
"invitedByName": "Ada Lovelace"
}
]
}
Same error shape as GET /workspace/team above, plus 403 forbidden
when the caller is a member (rather than owner/admin).
POST /api/v1/workspace/team/invite
Send an invitation. Owner/admin only.
Request body
{
"workspaceId": "ws_123",
"email": "mike@acme.com",
"role": "member"
}
| Field | Required | Notes |
|---|---|---|
workspaceId | yes | The target workspace. Caller must be an active owner/admin of it. |
email | yes | Valid email, ≤ 320 chars. |
role | yes | "admin" or "member". You can't invite another owner. |
Success response
Status 201.
{
"invitation": {
"id": "inv_1",
"email": "mike@acme.com",
"role": "member",
"invitedAt": "2026-04-17T10:15:00Z",
"invitedByName": "Ada Lovelace"
}
}
Errors
| Status | error | When |
|---|---|---|
400 | invalid_request | workspaceId, email, or role missing or malformed |
400 | invite_limit | Pending-invitation backlog is too large |
403 | forbidden | Caller isn't owner/admin of workspaceId |
403 | member_limit | Workspace is at its per-plan member cap |
409 | already_member | Email already belongs to an active member |
409 | already_invited | A pending invitation for this email already exists |
POST /api/v1/workspace/team/invitations/:id/resend
Re-send the invitation email and refresh its expiry.
{ "success": true }
Errors: 403 forbidden, 404 not_found.
DELETE /api/v1/workspace/team/invitations/:id
Cancel a pending invitation.
{ "success": true }
Errors: 403 forbidden, 404 not_found.
POST /api/v1/workspace/team/accept-invite
Accept an invitation token. Called from the ?token=… landing page — not
typically used from automation.
Request body
{ "token": "inv_signed_token_abc123" }
Success response
{
"workspaceId": "ws_123",
"role": "member"
}
Errors
| Status | error | When |
|---|---|---|
404 | not_found | Token doesn't match any invitation |
410 | expired | Invitation has expired — ask the inviter to re-send |
409 | already_member | Caller is already a member of this workspace |
403 | forbidden | Signed-in user's email does not match the invited email |
402 | billing_failed | Team seat quantity needed to increase before acceptance, but Paddle update failed |
PATCH /api/v1/workspace/team/members/:id/role
Change a member's role. Owner/admin only.
Request body
{ "role": "admin" }
Role must be "admin" or "member". The owner's role can't be changed
through this endpoint — use the ownership-transfer flow in the dashboard.
Success response
{
"member": {
"id": "mem_2",
"userId": "usr_2",
"name": "Grace Hopper",
"email": "grace@acme.com",
"image": null,
"role": "admin",
"joinedAt": "2026-02-14T09:30:00Z"
}
}
Errors: 403 forbidden, 404 not_found.
DELETE /api/v1/workspace/team/members/:id
Remove a member. Owner/admin only. The owner cannot be removed.
{ "success": true }
Errors: 403 forbidden, 404 not_found.
Invitation lifecycle
- send —
POST /workspace/team/invite— statuspending, email sent, token generated, 7-day expiry. - resend —
POST /workspace/team/invitations/:id/resend— re-send email, reset expiry. - cancel —
DELETE /workspace/team/invitations/:id— invitation is deleted; the token stops working. - accept —
POST /workspace/team/accept-invite— invitee becomes an active member, invitation row is marked consumed. - expire — if no accept within 7 days, the token starts returning
410 expired. Resending refreshes the expiry without issuing a new token URL.
Related pages
- Workspaces API — settings, branding, and slug
- Workspaces and team access — conceptual overview of how membership affects visibility
- Plans and limits — member caps per tier
- Errors and pagination — full error catalogue