Skip to main content

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"
}
]
}
}
  • code is the stable, machine-readable value to branch on. The message is human-readable and may change; never parse it.
  • request_id correlates the response with server-side logs.
  • details is 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

CodeTypical statusMeaning
invalid_request400Malformed JSON, unknown properties, bad query or header syntax (including a malformed If-Match or cursor)
validation_error422Well-formed input that violates field constraints
unauthorized401Missing, malformed, unknown, or revoked token
insufficient_scope403Valid token without the required scope
forbidden403The represented user lacks a management permission
not_found404Absent, cross-workspace, or private resource — intentionally indistinguishable
conflict409Semantic conflict with current resource state
idempotency_conflict409Same Idempotency-Key reused with a different request body — see Idempotency
file_not_ready409The file has not passed the readiness (clean) gate — see File attachments
precondition_required428A version-guarded write without an If-Match header
precondition_failed412The If-Match version is stale — see Optimistic concurrency
rate_limited429Too many requests — see Rate limits
cursor_expired410A pagination cursor older than 24 hours — see Pagination
internal5xxUnexpected 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)