Collaboration resources
Assignees, labels, subtasks, comments, and attachments hang off a card. They come in two API styles, and knowing which is which tells you how to call them safely:
- Desired-state relationships (assignees, applied labels, attachments,
covers): a
PUTensures the relationship exists, aDELETEensures it is absent. Both are naturally retryable — repeating a call is a no-op — so they need neitherIdempotency-KeynorIf-Match. - Owned resources (labels, subtasks, comments): they have their own
idandversion; creation is a POST with anIdempotency-Key, and updates or deletes are guarded byIf-Match.
Assignees
| Operation | Endpoint |
|---|---|
| Assign a user | PUT /cards/{card_id}/assignees/{user_id} |
| Unassign a user | DELETE /cards/{card_id}/assignees/{user_id} |
The user must be an active member of the workspace with access to the board. Discover valid IDs through the workspace member directory or the board member list. Assignment sends the same notification the web app sends, filtered to authorized recipients.
Labels
Labels are defined per board — a palette of name + color pairs — and then
applied to cards as a relationship:
| Operation | Endpoint |
|---|---|
| List the board palette | GET /boards/{board_id}/labels |
| Create a label | POST /boards/{board_id}/labels |
| Delete a label | DELETE /labels/{label_id} |
| Apply to a card | PUT /cards/{card_id}/labels/{label_id} |
| Remove from a card | DELETE /cards/{card_id}/labels/{label_id} |
The card and the label must belong to the same board; applying a label from
another board fails with 422 validation_error. Deleting a label removes it
from every card that carried it. Colors are six-digit #RRGGBB hex, accepted
case-insensitively and normalized to lowercase.
Subtasks
Subtasks are a card's checklist, ordered by position:
| Operation | Endpoint |
|---|---|
| List | GET /cards/{card_id}/subtasks |
| Create (appends at the end) | POST /cards/{card_id}/subtasks |
| Set completion | PATCH /subtasks/{subtask_id} |
| Delete | DELETE /subtasks/{subtask_id} |
The completion PATCH takes { "completed": true } or
{ "completed": false } — a desired state, not a toggle, so retries are
deterministic.
Comments
| Operation | Endpoint |
|---|---|
| List (cursor-paginated, oldest first) | GET /cards/{card_id}/comments |
| Create | POST /cards/{card_id}/comments |
| Edit | PATCH /comments/{comment_id} |
| Delete | DELETE /comments/{comment_id} |
Editing and deleting are author-only: you can only modify comments your
token's user wrote, regardless of scope. Comment bodies support @-mentions
that notify board members — see
Comment and mention users.
Every effective change bumps the parent card version
The card representation embeds assignee_user_ids, label_ids,
subtask_total, subtask_completed, and comment_count. To keep the card's
ETag a truthful validator of that whole representation, any actual
change to an assignee, applied label, subtask, or comment increments the
parent card's version atomically. Deleting a label bumps every card that
carried it.
Two practical consequences:
- A card
ETagyou are holding can go stale because a teammate commented. When a card write fails with412 precondition_failed, re-read the card and retry — see Optimistic concurrency. - Desired-state calls that change nothing (assigning an already-assigned user, removing an absent label) do not bump any version.
Attachments and covers are the exception: they are separately authorized subresources whose visibility can change with file permissions, so they are not part of the card representation and do not affect the card version. See File attachments.
Example: desired-state assignment
Assigning a user twice is safe — both calls return 200 with the same
relationship:
cURL:
CARD_ID="<card-id>"
USER_ID="<workspace-user-id>"
curl -sS -X PUT "$KANBAN_API_BASE_URL/cards/$CARD_ID/assignees/$USER_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
JavaScript:
const baseUrl =
process.env.KANBAN_API_BASE_URL ?? "https://app.filetag.ai/api/kanban/v1";
const token = process.env.KANBAN_API_TOKEN;
const cardId = process.env.KANBAN_CARD_ID;
const userId = process.env.KANBAN_ASSIGNEE_ID;
async function main() {
const response = await fetch(
`${baseUrl}/cards/${cardId}/assignees/${userId}`,
{
method: "PUT",
headers: { Authorization: `Bearer ${token}` },
}
);
const payload = await response.json();
if (!response.ok) {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
console.log("assigned at", payload.data.assigned_at);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Python:
import json
import os
import urllib.request
BASE_URL = os.environ.get(
"KANBAN_API_BASE_URL", "https://app.filetag.ai/api/kanban/v1"
)
TOKEN = os.environ["KANBAN_API_TOKEN"]
CARD_ID = os.environ["KANBAN_CARD_ID"]
USER_ID = os.environ["KANBAN_ASSIGNEE_ID"]
request = urllib.request.Request(
f"{BASE_URL}/cards/{CARD_ID}/assignees/{USER_ID}",
method="PUT",
headers={"Authorization": f"Bearer {TOKEN}"},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response)
print("assigned at", payload["data"]["assigned_at"])
For a complete walkthrough of these endpoints, see Manage assignees, labels, and subtasks.