API: Workspaces
List workspaces
Section titled “List workspaces”GET /api/workspacesReturns every workspace you are a member of, with your role in each.
[ { "id": "019f7462-ac2a-73e3-ab40-be2e43f943f9", "name": null, "personal": true, "ownerFirstName": "Greta", "role": "OWNER" }, { "id": "019f7464-1617-7464-b8ef-7f34b631bdfa", "name": "Lengl Family", "personal": false, "ownerFirstName": "Greta", "role": "OWNER" }]nameisnullfor personal workspaces — clients render a localized label fromownerFirstName.
Create a shared workspace
Section titled “Create a shared workspace”POST /api/workspaces{ "name": "Renovation" }Returns 201 with { "id": "…" }. Errors: 409 if you already own a
workspace with this name (case-insensitive), 403 if you reached your
shared-workspace limit.
Delete a workspace
Section titled “Delete a workspace”DELETE /api/workspaces/{workspaceId}Owner only. Returns 204. Permanently deletes all tasks, lists,
memberships, and attachments. Personal workspaces cannot be deleted.
Members
Section titled “Members”Every workspace has a member limit that counts all members including the owner; it is an entitlement of the workspace owner (default plan: 5).
GET /api/workspaces/{workspaceId}/members[ { "userId": "019f…", "firstName": "Greta", "lastName": "Lengl", "avatarUrl": "/api/avatars/019f…?v=3", "role": "OWNER" }]New members are added exclusively through email invitations.
Change a member’s role
Section titled “Change a member’s role”PUT /api/workspaces/{workspaceId}/members/{memberId}{ "role": "ADMIN" }Returns 204. Roles: ADMIN or MEMBER — ownership is not transferable
via this endpoint.
Remove a member
Section titled “Remove a member”DELETE /api/workspaces/{workspaceId}/members/{memberId}Returns 204. Owners cannot be removed.
Leave a workspace
Section titled “Leave a workspace”POST /api/workspaces/{workspaceId}/leaveReturns 204. Leaves a shared workspace you joined: the tasks you authored are
reassigned to a reserved “Inactive User” and your assignments are cleared. The
owner cannot leave (400) — transfer ownership first. Non-members get 404.
Transfer ownership
Section titled “Transfer ownership”POST /api/workspaces/{workspaceId}/transfer-ownership{ "newOwnerId": "019f…" }Owner only. Returns 204: the recipient becomes the owner and the caller
stays on as an administrator; the workspace’s storage and member limits
follow the new owner’s plan from then on. Errors: 404 when the recipient
is not a member, 400 for a personal workspace or a transfer to yourself,
403 when the recipient is at their owned-shared-workspaces limit, 409
when the recipient already owns a workspace with the same name.
Invitations
Section titled “Invitations”Invitations are personal: each one is bound to a single email address, valid for 7 days, and single-use. Expired invitations are also removed by a daily cleanup job.
Send invitations
Section titled “Send invitations”POST /api/workspaces/{workspaceId}/invites{ "emails": ["person@example.com", "other@example.com"] }Owner/admin only, shared workspaces only, at most 20 addresses per
request. Every successfully invited address receives an email with a personal
invite link (tdminv_… token). Re-inviting an address issues a fresh link
and voids the previous one. The response reports each address:
{ "results": [ { "email": "person@example.com", "outcome": "INVITED" }, { "email": "other@example.com", "outcome": "ALREADY_MEMBER" } ]}Outcomes: INVITED, ALREADY_MEMBER, INVALID_ADDRESS. Errors: 400 for
an empty or oversized batch, 403 for plain members or when the workspace
is already at its member limit, 404 for non-members.
Preview an invite
Section titled “Preview an invite”GET /api/workspaces/invites/{token}Requires authentication, and the invite must be addressed to the signed-in account’s email. Returns who invited you:
{ "workspaceName": "Lengl Family", "personal": false, "ownerFirstName": "Greta", "ownerLastName": "Lengl", "ownerAvatarUrl": null}Unknown, expired, and someone-else’s tokens are indistinguishable: all 404.
Accept an invite
Section titled “Accept an invite”POST /api/workspaces/join{ "token": "tdminv_…" }{ "workspaceId": "019f…", "newlyJoined": true }newlyJoined is false when you were already a member.
The same personal rule applies: the token only works for the account with the
invited email address, and it is consumed on success. Unknown, expired, and
someone-else’s tokens all return 404. Returns 403 when the workspace is
at its member limit — the invitation survives and can be retried while it is
valid.
Delta sync
Section titled “Delta sync”GET /api/workspaces/{workspaceId}/sync?since={cursor}Pulls everything that changed after the client’s cursor, for offline-capable
clients: changed entities as full current-state objects, deletions as id
lists, and the next cursor. since=0 returns the complete workspace (the
initial mirror). Applying a batch is an idempotent upsert, so repeating or
overlapping pulls is harmless.
{ "cursor": 812, "tasks": [ { "id": "019f…", "title": "…", "subtasks": [], "attachments": [] } ], "deletedTaskIds": ["019f…"], "lists": [], "deletedListIds": [], "members": [], "deletedMemberIds": []}- Task objects are the same shape as
GET /api/workspaces/{id}/tasks; subtask and attachment changes surface as changes of their parent task. - When a list is deleted, its tasks are not re-sent: clients clear that
listIdlocally, matching the server’s behavior. - Changes younger than about a second are included in the batch, but the cursor stops before them — the next pull delivers them again, and the idempotent upsert absorbs the repeat. A client that pulls right after its own write therefore always sees that write.
Change events (SSE)
Section titled “Change events (SSE)”GET /api/workspaces/{workspaceId}/eventsA server-sent-events stream that signals when workspace content changed — a doorbell, not a data feed: re-fetch what you need when it fires.
