Skip to main content

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 PUT ensures the relationship exists, a DELETE ensures it is absent. Both are naturally retryable — repeating a call is a no-op — so they need neither Idempotency-Key nor If-Match.
  • Owned resources (labels, subtasks, comments): they have their own id and version; creation is a POST with an Idempotency-Key, and updates or deletes are guarded by If-Match.

Assignees

OperationEndpoint
Assign a userPUT /cards/{card_id}/assignees/{user_id}
Unassign a userDELETE /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:

OperationEndpoint
List the board paletteGET /boards/{board_id}/labels
Create a labelPOST /boards/{board_id}/labels
Delete a labelDELETE /labels/{label_id}
Apply to a cardPUT /cards/{card_id}/labels/{label_id}
Remove from a cardDELETE /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:

OperationEndpoint
ListGET /cards/{card_id}/subtasks
Create (appends at the end)POST /cards/{card_id}/subtasks
Set completionPATCH /subtasks/{subtask_id}
DeleteDELETE /subtasks/{subtask_id}

The completion PATCH takes { "completed": true } or { "completed": false } — a desired state, not a toggle, so retries are deterministic.

Comments

OperationEndpoint
List (cursor-paginated, oldest first)GET /cards/{card_id}/comments
CreatePOST /cards/{card_id}/comments
EditPATCH /comments/{comment_id}
DeleteDELETE /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 ETag you are holding can go stale because a teammate commented. When a card write fails with 412 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.