Columns and ordering
Columns are the vertical lanes of a board. Every card lives in exactly one
column, and both columns and cards carry a server-computed position that
defines their canonical order.
The column representation is documented in the
API reference: id, type, version,
board_id, name, position, archived_at, and created_at.
Position is read-only
position is a number the server maintains; you never write it. To reorder,
call the move endpoint with identifiers, not numbers:
POST /columns/{column_id}/movetakes{ "before_column_id": "<uuid-or-null>" }— the column is placed immediately before the named column, or at the end of the board whennull.- Cards work the same way through
POST /cards/{card_id}/movewithcolumn_idandbefore_card_id.
The server computes the resulting position atomically, so two concurrent
moves can never interleave into a corrupted order.
Listing order
GET /boards/{board_id}/columns returns
active columns by default, ordered by (position ASC, id ASC) — exactly the
left-to-right order users see on the board. With archived=true, archived
columns are returned ordered by most recently archived first.
Creating, renaming, moving, archiving
POST /boards/{board_id}/columnscreates a column at the end of the board. Requires anIdempotency-Key.PATCH /columns/{column_id}renames a column ({ "name": "In review" }), guarded byIf-Match.POST /columns/{column_id}/movereorders, with bothIdempotency-KeyandIf-Matchrequired.POST /columns/{column_id}/archivearchives the column — with an important nuance below. Archive is a POST with no request body: sending any body, even{}, returns400 invalid_request.
Example — create a column and move it to the front of the board:
cURL:
BOARD_ID="<board-id>"
FIRST_COLUMN_ID="<current-first-column-id>"
curl -sS -i -X POST "$KANBAN_API_BASE_URL/boards/$BOARD_ID/columns" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: docs-create-column-001" \
-d '{"name": "Triage"}'
NEW_COLUMN_ID="<id-from-the-create-response>"
curl -sS -i -X POST "$KANBAN_API_BASE_URL/columns/$NEW_COLUMN_ID/move" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: docs-move-column-001" \
-H 'If-Match: "1"' \
-d "{\"before_column_id\": \"$FIRST_COLUMN_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 boardId = process.env.KANBAN_BOARD_ID;
async function main() {
const listResponse = await fetch(`${baseUrl}/boards/${boardId}/columns`, {
headers: { Authorization: `Bearer ${token}` },
});
const columns = (await listResponse.json()).data;
const createResponse = await fetch(`${baseUrl}/boards/${boardId}/columns`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": "docs-create-column-001",
},
body: JSON.stringify({ name: "Triage" }),
});
const created = (await createResponse.json()).data;
const etag = createResponse.headers.get("etag");
const moveResponse = await fetch(`${baseUrl}/columns/${created.id}/move`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": "docs-move-column-001",
"If-Match": etag,
},
body: JSON.stringify({ before_column_id: columns[0].id }),
});
const moved = (await moveResponse.json()).data;
console.log("column", moved.id, "now at position", moved.position);
}
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"]
BOARD_ID = os.environ["KANBAN_BOARD_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:
return response.headers, json.load(response)
_, columns = api(f"/boards/{BOARD_ID}/columns")
first_column_id = columns["data"][0]["id"]
created_headers, created = api(
f"/boards/{BOARD_ID}/columns",
method="POST",
body={"name": "Triage"},
headers={"Idempotency-Key": "docs-create-column-001"},
)
_, moved = api(
f"/columns/{created['data']['id']}/move",
method="POST",
body={"before_column_id": first_column_id},
headers={
"Idempotency-Key": "docs-move-column-001",
"If-Match": created_headers.get("ETag"),
},
)
print("column", moved["data"]["id"], "now at position", moved["data"]["position"])
Archiving a column does not archive its cards
POST /columns/{column_id}/archive sets
the column's archived_at and leaves every card in it untouched. Cards
keep their own independent archive state: they remain queryable through
GET /boards/{board_id}/cards and can be moved
out of the archived column by ID. If you want the cards archived too,
archive them individually.
There is no column restore or delete endpoint in v1; list archived columns
with archived=true to inspect them.