Skip to content

API: Workspaces

GET /api/workspaces

Returns 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"
}
]
  • name is null for personal workspaces — clients render a localized label from ownerFirstName.
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 /api/workspaces/{workspaceId}

Owner only. Returns 204. Permanently deletes all tasks, lists, memberships, and attachments. Personal workspaces cannot be deleted.

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.

PUT /api/workspaces/{workspaceId}/members/{memberId}
{ "role": "ADMIN" }

Returns 204. Roles: ADMIN or MEMBER — ownership is not transferable via this endpoint.

DELETE /api/workspaces/{workspaceId}/members/{memberId}

Returns 204. Owners cannot be removed.

POST /api/workspaces/{workspaceId}/leave

Returns 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.

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 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.

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.

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.

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.

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 listId locally, 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.
GET /api/workspaces/{workspaceId}/events

A server-sent-events stream that signals when workspace content changed — a doorbell, not a data feed: re-fetch what you need when it fires.