Skip to main content

Errors

Errors returned by Docana’s API handlers use a JSON object with an error message and optional structured fields. A proxy, CDN, or unavailable service can return HTML or plain text instead. Check the HTTP status and content type before parsing the response:

{
"error": "Invalid body",
"code": "Optional machine-readable code, e.g. auth/credentials-missing",
"details": "Optional extra context, e.g. the offending field or id",
"validationErrors": {
"formErrors": [],
"fieldErrors": {
"name": ["String must contain at least 1 character(s)"]
}
}
}

One endpoint predates this envelope: document upload (POST /collections/{id}/documents/ and the one-time upload URL) wraps its errors as { "success": false, "error": ..., "code": ... }. The error and code fields mean the same thing there. Only the extra success flag differs.

validationErrors appears only on 400 responses caused by request validation. It is the flattened output of the schema check, with one entry per invalid field.

code appears on errors raised before your request reaches an operation: missing or rejected credentials (auth/*), a retired versionless path (api/versionless-retired), a path that matches no operation (api/unknown-path), and rate limiting (rate-limit/exceeded). It also appears on the errors a client has to branch on: plan limits (budget-exceeded, quota-exceeded-<quota>) and upload size problems (upload/*). Branch on code, never on the error sentence, which is written for people and can change.

Status codes

CodeMeaning
200 / 201It worked.
400The body, query, or path failed validation, or the body is not valid JSON. Check validationErrors.
401Your key is missing, expired, or malformed. Check that you used the ApiKey scheme, not Bearer. A Bearer token from docana login lives for one hour. See Access token lifetime.
402Your plan is out of budget or over a quota. Resolve the budget or quota condition before retrying. See Plan limits.
403Your key works but its scope does not cover this endpoint. Cloudflare also answers 403 (error 1010) to requests without a User-Agent header. Send one.
404The resource does not exist, or it belongs to a different application than your key. A path that matches no operation at all also returns 404, with code: "api/unknown-path" and a pointer at the OpenAPI spec.
409The request conflicts with current state, for example creating something that already exists.
413An upload is too large. code says which ceiling you hit. See Upload size limits.
429You sent too many requests. Check the Retry-After header for how many seconds to wait. 429s issued at the platform edge also carry code: "rate-limit/exceeded". Wait before retrying. For writes, follow the endpoint’s idempotency contract.
500 / 502 / 503 / 504The service could not complete the request. Retry reads with bounded backoff. Before retrying a write, check whether it already took effect.

Rate limits

Requests are rate limited by client IP at the platform edge, so callers behind a shared egress IP (NAT, CI runners, PaaS) share one bucket. When you cross the limit, the API answers 429 with the JSON envelope above and a Retry-After header. Back off for that many seconds and retry.

You do not have to wait for the 429 to know where you stand. Responses that pass through the platform rate limiter carry RateLimit headers, so you can self-throttle in real time:

HeaderMeaning
RateLimit-LimitRequests allowed per window.
RateLimit-RemainingRequests left in the current window.
RateLimit-ResetSeconds until the window resets.
RateLimit-PolicyThe whole policy in one line, e.g. 5000;w=60 for 5000 requests per 60 seconds.

Plan limits

Every metered operation (document ingestion, search, assistant messages, insights) checks the company plan first. When the monthly budget is spent or a quota is reached, the API answers 402:

{
"error": "Budget exceeded. Please upgrade your plan or contact support.",
"code": "budget-exceeded"
}

code is budget-exceeded for the monthly budget and quota-exceeded-<quota> for a plan quota, for example quota-exceeded-numberOfDocumentPages. Neither is transient. A client that treats 402 like 429 and retries with backoff wastes a request per file and never gets through. Stop the job instead. A 402 is also not a failure of the request itself: the file was fine, and uploading it again after the budget is raised or the month rolls over works.

You do not have to hit the wall to find out where it is. GET /api/v1/usage/ returns the month's budget, what is used, what remains, and the metered quantities behind it, as JSON. Read it before starting a large job and size the job against budget.remaining. Ingestion is metered per processed page and per media minute, not per file, so cost tracks pages and images rather than file count. Creating collections and folders is not metered and keeps working after a 402.

Upload size limits

Document uploads have two ceilings, and the code on a 413 tells you which one you hit:

CodeMeaning
upload/file-too-largeThe file is over the company plan's document size limit (maxFileSizeInMBytes). It cannot be ingested on this plan, chunked or not.
upload/request-too-largeThe request body is over what one request may carry through the infrastructure in front of the platform (maxRequestSizeInMBytes, 100 MB on the hosted platform). The file itself is fine: send it through the chunked upload protocol. The body also carries useChunkedUpload: true.

The edge in front of the hosted platform enforces its 100 MB ceiling itself, so a single request above it can come back as an HTML 413 from Cloudflare rather than the JSON above. Use chunks for the request-size ceiling. A file above the company plan’s file-size limit still fails when chunked. Both ceilings are reported by createDocumentUploadUrl so you can pick the path before sending a byte.

A multipart body the server cannot parse answers 400 with code: "upload/malformed-multipart". The usual cause is a filename with a double quote inside Content-Disposition: form-data; filename="...", which ends the header value early. Escape the quote or use RFC 5987 encoding.

Access token lifetime

Two credentials reach the API. API keys expire at the date selected when they are created, unless you choose Never. Revoking a key invalidates it immediately. The OAuth access token that docana login stores in ~/.docana/config.json lives for one hour. The refresh token next to it lives for 30 days. The CLI refreshes the access token on its own, but a script that reads accessToken from that file once and runs longer than an hour starts getting 401 with code: "auth/unauthorized" partway through. Either use an API key for scripts, or refresh the token before it expires: POST /api/mcp/oauth/token/ with grant_type=refresh_token and the stored refreshToken, as the CLI does.

Retry without duplicating work

Read operations can usually be retried after a transient failure. Use a limited number of attempts with backoff, and respect Retry-After when it is present.

For writes, a timeout does not prove that the operation failed. An execution may already be running or a document may already have been accepted. Check the resource or execution status before submitting another request. Use Idempotency-Key only where the endpoint explicitly supports it, such as thread archiving, and reuse the same key and body for the same logical operation.

When reporting an error, include the endpoint, HTTP status, machine-readable code, and time of the request. Remove API keys and sensitive request content from logs you share.