Errors
Constaia API error format, every error code by HTTP status, the JavaScript and PHP SDK error classes and which errors are safe to retry.
When a request fails, the API responds with a non-2xx HTTP status and a JSON body like this:
{
"error": {
"type": "invalid_request",
"code": "invalid_parameter",
"message": "Tipo de documento desconocido. Consulta GET /v1/document-types.",
"param": "options.expect",
"request_id": "req_01J9Z8Q3K4M5N6P7Q8R9S0T1V2"
}
}| Field | Description |
|---|---|
type | Error category. There are seven, see the table below. |
code | Stable, specific code. Write your logic against code, not message. |
message | Explanation for people (in Spanish). Wording may change. |
param | Optional. The parameter or header involved: file, options.expect, options.checks.max_age_days, Idempotency-Key… |
request_id | Request identifier. Same value as the X-Request-Id header. |
A failed analysis is not an HTTP error
If the file is accepted but the analysis cannot be completed, the response is an analysis with status: "failed" and
error: { code, message } (for example processing_failed), not an HTTP error. Those analyses are not charged. For
async analyses you get the analysis.failed webhook.
Types and HTTP statuses
| HTTP | type | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed request. |
| 401 | authentication | API key missing or invalid. |
| 402 | insufficient_credits | No credits left in live mode, or the monthly spend cap would be exceeded. |
| 403 | permission | The key cannot perform that action. |
| 404 | not_found | The resource or route does not exist. |
| 409 | invalid_request | State conflict (idempotency in progress, analysis not finished). |
| 413 | invalid_request | File too large. |
| 415 | invalid_request | Unsupported file or content type. |
| 422 | invalid_request | Parameters well-formed but invalid, or unreadable document. |
| 429 | rate_limited | Too many requests per second, concurrent analyses or pages per minute. |
| 500 | api_error | Constaia internal error. |
| 501 | invalid_request | Feature not available yet. |
| 503 | api_error | Service temporarily unavailable or live mode unavailable. |
Every code
| HTTP | code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_json | The body is not valid JSON. | Check serialisation and Content-Type: application/json. |
| 400 | invalid_multipart | The multipart/form-data body could not be read. | Let your HTTP client generate the boundary; don't set Content-Type by hand. |
| 400 | invalid_options | The multipart options field is not valid JSON. | Send options as a JSON string. |
| 400 | missing_file | No file: file, file_url or file_base64 is missing (or a batch item has none). | Add the file. |
| 400 | empty_file | The file is empty. | Make sure you read the file correctly before sending it. |
| 400 | invalid_base64 | file_base64 is not valid base64. | Use standard base64 (a data:…;base64, prefix is tolerated). |
| 400 | invalid_file_url | file_url is not a valid URL, is not https, or points to a disallowed address (private IP). | Use a public https URL. |
| 400 | batch_too_large | The batch has more than 100 documents. | Split into batches of up to 100. |
| 400 | empty_batch | The batch has no documents. | Add at least one. |
| 400 | invalid_url | A webhook endpoint URL is not https. | Use https. |
| 400 | invalid_idempotency_key | Idempotency-Key is longer than 255 characters. | Shorten the key. |
| 401 | missing_api_key | Authorization: Bearer <key> is missing. | Add the header. |
| 401 | invalid_api_key | Key malformed, unknown or revoked. | Check the environment variable and the key in the dashboard. |
| 402 | insufficient_credits | Not enough credits for this analysis (or, for a batch, for every document). | Buy a pack or wait for the monthly renewal. See credits. |
| 402 | monthly_cap_reached | The analysis would exceed the account's monthly spend cap (monthly_credit_cap). Not charged. | Raise the cap or wait until the 1st (UTC). Don't retry. See monthly spend cap. |
| 402 | email_not_verified | You are trying to spend free credits in live mode and nobody in the account has verified their email. | Verify your email from the link we sent you (or resend it from the dashboard). |
| 403 | forbidden | You are not allowed to perform this action. | Check the key and the account. (In the dashboard, creating live keys without a verified email gives email_not_verified.) |
| 404 | resource_missing | The resource doesn't exist, belongs to another account, was deleted or was created with keep_results: false. | Check the id. |
| 404 | route_not_found | The route doesn't exist. | Check method and URL (/v1/…). |
| 409 | idempotency_in_progress | The original request with that Idempotency-Key is still running. | Retry after a few seconds. See idempotency. |
| 409 | analysis_not_completed | You asked for the export of an unfinished analysis. | Wait for completed (webhook or GET /v1/analyses/{id}). |
| 413 | file_too_large | The file exceeds 20 MB. | Compress or reduce the resolution. |
| 415 | unsupported_file_type | The content is not JPEG, PNG, WEBP, HEIC or PDF (detected from the bytes, not the extension). | Convert the file. |
| 415 | unsupported_content_type | The request is neither multipart/form-data nor application/json. | Use one of them. |
| 422 | invalid_parameter | A parameter is invalid: unknown type in expect, unknown option, out-of-range value. param tells you which. | Fix the parameter. See checks. |
| 422 | idempotency_key_reused | That Idempotency-Key was already used with a different request. | Use a new key per operation. |
| 422 | too_many_pages | The PDF has more than 30 pages (sync) or 200 (async or batches). | Use async: true or split the PDF. |
| 422 | unreadable_image | The image could not be read. | Ask for another photo. |
| 422 | unreadable_pdf | The PDF could not be read (damaged or protected). | Ask for another file. |
| 422 | file_url_unreachable | file_url could not be downloaded (network error, non-2xx response or 15 s timeout). Over 20 MB you get 413 file_too_large. | Make sure the URL is public and fast, or upload the file. |
| 422 | processing_unavailable | The requested processing profile is not available. | Use the other profile or don't send processing. See data residency. |
| 429 | rate_limited | You exceeded your key's requests per second. | Wait for Retry-After and retry. See rate limits. |
| 429 | concurrency_limit | Your account already has its plan's maximum of concurrent synchronous analyses running (live mode only). | Wait for them to finish, or use async: true or batches. |
| 429 | pages_rate_limited | You exceeded your plan's pages per minute (live mode only). | Wait for Retry-After. |
| 500 | internal_error | Unexpected Constaia error. | Retry with the same Idempotency-Key. If it persists, contact support with the request_id. |
| 501 | not_implemented | The feature doesn't exist yet (today, POST /v1/verification-links). | Don't retry. |
| 503 | live_mode_unavailable | The environment has no real AI providers configured; live keys never get simulated results. | Use a ck_test_ key meanwhile. Don't retry it in a loop. |
| 503 | billing_unavailable | Pack checkout is unavailable (dashboard only). | Try again later. |
X-Request-Id
Every response, successful or not, carries the X-Request-Id: req_… header. On errors it also comes in
error.request_id. Log it: it is the first thing we will ask for if you contact support
(hola@constaia.com).
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
X-Request-Id: req_01J9Z8Q3K4M5N6P7Q8R9S0T1V2SDK error classes
Every JavaScript SDK error extends ConstaiaError and exposes status, type, code, param, requestId,
headers and raw. PHP SDK errors extend Constaia\Exception\ConstaiaException and expose getHttpStatus(),
getType(), getErrorCode(), getParam() and getRequestId(). Python SDK errors extend constaia.ConstaiaError
and expose .message, .status, .type, .code, .param and .request_id.
| Case | JavaScript (@constaia/sdk) | PHP (Constaia\Exception\…) | Python (constaia) |
|---|---|---|---|
| 400, 409, 413, 415, 422 | InvalidRequestError | InvalidRequestException | InvalidRequestError |
| 401 | AuthenticationError | AuthenticationException | AuthenticationError |
| 402 | InsufficientCreditsError | InsufficientCreditsException | InsufficientCreditsError |
| 403 | PermissionError | PermissionException | PermissionDeniedError |
| 404 | NotFoundError | NotFoundException | NotFoundError |
| 429 | RateLimitError (.retryAfter in seconds) | RateLimitException (getRetryAfter()) | RateLimitError (.retry_after) |
| 501 | InvalidRequestError | ApiException | InvalidRequestError |
| 500, 503 and other 5xx | APIError | ApiException | APIError |
| Network, DNS, TLS | APIConnectionError | ConnectionException | APIConnectionError |
| Timeout | APITimeoutError (extends APIConnectionError) | ConnectionException | APITimeoutError (extends APIConnectionError) |
| Webhook signature | WebhookVerificationError | SignatureVerificationException | WebhookVerificationError |
In a browser, the Constaia constructor throws an error with code secret_key_in_browser if you pass it a ck_ key:
the key must stay on your server. See security.
What to retry
| Retryable | Not retryable (fix the request) |
|---|---|
429 rate_limited (honour Retry-After) | 400, 401, 403, 404 |
409 idempotency_in_progress | 402 insufficient_credits (until you top up) |
500, 502, 503, 504 | 413, 415, 422 |
408 and network errors or timeouts | 409 analysis_not_completed (wait for it to finish, don't retry in a loop) |
501 not_implemented |
The JavaScript, PHP and Python SDKs already retry the left-hand cases for you (2 retries by default, maxRetries in
JavaScript, max_retries in PHP and Python), honour Retry-After and reuse the same Idempotency-Key on every retry, so a retry never charges
twice. Set it to 0 to disable retries and handle them yourself. If you make raw HTTP calls, send your own Idempotency-Key on every POST before retrying. See
idempotency.
live_mode_unavailable
It is a 5xx and the SDKs retry it, but it does not fix itself within seconds. If you get it, work with a ck_test_
key and see status and SLA.
Handling examples
import {
Constaia,
ConstaiaError,
InsufficientCreditsError,
InvalidRequestError,
RateLimitError,
APIConnectionError,
} from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia({ maxRetries: 3 });
try {
const analysis = await constaia.analyze(await fromPath("./dni.jpg"), { expect: "es_dni" });
console.log(analysis.verdict?.status);
} catch (err) {
if (err instanceof InvalidRequestError) {
// 400/409/413/415/422: the problem is in the request or the file
console.error(`Invalid request (${err.code}) at ${err.param ?? "-"}: ${err.message}`);
} else if (err instanceof InsufficientCreditsError) {
console.error("Out of credits: buy a pack in the dashboard.");
} else if (err instanceof RateLimitError) {
console.error(`Rate limited after retrying; try again in ${err.retryAfter ?? 1} s.`);
} else if (err instanceof APIConnectionError) {
console.error("Could not reach Constaia:", err.message);
} else if (err instanceof ConstaiaError) {
console.error(`Error ${err.status} ${err.code} (request ${err.requestId})`);
} else {
throw err;
}
}Next steps
Digital signature verification in PDF
How Constaia verifies the PAdES electronic signature of a PDF: integrity, chain of trust and later changes, the signature object, require_valid_signature and what it doesn't check yet.
Rate limits and quotas
Constaia API limits per plan (requests per second, concurrent analyses and pages per minute), RateLimit headers, 429 responses with Retry-After, the monthly spend cap, and how to retry with backoff.