Skip to main content

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

OperationEndpoint
List attachmentsGET /cards/{card_id}/attachments
Attach a filePUT /cards/{card_id}/attachments/{file_id}
Detach a fileDELETE /cards/{card_id}/attachments/{file_id}
Read the coverGET /cards/{card_id}/cover
Set the coverPUT /cards/{card_id}/cover/{file_id}
Clear the coverDELETE /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:

  1. you have access to the card's board;
  2. 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;
  3. the file has passed FileTag's malware scanning and is in the clean state — otherwise the response is 409 with code file_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