Errors and request IDs
Every error the API returns — from a malformed request to an internal failure — uses one envelope:
{
"error": {
"code": "validation_error",
"message": "The request contains invalid fields.",
"request_id": "7b9f4be2-b790-4d68-b534-c53d87f4e699",
"details": [
{
"field": "title",
"reason": "must not be empty"
}
]
}
}
codeis the stable, machine-readable value to branch on. Themessageis human-readable and may change; never parse it.request_idcorrelates the response with server-side logs.detailsis optional and appears mainly on validation errors, as a list of{ field, reason }items. It only ever contains safe validation metadata — no database messages, storage paths, or foreign identifiers.
The error codes
| Code | Typical status | Meaning |
|---|---|---|
invalid_request | 400 | Malformed JSON, unknown properties, bad query or header syntax (including a malformed If-Match or cursor) |
validation_error | 422 | Well-formed input that violates field constraints |
unauthorized | 401 | Missing, malformed, unknown, or revoked token |
insufficient_scope | 403 | Valid token without the required scope |
forbidden | 403 | The represented user lacks a management permission |
not_found | 404 | Absent, cross-workspace, or private resource — intentionally indistinguishable |
conflict | 409 | Semantic conflict with current resource state |
idempotency_conflict | 409 | Same Idempotency-Key reused with a different request body — see Idempotency |
file_not_ready | 409 | The file has not passed the readiness (clean) gate — see File attachments |
precondition_required | 428 | A version-guarded write without an If-Match header |
precondition_failed | 412 | The If-Match version is stale — see Optimistic concurrency |
rate_limited | 429 | Too many requests — see Rate limits |
cursor_expired | 410 | A pagination cursor older than 24 hours — see Pagination |
internal | 5xx | Unexpected server failure |
5xx responses are deliberately non-verbose: internal, a generic message,
and the request ID. Nothing about the underlying failure is exposed — the
request ID is how it gets investigated.
Request IDs
Every response — success or error — carries an X-Request-Id header, and
error envelopes repeat the value as error.request_id. If you send a valid
request ID of your own the API echoes it; otherwise one is generated.
When something behaves unexpectedly, log the request ID and include it in any support report. It is the exact correlation key for the server-side request log, so support can find your request without you sharing tokens or payloads.
All responses also carry Cache-Control: no-store: responses are private,
per-token views and must never be cached by intermediaries.
A robust error-handling pattern
Branch on the status class and error.code, keep the request ID, and only
retry what is retryable (429 after Retry-After, 5xx with backoff,
412 after re-reading the resource).
cURL (inspect status and body together):
curl -sS -w '\nHTTP %{http_code}\n' "$KANBAN_API_BASE_URL/boards" \
-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;
class KanbanError extends Error {
constructor(status, envelope) {
super(`${envelope.error.code}: ${envelope.error.message}`);
this.status = status;
this.code = envelope.error.code;
this.requestId = envelope.error.request_id;
this.details = envelope.error.details ?? [];
}
}
async function api(path, options = {}) {
const response = await fetch(`${baseUrl}${path}`, {
...options,
headers: { Authorization: `Bearer ${token}`, ...options.headers },
});
if (response.status === 204) {
return null;
}
const payload = await response.json();
if (!response.ok) {
throw new KanbanError(response.status, payload);
}
return payload;
}
api("/boards")
.then((payload) => console.log(payload.data.length, "boards"))
.catch((error) => {
if (error instanceof KanbanError) {
console.error(
`request ${error.requestId} failed with ${error.code} (${error.status})`
);
} else {
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"]
class KanbanError(Exception):
def __init__(self, status, envelope):
error = envelope["error"]
super().__init__(f"{error['code']}: {error['message']}")
self.status = status
self.code = error["code"]
self.request_id = error["request_id"]
self.details = error.get("details", [])
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}", **(headers or {})},
)
try:
with urllib.request.urlopen(request) as response:
if response.status == 204:
return None
return json.load(response)
except urllib.error.HTTPError as exc:
raise KanbanError(exc.code, json.load(exc)) from exc
try:
payload = api("/boards")
print(len(payload["data"]), "boards")
except KanbanError as error:
print(
f"request {error.request_id} failed "
f"with {error.code} ({error.status})"
)
raise SystemExit(1)