Skip to main content

Manage assignees, labels, and subtasks

This guide enriches a card with the three everyday collaboration writes: assign a teammate, apply a label, and build a checklist. Each uses a different API style, and using the right one makes your integration naturally retry-safe. Background: Collaboration resources.

ResourceStyleRetry safety
Assigneesdesired-state PUT/DELETErepeat freely; no headers needed
Applied labelsdesired-state PUT/DELETErepeat freely; no headers needed
Labels (palette)owned resourcePOST + Idempotency-Key
Subtasksowned resourcePOST + Idempotency-Key; PATCH/DELETE + If-Match

1. Assign a teammate (desired state)

Find the user ID in the workspace member directory, then PUT /cards/{card_id}/assignees/{user_id}. The PUT declares "this user is assigned": if they already are, it returns 200 with the same relationship and changes nothing. DELETE declares the opposite and returns 204 even when the user was not assigned.

2. Create and apply a label

Labels live on the board as a palette (POST /boards/{board_id}/labels, name + #RRGGBB color), and are applied to cards as a desired-state relationship (PUT /cards/{card_id}/labels/{label_id}). The card and label must belong to the same board — a cross-board apply is 422 validation_error.

3. Build a checklist (idempotent create)

POST /cards/{card_id}/subtasks appends a subtask to the card's checklist. Because a POST could double-create on retry, it requires an Idempotency-Key — replaying the same key + body returns the original subtask instead of adding a twin. PATCH /subtasks/{subtask_id} then sets completion ({ "completed": true } / { "completed": false }) — a desired state, not a toggle, so a retried PATCH cannot flip it back.

cURL

CARD_ID="<card-id>"
BOARD_ID="<board-id>"
USER_ID="<workspace-user-id>"

# 1. Assign a teammate: desired-state PUT, no body, no If-Match.
curl -sS -X PUT "$KANBAN_API_BASE_URL/cards/$CARD_ID/assignees/$USER_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"

# 2a. Create a label on the board.
curl -sS -X POST "$KANBAN_API_BASE_URL/boards/$BOARD_ID/labels" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: guide-create-label-001" \
-d '{"name": "Legal", "color": "#0a8357"}'

LABEL_ID="<label-id-from-the-response>"

# 2b. Apply it to the card: desired-state PUT.
curl -sS -X PUT "$KANBAN_API_BASE_URL/cards/$CARD_ID/labels/$LABEL_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"

# 3a. Create a subtask (idempotent POST).
curl -sS -i -X POST "$KANBAN_API_BASE_URL/cards/$CARD_ID/subtasks" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: guide-create-subtask-001" \
-d '{"title": "Read clause 9"}'

SUBTASK_ID="<subtask-id-from-the-response>"

# 3b. Complete it (If-Match from the subtask's ETag, e.g. "1").
curl -sS -X PATCH "$KANBAN_API_BASE_URL/subtasks/$SUBTASK_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "1"' \
-d '{"completed": true}'

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 boardId = process.env.KANBAN_BOARD_ID;
const userId = process.env.KANBAN_ASSIGNEE_ID;

async function api(path, options = {}) {
const response = await fetch(`${baseUrl}${path}`, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
const payload = response.status === 204 ? null : await response.json();
if (!response.ok) {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
return { response, payload };
}

async function main() {
// 1. Assign a teammate — safe to repeat.
const assignee = await api(`/cards/${cardId}/assignees/${userId}`, {
method: "PUT",
});
console.log("assigned:", assignee.payload.data.member.display_name);

// 2. Create a label and apply it to the card.
const label = await api(`/boards/${boardId}/labels`, {
method: "POST",
headers: { "Idempotency-Key": "guide-create-label-001" },
body: JSON.stringify({ name: "Legal", color: "#0a8357" }),
});
await api(`/cards/${cardId}/labels/${label.payload.data.id}`, {
method: "PUT",
});
console.log("labeled:", label.payload.data.name);

// 3. Create a checklist and complete the first item.
const titles = ["Read clause 9", "Confirm signatures", "File the copy"];
const subtasks = [];
for (const [index, title] of titles.entries()) {
const subtask = await api(`/cards/${cardId}/subtasks`, {
method: "POST",
headers: { "Idempotency-Key": `guide-create-subtask-00${index + 1}` },
body: JSON.stringify({ title }),
});
subtasks.push({
...subtask.payload.data,
etag: subtask.response.headers.get("etag"),
});
}

const first = subtasks[0];
const done = await api(`/subtasks/${first.id}`, {
method: "PATCH",
headers: { "If-Match": first.etag },
body: JSON.stringify({ completed: true }),
});
console.log(
"completed:",
done.payload.data.title,
"at",
done.payload.data.completed_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"]
BOARD_ID = os.environ["KANBAN_BOARD_ID"]
USER_ID = os.environ["KANBAN_ASSIGNEE_ID"]


def api(path, method="GET", body=None, headers=None):
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(
f"{BASE_URL}{path}",
data=data,
method=method,
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
**(headers or {}),
},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response) if response.status != 204 else None
return response.headers, payload


# 1. Assign a teammate — safe to repeat.
_, assignee = api(f"/cards/{CARD_ID}/assignees/{USER_ID}", method="PUT")
print("assigned:", assignee["data"]["member"]["display_name"])

# 2. Create a label and apply it to the card.
_, label = api(
f"/boards/{BOARD_ID}/labels",
method="POST",
body={"name": "Legal", "color": "#0a8357"},
headers={"Idempotency-Key": "guide-create-label-001"},
)
api(f"/cards/{CARD_ID}/labels/{label['data']['id']}", method="PUT")
print("labeled:", label["data"]["name"])

# 3. Create a checklist and complete the first item.
titles = ["Read clause 9", "Confirm signatures", "File the copy"]
subtasks = []
for index, title in enumerate(titles, start=1):
subtask_headers, subtask = api(
f"/cards/{CARD_ID}/subtasks",
method="POST",
body={"title": title},
headers={"Idempotency-Key": f"guide-create-subtask-00{index}"},
)
subtasks.append((subtask["data"], subtask_headers.get("ETag")))

first, first_etag = subtasks[0]
_, done = api(
f"/subtasks/{first['id']}",
method="PATCH",
body={"completed": True},
headers={"If-Match": first_etag},
)
print("completed:", done["data"]["title"], "at", done["data"]["completed_at"])

What this does to the card

Each effective change above — a new assignee, an applied label, a created or completed subtask — increments the parent card's version, and the card's assignee_user_ids, label_ids, subtask_total, and subtask_completed reflect it on the next read. Desired-state calls that change nothing (e.g. re-assigning the same user) bump nothing. If you hold a card ETag across these writes, expect it to be stale afterwards — see Optimistic concurrency.

Failure modes to expect: 404 not_found when the target user is not a visible workspace member or lacks board access; 422 validation_error for a cross-board label; 409 idempotency_conflict when an Idempotency-Key is reused with a different body.