Workspaces and people
Workspaces, their members, groups and invitations, the capabilities of the calling account, and what the workspace has used of its plan.
24 operations. Every path is relative to the instance origin; every response body is JSON unless stated. See API for authentication, errors and pagination.
GET /api/v1/workspaces
List workspaces the caller is a member of.
listWorkspaces · token scope: session only
Query parameters
cursor· string — length 1–∞limit· integer — 1–100
Response 200 — application/json
items· object[] — requiredid· string (uuid) — requiredslug· string — required, length 1–100name· string — requiredicon· string | null — requiredrole·"owner"|"admin"|"member"|"guest"— requiredcreatedAt· string (date-time) — required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces
Create a workspace; the caller becomes its owner.
createWorkspace · token scope: session only
Request body — application/json, required
name· string — required, length 1–200slug· string — length 1–100
Response 200 — application/json
id· string (uuid) — requiredslug· string — required, length 1–100name· string — requiredicon· string | null — requiredrole·"owner"|"admin"|"member"|"guest"— requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}
Get a single workspace.
getWorkspace · token scope: content:read
Path parameters
workspaceId· string (uuid) — required
Response 200 — application/json
id· string (uuid) — requiredslug· string — required, length 1–100name· string — requiredicon· string | null — requiredrole·"owner"|"admin"|"member"|"guest"— requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PATCH /api/v1/workspaces/{workspaceId}
Update a workspace name and/or icon.
updateWorkspace · token scope: admin:write
Path parameters
workspaceId· string (uuid) — required
Request body — application/json, required
name· string — length 1–200icon· string | null
Response 200 — application/json
id· string (uuid) — requiredslug· string — required, length 1–100name· string — requiredicon· string | null — requiredrole·"owner"|"admin"|"member"|"guest"— requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}
Delete a workspace and everything in it — irreversible. Requires the workspace.delete capability, i.e. owner (ADR-0016 D1); an admin gets 403. Removes memberships, groups, invitations, collections, the field registry, every block with its doc and projections, and every attachment.
deleteWorkspace · token scope: admin:write
Path parameters
workspaceId· string (uuid) — required
Response 200 — application/json
id· string (uuid) — requiredslug· string — required, length 1–100name· string — requiredicon· string | null — requiredrole·"owner"|"admin"|"member"|"guest"— requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/members
List the members of a workspace. Email addresses are disclosed only to callers holding members.manage (admin/owner) — user.email is null for everyone else (ADR-0016 D9d). A guest sees only the members co-present on a page they can read, names only.
listWorkspaceMembers · token scope: admin:read
Path parameters
workspaceId· string (uuid) — required
Query parameters
cursor· string — length 1–∞limit· integer — 1–100
Response 200 — application/json
items· object[] — requiredworkspaceId· string (uuid) — requireduserId· string (uuid) — requiredrole·"owner"|"admin"|"member"|"guest"— requireduser· object — requiredid· string (uuid) — requiredemail· string (email) | null — requiredname· string | null — requiredimage· string | null — required
joinedAt· string (date-time) — required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/collections/{collectionId}/members
List a collection's explicit grants (collection_member, ADR-0016 D3) with each subject resolved for display. These are step 2 of resolution — the implicit levels a visibility gives a workspace role (D2) are NOT rows and do not appear here. Requires full on the collection.
listCollectionMembers · token scope: admin:read
Path parameters
workspaceId· string (uuid) — requiredcollectionId· string (uuid) — required
Query parameters
cursor· string — length 1–∞limit· integer — 1–100
Response 200 — application/json
items· object[] — requiredcollectionId· string (uuid) — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredrole·"read"|"comment"|"edit"|"full"— requiredsubject· object — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredname· string — requiredimage· string | null — required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PUT /api/v1/workspaces/{workspaceId}/collections/{collectionId}/members
Grant a user or group a level in a collection — upsert on (collection, subjectType, subjectId), so re-sending changes the level. Requires full on the collection; an owner may always write themselves in (D1: owners can take access, never quietly have it). Bumps the access generation.
setCollectionMember · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requiredcollectionId· string (uuid) — required
Request body — application/json, required
subjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredrole·"read"|"comment"|"edit"|"full"— required
Response 200 — application/json
collectionId· string (uuid) — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredrole·"read"|"comment"|"edit"|"full"— requiredsubject· object — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredname· string — requiredimage· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}/collections/{collectionId}/members/{subjectType}/{subjectId}
Revoke a subject's explicit grant in a collection. Requires full on the collection. Refused (409) when it would remove the LAST full member (ADR-0016 D6 — the same last-subject invariant as ACLs). Bumps the access generation. Returns the membership that was removed.
removeCollectionMember · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — required
Response 200 — application/json
collectionId· string (uuid) — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredrole·"read"|"comment"|"edit"|"full"— requiredsubject· object — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredname· string — requiredimage· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/usage
This workspace's storage and seat usage against its plan's limits. Readable by every member (guests included): it is the answer to “why was my upload refused”. Being over a limit stops new writes of that kind and never removes, hides or locks anything that already exists.
getWorkspaceUsage · token scope: content:read
Path parameters
workspaceId· string (uuid) — required
Response 200 — application/json
workspaceId· string (uuid) — requiredplan·"free"|"team"|"business"|"self_hosted"— requiredlimits· object — requiredstorageBytes· integer | null — requiredfileSizeBytes· integer | null — requiredmembers· integer | null — requiredguests· integer | null — requiredversionHistoryDays· integer | null — required
storageBytes· integer — required, 0–9007199254740991attachmentCount· integer — required, 0–9007199254740991members· integer — required, 0–9007199254740991guests· integer — required, 0–9007199254740991over·"storageBytes"|"fileSizeBytes"|"members"|"guests"|"versionHistoryDays"[] — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/me/capabilities
The caller's own workspace role and the administrative capabilities it carries (ADR-0016 D1), derived server-side from workspaceCapabilities(). The single source for enabling admin UI — content permissions are NOT here (no role grants implicit read); those come from getPageAccess.
getMyCapabilities · token scope: admin:read
Path parameters
workspaceId· string (uuid) — required
Response 200 — application/json
workspaceId· string (uuid) — requireduserId· string (uuid) — requiredrole·"owner"|"admin"|"member"|"guest"— requiredcapabilities·"workspace.delete"|"workspace.transferOwnership"|"workspace.manageOwnerRole"|"workspace.manage"|"members.manage"|"groups.manage"|"invitations.manage"|"fields.manage"|"collections.create"|"collections.delete"|"purge"[] — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PATCH /api/v1/workspaces/{workspaceId}/members/{userId}
Change a member's workspace role (ADR-0016 D11a). Granting or revoking owner requires workspace.manageOwnerRole (owner only); every other change requires members.manage. Nobody may raise their OWN role, and the last owner cannot be demoted (409). Bumps the workspace access generation.
updateMemberRole · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requireduserId· string (uuid) — required
Request body — application/json, required
role·"owner"|"admin"|"member"|"guest"— required
Response 200 — application/json
workspaceId· string (uuid) — requireduserId· string (uuid) — requiredrole·"owner"|"admin"|"member"|"guest"— requireduser· object — requiredid· string (uuid) — requiredemail· string (email) | null — requiredname· string | null — requiredimage· string | null — required
joinedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}/members/{userId}
Remove a member from the workspace. Requires members.manage; removing an owner additionally requires workspace.manageOwnerRole, and the last owner cannot be removed (409). Drops their group memberships; their explicit ACL and collection-member rows are removed with them, so a restricted page may be left without a full subject — the D6 invariant is re-checked and the removal is refused (409) when it would strand one. Returns the membership that was removed.
removeMember · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requireduserId· string (uuid) — required
Response 200 — application/json
workspaceId· string (uuid) — requireduserId· string (uuid) — requiredrole·"owner"|"admin"|"member"|"guest"— requireduser· object — requiredid· string (uuid) — requiredemail· string (email) | null — requiredname· string | null — requiredimage· string | null — required
joinedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/groups
List the workspace groups with their member counts. Readable by any member: a group name has to be legible in a share dialog for the permission model to be understandable. Writes require groups.manage.
listGroups · token scope: admin:read
Path parameters
workspaceId· string (uuid) — required
Query parameters
cursor· string — length 1–∞limit· integer — 1–100
Response 200 — application/json
items· object[] — requiredid· string (uuid) — requiredworkspaceId· string (uuid) — requiredname· string — requiredmemberCount· integer — required, 0–9007199254740991createdAt· string (date-time) — required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/groups
Create a group. Requires groups.manage. Groups are flat — no nesting (D7).
createGroup · token scope: admin:write
Path parameters
workspaceId· string (uuid) — required
Request body — application/json, required
name· string — required, length 1–200
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredname· string — requiredmemberCount· integer — required, 0–9007199254740991createdAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PATCH /api/v1/workspaces/{workspaceId}/groups/{groupId}
Rename a group. Requires groups.manage.
updateGroup · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requiredgroupId· string (uuid) — required
Request body — application/json, required
name· string — required, length 1–200
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredname· string — requiredmemberCount· integer — required, 0–9007199254740991createdAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}/groups/{groupId}
Delete a group. Requires groups.manage. Its permission and collection_member rows go with it, so access granted only through this group is revoked — refused (409) when that would leave a restricted block or a collection without a full subject (D6). Bumps the access generation. Returns the deleted group.
deleteGroup · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requiredgroupId· string (uuid) — required
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredname· string — requiredmemberCount· integer — required, 0–9007199254740991createdAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/groups/{groupId}/members
List a group's users. Readable by any member (see listGroups); changing the membership requires groups.manage. Because the read half is open to every member — a guest included — user.email follows the same D9d rule as listWorkspaceMembers: an address for a caller holding members.manage, null for everybody else.
listGroupMembers · token scope: admin:read
Path parameters
workspaceId· string (uuid) — requiredgroupId· string (uuid) — required
Query parameters
cursor· string — length 1–∞limit· integer — 1–100
Response 200 — application/json
items· object[] — requiredgroupId· string (uuid) — requireduserId· string (uuid) — requireduser· object — requiredid· string (uuid) — requiredemail· string (email) | null — requiredname· string | null — requiredimage· string | null — required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PUT /api/v1/workspaces/{workspaceId}/groups/{groupId}/members/{userId}
Add a workspace member to a group (idempotent: re-adding is a no-op that returns the existing membership). Requires groups.manage; the user must already be a member of the workspace. Bumps the access generation (D10).
addGroupMember · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requiredgroupId· string (uuid) — requireduserId· string (uuid) — required
Response 200 — application/json
groupId· string (uuid) — requireduserId· string (uuid) — requireduser· object — requiredid· string (uuid) — requiredemail· string (email) | null — requiredname· string | null — requiredimage· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}/groups/{groupId}/members/{userId}
Remove a user from a group. Requires groups.manage. Refused (409) when the group is the only remaining full subject of a restricted block or a collection and this user is its last member (D6). Bumps the access generation. Returns the membership that was removed.
removeGroupMember · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requiredgroupId· string (uuid) — requireduserId· string (uuid) — required
Response 200 — application/json
groupId· string (uuid) — requireduserId· string (uuid) — requireduser· object — requiredid· string (uuid) — requiredemail· string (email) | null — requiredname· string | null — requiredimage· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/invitations
List the workspace's invitations (pending first, newest first). Requires invitations.manage. Tokens are never returned again — only createInvitation ever shows one.
listInvitations · token scope: admin:read
Path parameters
workspaceId· string (uuid) — required
Query parameters
cursor· string — length 1–∞limit· integer — 1–100
Response 200 — application/json
items· object[] — requiredid· string (uuid) — requiredworkspaceId· string (uuid) — requiredemail· string (email) — requiredrole·"admin"|"member"|"guest"— requiredinvitedBy· string (uuid) — requiredexpiresAt· string (date-time) — requiredacceptedAt· string (date-time) | null — requiredcreatedAt· string (date-time) — required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/invitations
Invite an email address. Requires invitations.manage; the invited role may not exceed the inviter's own, and owner is not invitable (ownership is transferred). At most one un-accepted invitation exists per (workspace, lower(email)) — a repeat invite replaces the pending one. The token is 32 CSPRNG bytes stored HASHED, single-use, 14-day expiry, and appears exactly once: in this response's url. The response never reveals whether the address already has an account.
createInvitation · token scope: admin:write
Path parameters
workspaceId· string (uuid) — required
Request body — application/json, required
email· string (email) — required, length 0–320role·"admin"|"member"|"guest"— required
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredemail· string (email) — requiredrole·"admin"|"member"|"guest"— requiredinvitedBy· string (uuid) — requiredexpiresAt· string (date-time) — requiredacceptedAt· string (date-time) | null — requiredcreatedAt· string (date-time) — requiredurl· string — requiredmailed· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}/invitations/{invitationId}
Revoke an invitation by deleting its row — the token stops working immediately. Requires invitations.manage. Returns the revoked invitation.
revokeInvitation · token scope: admin:write
Path parameters
workspaceId· string (uuid) — requiredinvitationId· string (uuid) — required
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredemail· string (email) — requiredrole·"admin"|"member"|"guest"— requiredinvitedBy· string (uuid) — requiredexpiresAt· string (date-time) — requiredacceptedAt· string (date-time) | null — requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/invitations/accept
Redeem an invitation token and join its workspace. Deliberately OUTSIDE the /workspaces/:workspaceId scope (ADR-0016 D11): the membership guard would 404 the very person being invited. Requires a session and matches on its email (lower(email) on both sides; plus-addressing NOT stripped). The invited role is re-validated against the INVITER's current role at accept time. insert … on conflict do nothing on workspace_member, so an existing membership wins and a second acceptance cannot change a role. Returns the resulting membership (existing or new).
acceptInvitation · token scope: session only
Request body — application/json, required
token· string — required, length 1–200
Response 200 — application/json
workspaceId· string (uuid) — requireduserId· string (uuid) — requiredrole·"owner"|"admin"|"member"|"guest"— requireduser· object — requiredid· string (uuid) — requiredemail· string (email) | null — requiredname· string | null — requiredimage· string | null — required
joinedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required