Skip to main content

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.

Session auth only

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:

RoleWhat it grants
ownerEvery permission, plus the right to transfer ownership. One per workspace.
adminFull admin: rename workspace, change slug, manage branding, invite / remove / re-role members. Can't remove the owner.
memberRead/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:

PlanMembers per workspace
Free3
Pro5
Teamunlimited

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 endpoints

Every 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

StatuserrorWhen
400missing_workspace_idworkspaceId query param absent
403forbiddenCaller 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"
}
FieldRequiredNotes
workspaceIdyesThe target workspace. Caller must be an active owner/admin of it.
emailyesValid email, ≤ 320 chars.
roleyes"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

StatuserrorWhen
400invalid_requestworkspaceId, email, or role missing or malformed
400invite_limitPending-invitation backlog is too large
403forbiddenCaller isn't owner/admin of workspaceId
403member_limitWorkspace is at its per-plan member cap
409already_memberEmail already belongs to an active member
409already_invitedA 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

StatuserrorWhen
404not_foundToken doesn't match any invitation
410expiredInvitation has expired — ask the inviter to re-send
409already_memberCaller is already a member of this workspace
403forbiddenSigned-in user's email does not match the invited email
402billing_failedTeam 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

  1. sendPOST /workspace/team/invite — status pending, email sent, token generated, 7-day expiry.
  2. resendPOST /workspace/team/invitations/:id/resend — re-send email, reset expiry.
  3. cancelDELETE /workspace/team/invitations/:id — invitation is deleted; the token stops working.
  4. acceptPOST /workspace/team/accept-invite — invitee becomes an active member, invitation row is marked consumed.
  5. expire — if no accept within 7 days, the token starts returning 410 expired. Resending refreshes the expiry without issuing a new token URL.