Errors
Canonical error shape, status codes, and what your client should do for each.
Every error from the bichito API uses the same JSON shape:
{
"detail": "Human-readable message"
}
Validation errors (Pydantic) come with a list:
{
"detail": [
{ "loc": ["body", "title"], "msg": "field required", "type": "value_error.missing" }
]
}
The HTTP status code is the source of truth for what category the error falls in.
Status codes
400 Bad Request
Your input is well-formed but logically invalid for the operation. Example: a bulk op with no ids, a label color that isn't #RRGGBB, a duplicate-of pointing across projects.
The detail is human-readable and safe to surface to your user.
401 Unauthorized
No auth header, or auth header invalid. Either:
- You forgot to send
Authorization: Bearer …/X-API-Key/X-MCP-Token. - The token expired (JWT after 30 days), was revoked (API key, MCP token), or never existed.
Recovery: refresh the credential and retry.
403 Forbidden
You're authenticated, but not allowed to do this. The two flavours:
- Email not verified — the user hasn't completed email verification yet. Surface "verify your email" UI.
- Scope missing (MCP routes) — the MCP token doesn't carry the required scope. Re-mint with the right scope.
404 Not Found
The resource doesn't exist for you. We deliberately mask "exists but you don't own it" as 404 to avoid leaking IDs across teams / users. So if you're sure the id is correct and you still get 404, it's most likely an auth context mismatch (wrong JWT user, MCP token under a different account).
409 Conflict
A uniqueness constraint failed. Examples: creating a label with a name that already exists in the team, creating a saved view with a duplicate name. The detail message names the conflicting field.
422 Unprocessable Entity
Pydantic validation failed: missing fields, wrong types, enum mismatches, out-of-range numbers.
Response body is the structured detail array shown above. Clients with a typed schema should never hit this; it's a sign of a contract mismatch.
429 Too Many Requests
You've hit a rate limit. The response carries:
detailwith which limit fired.Retry-Afterheader (seconds).
See Rate limits for the limits per endpoint and the recommended backoff strategy.
500 / 502 / 503
Something on our side broke. Retry with backoff — the request was almost certainly safe to retry (we don't have non-idempotent state side effects on most endpoints). If 5xx persists, the status page (planned) or the GitHub issues are the places to check.
Error handling pattern
async function call<T>(req: () => Promise<Response>): Promise<T> {
const res = await req();
if (res.status === 429) {
const retry = Number(res.headers.get("retry-after") ?? "60");
await new Promise((r) => setTimeout(r, retry * 1000 + Math.random() * 1000));
return call(req);
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(`${res.status} ${body.detail ?? res.statusText}`);
}
return res.json();
}
This handles 429 backoff and surfaces a sane error message for everything else. Most clients only need this much.
Logging on our side
Server-side errors include a request id you can grep us for if you need help diagnosing. The id is in the X-Request-Id response header. Keep it on hand if you ever open a support thread — it makes log lookups instant.