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.
| Resource | Style | Retry safety |
|---|---|---|
| Assignees | desired-state PUT/DELETE | repeat freely; no headers needed |
| Applied labels | desired-state PUT/DELETE | repeat freely; no headers needed |
| Labels (palette) | owned resource | POST + Idempotency-Key |
| Subtasks | owned resource | POST + 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.