File attachments
Cards can carry file attachments and a cover image. Both point to files that
already exist in the FileTag workspace — the Kanban API never uploads
bytes. You attach a file by its canonical file_id; uploading and managing
the files themselves stays in FileTag's file surface.
Endpoints
| Operation | Endpoint |
|---|---|
| List attachments | GET /cards/{card_id}/attachments |
| Attach a file | PUT /cards/{card_id}/attachments/{file_id} |
| Detach a file | DELETE /cards/{card_id}/attachments/{file_id} |
| Read the cover | GET /cards/{card_id}/cover |
| Set the cover | PUT /cards/{card_id}/cover/{file_id} |
| Clear the cover | DELETE /cards/{card_id}/cover |
Attach/detach and cover set/clear are desired-state operations: repeating a
call that is already satisfied succeeds without changing anything, and none
of them require If-Match or increment the card version.
What you can attach
An attach or cover PUT succeeds only when all of these hold:
- you have access to the card's board;
- the file exists in the same workspace and is currently visible to you —
otherwise the response is
404 not_found, indistinguishable from a nonexistent file; - the file has passed FileTag's malware scanning and is in the clean
state — otherwise the response is
409with codefile_not_ready.
For covers, the file must additionally be an image.
Handling 409 file_not_ready
A freshly uploaded file may still be scanning. Until it is clean, attaching it returns:
{
"error": {
"code": "file_not_ready",
"message": "The file is not ready to be attached.",
"request_id": "7b9f4be2-b790-4d68-b534-c53d87f4e699"
}
}
The response intentionally does not say why (pending scan, quarantined, rejected — all look the same). Treat it as retryable with backoff for a bounded time; a file that stays not-ready should be surfaced to a human. See Attach an existing file for a complete retry pattern.
Visibility filtering on reads
GET /cards/{card_id}/attachments
returns only files still visible to you. If a file's permissions change
after it was attached, it silently drops out of your attachment list without
affecting the card — and reappears if visibility is restored. The cover
behaves the same way:
GET /cards/{card_id}/cover returns 204
when there is no cover or when the stored cover is not currently visible
to you.
Because visibility can change independently of the card, attachments and
covers are not part of the card's ETagged representation and never affect
the card version.
Attachment summaries contain card_id, file_id, name, mime_type,
size_bytes, and modified_at — no storage paths or download URLs.
Downloading content goes through FileTag's authenticated file surface, not
the Kanban API.
Detaching never deletes the file
DELETE /cards/{card_id}/attachments/{file_id}
removes the card-file relationship and returns 204 (even when the
relationship was already absent). The underlying file, its versions, and its
sharing are untouched. The same holds in the other direction: deleting a
card or a board never deletes attached files.
Example: attach, list, detach
cURL:
CARD_ID="<card-id>"
FILE_ID="<workspace-file-id>"
curl -sS -X PUT "$KANBAN_API_BASE_URL/cards/$CARD_ID/attachments/$FILE_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
curl -sS "$KANBAN_API_BASE_URL/cards/$CARD_ID/attachments" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
curl -sS -X DELETE "$KANBAN_API_BASE_URL/cards/$CARD_ID/attachments/$FILE_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 fileId = process.env.KANBAN_FILE_ID;
async function main() {
const attach = await fetch(
`${baseUrl}/cards/${cardId}/attachments/${fileId}`,
{ method: "PUT", headers: { Authorization: `Bearer ${token}` } }
);
const attached = await attach.json();
if (!attach.ok) {
throw new Error(
`${attached.error.code}: ${attached.error.message} (request ${attached.error.request_id})`
);
}
console.log("attached", attached.data.name);
const list = await fetch(`${baseUrl}/cards/${cardId}/attachments`, {
headers: { Authorization: `Bearer ${token}` },
});
const attachments = (await list.json()).data;
console.log(attachments.length, "attachment(s) visible");
const detach = await fetch(
`${baseUrl}/cards/${cardId}/attachments/${fileId}`,
{ method: "DELETE", headers: { Authorization: `Bearer ${token}` } }
);
console.log("detach status", detach.status); // 204
}
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"]
FILE_ID = os.environ["KANBAN_FILE_ID"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
attach = urllib.request.Request(
f"{BASE_URL}/cards/{CARD_ID}/attachments/{FILE_ID}",
method="PUT",
headers=HEADERS,
)
with urllib.request.urlopen(attach) as response:
attached = json.load(response)
print("attached", attached["data"]["name"])
list_request = urllib.request.Request(
f"{BASE_URL}/cards/{CARD_ID}/attachments", headers=HEADERS
)
with urllib.request.urlopen(list_request) as response:
attachments = json.load(response)["data"]
print(len(attachments), "attachment(s) visible")
detach = urllib.request.Request(
f"{BASE_URL}/cards/{CARD_ID}/attachments/{FILE_ID}",
method="DELETE",
headers=HEADERS,
)
with urllib.request.urlopen(detach) as response:
print("detach status", response.status) # 204