Comment and mention users
Comments are how integrations talk to the humans on a board — status updates, bot reports, review requests. This guide covers creating comments, mentioning users so they get notified, and the author-only edit rules.
Endpoints:
GET /cards/{card_id}/comments (cursor-
paginated, oldest first),
POST /cards/{card_id}/comments,
PATCH /comments/{comment_id},
DELETE /comments/{comment_id}.
Mention syntax
A mention embeds the user's display text and user ID in the comment body:
@[Ana Souza](87fd9b73-0ff6-47f3-a680-0ca0a03b3e07)
Resolve display names and user IDs from the workspace member directory or the board member list.
Who gets notified:
- Mention notifications go to authorized board members only. Mentioning a user who cannot access the board never notifies them and never leaks board content; the text simply stays text.
- On edit, only newly introduced mentions notify. Users already mentioned in the previous version of the body are not re-notified when you update the comment.
Creating a comment
Comment creation is a mutating POST, so it requires an
Idempotency-Key — a retried create posts
one comment, not two. The body is 1–20,000 characters.
cURL:
CARD_ID="<card-id>"
MENTION_USER_ID="<board-member-user-id>"
curl -sS -i -X POST "$KANBAN_API_BASE_URL/cards/$CARD_ID/comments" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: guide-create-comment-001" \
-d "{\"body\": \"Review is ready for @[Ana Souza]($MENTION_USER_ID)\"}"
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;
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() {
// Resolve the teammate to mention from the member directory.
const members = await api("/members");
const ana = members.payload.data.find(
(member) => member.display_name === "Ana Souza"
);
if (!ana) {
throw new Error("no workspace member named Ana Souza");
}
const mention = `@[${ana.display_name}](${ana.user_id})`;
const comment = await api(`/cards/${cardId}/comments`, {
method: "POST",
headers: { "Idempotency-Key": "guide-create-comment-001" },
body: JSON.stringify({ body: `Review is ready for ${mention}` }),
});
console.log(
"comment",
comment.payload.data.id,
"version",
comment.response.headers.get("etag")
);
}
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"]
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
# Resolve the teammate to mention from the member directory.
_, members = api("/members")
ana = next(
(m for m in members["data"] if m["display_name"] == "Ana Souza"),
None,
)
if ana is None:
raise SystemExit("no workspace member named Ana Souza")
mention = f"@[{ana['display_name']}]({ana['user_id']})"
comment_headers, comment = api(
f"/cards/{CARD_ID}/comments",
method="POST",
body={"body": f"Review is ready for {mention}"},
headers={"Idempotency-Key": "guide-create-comment-001"},
)
print("comment", comment["data"]["id"], "version", comment_headers.get("ETag"))
Editing and deleting: author-only
You can only edit or delete comments your token's user wrote — no scope
overrides this, not even kanban:admin. Attempting to edit someone else's
comment fails. Both operations are version-guarded: send the comment's
current version in If-Match (comments return an ETag on creation; a
fresh version is also in each comment's version field when listing).
cURL:
COMMENT_ID="<comment-id>"
# Edit your comment (adds a NEW mention -> only that user is notified).
curl -sS -X PATCH "$KANBAN_API_BASE_URL/comments/$COMMENT_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "1"' \
-d '{"body": "Review is ready — see the updated checklist"}'
# Delete your comment.
curl -sS -X DELETE "$KANBAN_API_BASE_URL/comments/$COMMENT_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H 'If-Match: "2"'
JavaScript:
const baseUrl =
process.env.KANBAN_API_BASE_URL ?? "https://app.filetag.ai/api/kanban/v1";
const token = process.env.KANBAN_API_TOKEN;
const commentId = process.env.KANBAN_COMMENT_ID;
async function main() {
const response = await fetch(`${baseUrl}/comments/${commentId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"If-Match": '"1"',
},
body: JSON.stringify({
body: "Review is ready — see the updated checklist",
}),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
console.log("updated at", payload.data.updated_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"]
COMMENT_ID = os.environ["KANBAN_COMMENT_ID"]
request = urllib.request.Request(
f"{BASE_URL}/comments/{COMMENT_ID}",
data=json.dumps(
{"body": "Review is ready — see the updated checklist"}
).encode("utf-8"),
method="PATCH",
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
"If-Match": '"1"',
},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response)
print("updated at", payload["data"]["updated_at"])
Reading the thread
List comments with
GET /cards/{card_id}/comments — they come
oldest-first, cursor-paginated like other collections
(Pagination). Each comment carries its
author.user_id; join it against the member directory for display names.
The parent card's comment_count (and version) update with every created
or deleted comment, so a card read tells you whether there is anything new
before you fetch the thread.