Getting started
This page walks you from zero to a moved card: create a token, verify it, list boards, create a board, then create and move a card. Every step is copy-paste ready in cURL, JavaScript, and Python.
All examples read two environment variables so no credential ever appears in your code or shell history:
export KANBAN_API_BASE_URL="https://app.filetag.ai/api/kanban/v1"
export KANBAN_API_TOKEN="<your-personal-access-token>"
Use https://staging.filetag.ai/api/kanban/v1 as the base URL if you were
given access to the staging environment instead.
1. Create a scoped Personal Access Token
The API authenticates with a workspace-scoped Personal Access Token (PAT).
Create one in the FileTag web app under Settings → API tokens: choose the
scopes the integration needs (for this walkthrough you need kanban:write,
which includes read access) and copy the token when it is shown — it is
displayed exactly once and only a hash is stored server-side.
Read Authentication for the token model and Scopes for what each scope allows. During the controlled beta, token issuance for Kanban scopes is limited to enrolled workspaces; see Changelog and deprecation policy.
2. Verify the token with GET /account
getAccount returns the represented user, the
workspace the token is bound to, and the effective scopes — a cheap way to
confirm the token works before doing anything else.
cURL:
curl -sS "$KANBAN_API_BASE_URL/account" \
-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;
async function main() {
const response = await fetch(`${baseUrl}/account`, {
headers: { Authorization: `Bearer ${token}` },
});
const payload = await response.json();
if (!response.ok) {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
console.log(payload.data);
}
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"]
request = urllib.request.Request(
f"{BASE_URL}/account",
headers={"Authorization": f"Bearer {TOKEN}"},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response)
print(payload["data"])
A successful response looks like:
{
"data": {
"user": { "id": "5b1e4d9a-6a7e-4b39-a9c1-2f47a1c05a10" },
"workspace": {
"id": "0f2a7c44-9f13-4a8e-8f5f-64c1a9d2b3c4",
"slug": "acme",
"name": "Acme Inc"
},
"scopes": ["kanban:write"]
}
}
3. List boards
listBoards returns the boards visible to the
represented user, cursor-paginated. See
Pagination for how cursors work.
cURL:
curl -sS "$KANBAN_API_BASE_URL/boards?limit=50" \
-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;
async function main() {
const response = await fetch(`${baseUrl}/boards?limit=50`, {
headers: { Authorization: `Bearer ${token}` },
});
const payload = await response.json();
if (!response.ok) {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
for (const board of payload.data) {
console.log(board.id, board.name);
}
}
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"]
request = urllib.request.Request(
f"{BASE_URL}/boards?limit=50",
headers={"Authorization": f"Bearer {TOKEN}"},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response)
for board in payload["data"]:
print(board["id"], board["name"])
4. Create a board with an Idempotency-Key
Every mutating POST requires an Idempotency-Key header so a retried request
can never create a duplicate. Pick any unique string (1–128 printable ASCII
characters) per logical operation and reuse it on retries. See
Idempotency.
createBoard creates the board and its
default columns in one transaction and returns 201 with a Location header
and an ETag.
cURL:
curl -sS -i -X POST "$KANBAN_API_BASE_URL/boards" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: getting-started-create-board-001" \
-d '{"name": "Launch plan", "description": "Tasks for the launch"}'
JavaScript:
const baseUrl =
process.env.KANBAN_API_BASE_URL ?? "https://app.filetag.ai/api/kanban/v1";
const token = process.env.KANBAN_API_TOKEN;
async function main() {
const response = await fetch(`${baseUrl}/boards`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": "getting-started-create-board-001",
},
body: JSON.stringify({
name: "Launch plan",
description: "Tasks for the launch",
}),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
console.log("created board", payload.data.id);
console.log("etag", 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"]
body = json.dumps(
{"name": "Launch plan", "description": "Tasks for the launch"}
).encode("utf-8")
request = urllib.request.Request(
f"{BASE_URL}/boards",
data=body,
method="POST",
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
"Idempotency-Key": "getting-started-create-board-001",
},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response)
print("created board", payload["data"]["id"])
print("etag", response.headers.get("ETag"))
If the request times out, send it again with the same Idempotency-Key
and the same body: you get the original 201 back, not a second board.
5. Create and move a card with If-Match
New boards come with default columns. List them with
listColumns to get a column_id, create a
card in it with createCard, then move the
card with moveCard.
Moves are version-guarded: read the card's version from the ETag response
header (for example ETag: "1") and send it back verbatim in If-Match.
See Optimistic concurrency.
cURL (replace the placeholder IDs with values from the previous responses):
BOARD_ID="<board-id-from-step-4>"
curl -sS "$KANBAN_API_BASE_URL/boards/$BOARD_ID/columns" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
COLUMN_ID="<first-column-id>"
curl -sS -i -X POST "$KANBAN_API_BASE_URL/columns/$COLUMN_ID/cards" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: getting-started-create-card-001" \
-d '{"title": "Review contract"}'
CARD_ID="<card-id-from-the-create-response>"
TARGET_COLUMN_ID="<another-column-id>"
curl -sS -i -X POST "$KANBAN_API_BASE_URL/cards/$CARD_ID/move" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: getting-started-move-card-001" \
-H 'If-Match: "1"' \
-d "{\"column_id\": \"$TARGET_COLUMN_ID\", \"before_card_id\": null}"
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; // from step 4
async function api(path, options = {}) {
const response = await fetch(`${baseUrl}${path}`, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (response.status === 204) {
return { response, payload: null };
}
const payload = 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() {
const columns = await api(`/boards/${boardId}/columns`);
const [firstColumn, secondColumn] = columns.payload.data;
const created = await api(`/columns/${firstColumn.id}/cards`, {
method: "POST",
headers: { "Idempotency-Key": "getting-started-create-card-001" },
body: JSON.stringify({ title: "Review contract" }),
});
const card = created.payload.data;
const etag = created.response.headers.get("etag");
console.log("created card", card.id, "version", etag);
const moved = await api(`/cards/${card.id}/move`, {
method: "POST",
headers: {
"Idempotency-Key": "getting-started-move-card-001",
"If-Match": etag,
},
body: JSON.stringify({ column_id: secondColumn.id, before_card_id: null }),
});
console.log("card now in column", moved.payload.data.column_id);
}
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"] # from step 4
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
_, columns = api(f"/boards/{BOARD_ID}/columns")
first_column, second_column = columns["data"][0], columns["data"][1]
created_headers, created = api(
f"/columns/{first_column['id']}/cards",
method="POST",
body={"title": "Review contract"},
headers={"Idempotency-Key": "getting-started-create-card-001"},
)
card = created["data"]
etag = created_headers.get("ETag")
print("created card", card["id"], "version", etag)
_, moved = api(
f"/cards/{card['id']}/move",
method="POST",
body={"column_id": second_column["id"], "before_card_id": None},
headers={
"Idempotency-Key": "getting-started-move-card-001",
"If-Match": etag,
},
)
print("card now in column", moved["data"]["column_id"])
before_card_id: null means "place at the end of the target column"; to
place the card before a specific card, pass that card's ID instead. See
Move cards safely.
6. Handle a representative error
Every error response uses the same envelope and carries a request ID. If you
now retry the move with the version from before the move (the card's version
was incremented by the move itself), the API rejects it with
412 precondition_failed:
{
"error": {
"code": "precondition_failed",
"message": "The resource version does not match the supplied If-Match value.",
"request_id": "7b9f4be2-b790-4d68-b534-c53d87f4e699"
}
}
The fix is always the same: re-read the resource, take the fresh ETag, and
retry. Here is the pattern in all three languages.
cURL:
CARD_ID="<card-id>"
# Re-read the card to get the current version from the ETag header.
curl -sS -i "$KANBAN_API_BASE_URL/cards/$CARD_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
# Retry the mutation with the fresh value, e.g. 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 cardId = process.env.KANBAN_CARD_ID;
const targetColumnId = process.env.KANBAN_TARGET_COLUMN_ID;
async function moveWithRetry() {
for (let attempt = 0; attempt < 3; attempt += 1) {
const read = await fetch(`${baseUrl}/cards/${cardId}`, {
headers: { Authorization: `Bearer ${token}` },
});
const etag = read.headers.get("etag");
const move = await fetch(`${baseUrl}/cards/${cardId}/move`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": `move-${cardId}-attempt-${attempt}`,
"If-Match": etag,
},
body: JSON.stringify({ column_id: targetColumnId, before_card_id: null }),
});
if (move.ok) {
return move.json();
}
const payload = await move.json();
if (payload.error.code !== "precondition_failed") {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
// Someone changed the card between our read and our move; loop and retry.
}
throw new Error("card kept changing; giving up after 3 attempts");
}
moveWithRetry()
.then((result) => console.log(result.data))
.catch((error) => {
console.error(error);
process.exit(1);
});
Python:
import json
import os
import urllib.error
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"]
TARGET_COLUMN_ID = os.environ["KANBAN_TARGET_COLUMN_ID"]
def move_with_retry():
for attempt in range(3):
read = urllib.request.Request(
f"{BASE_URL}/cards/{CARD_ID}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
with urllib.request.urlopen(read) as response:
etag = response.headers.get("ETag")
move = urllib.request.Request(
f"{BASE_URL}/cards/{CARD_ID}/move",
data=json.dumps(
{"column_id": TARGET_COLUMN_ID, "before_card_id": None}
).encode("utf-8"),
method="POST",
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
"Idempotency-Key": f"move-{CARD_ID}-attempt-{attempt}",
"If-Match": etag,
},
)
try:
with urllib.request.urlopen(move) as response:
return json.load(response)["data"]
except urllib.error.HTTPError as exc:
error = json.load(exc)["error"]
if error["code"] != "precondition_failed":
raise RuntimeError(
f"{error['code']}: {error['message']} "
f"(request {error['request_id']})"
) from exc
# Someone changed the card between our read and our move; retry.
raise RuntimeError("card kept changing; giving up after 3 attempts")
print(move_with_retry())
When you contact support about an unexpected response, include the
request_id from the envelope (also present in the X-Request-Id response
header) — it is the correlation key for server-side logs. See
Errors and request IDs.
Next steps
- Authentication and Scopes for the full token model.
- The core concepts behind boards, columns, cards, and collaboration.
- The integration guides for complete end-to-end flows.
- The API reference for every endpoint, schema, and status code.