Skip to main content

File attachments

Cards can carry file attachments and a cover image. Both point to files that already exist in the Broome workspace — the Kanban API never uploads bytes. You attach a file by its canonical file_id; uploading and managing the files themselves stays in Broome'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 is in an attachable trust state — otherwise the response is 409 with code file_not_ready.

Hostile-file scanning is disabled in production today, with no schedule to turn it back on. While it is off, "attachable" does not mean scanned clean — it means the file finished ingesting through its authorized upload path and was not independently flagged. Concretely, attaching succeeds for a file whose trust state is clean (scanned clean, from before scanning was disabled or a future re-enable), legacy (predates the trust system), or missing (the common case today: published directly, with no scan run and no clean verdict manufactured for it — see ADR-003). file_not_ready still applies to a file that is pending, rejected, or quarantined, which can happen independently of the scanner. If scanning is re-enabled, the same endpoint additionally requires a scanner-produced clean verdict before attachment succeeds.

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 Broome'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.broometech.com/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.broometech.com/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