POST /v1/analyze
Full reference for POST /v1/analyze: request formats, every option and check, the analysis object field by field, status codes and examples.
POST https://api.constaia.com/v1/analyze takes a document, classifies it, extracts its fields and, if you tell it what you expect with expect, validates it and returns a valid, invalid or review verdict. It is the main endpoint of the API.
If you only need to know which document it is (for example, to route it), use POST /v1/classify, which costs 0.2 credits. For many documents at once, POST /v1/batches. If in doubt, see Which endpoint to use.
Sending the document
There are two ways to send the request. Any other Content-Type returns 415 unsupported_content_type.
1. multipart/form-data
| Field | Type | Description |
|---|---|---|
file | binary | The document. Required. |
options | string (JSON) | The options serialised as JSON. Optional. |
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F file=@dni_valid.jpg \
-F 'options={"expect":"es_dni","checks":{"min_age_years":18}}'Shortcut for quick tests: if you do not send options, you can pass expect as a form field, repeatable for several types:
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F file=@nie.jpg -F expect=es_dni -F expect=es_nie -F expect=passportIf you send options, the loose expect field is ignored.
2. application/json
With a public URL or with the file in base64:
| Field | Type | Description |
|---|---|---|
file_url | string | https:// URL of the document. Constaia downloads it with a 20 MB and 15 s limit, without following redirects and without reaching private IPs. |
file_base64 | string | File content in base64. A data:…;base64, prefix is tolerated. |
filename | string | File name. Recommended with file_base64; with file_url it replaces the name taken from the URL. |
options | object | The options. You can also put them directly at the root of the body. |
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_url": "https://example.com/uploads/receipt.pdf",
"options": {
"expect": "payment_receipt",
"checks": { "expected_amount": 45, "expected_reference": "INSCRIPCION 123" }
}
}'curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"file_base64\": \"$(base64 < dni_valid.jpg | tr -d '\n')\",
\"filename\": \"dni_valid.jpg\",
\"expect\": \"es_dni\",
\"language\": \"en\"
}"Use one of the two forms for options: if the body has options, root keys (other than file_url, file_base64 and filename) are not read.
Accepted files
- Formats: JPEG, PNG, WEBP, HEIC and PDF. The type is detected from the content (magic bytes), not the extension.
- Size: up to 20 MB (
413 file_too_large). - Pages: a PDF can have up to 30 pages in synchronous mode and up to 200 with
async: trueor inside a batch. Above that,422 too_many_pages. - A DNI with front and back can go in a single file (one image with both sides or a two-page PDF).
- In test mode the file name decides the simulated response, so mind
filename.
Options
Options are strict: an unknown key returns 422 invalid_parameter with param: "options.<key>".
| Option | Type | Default | Description |
|---|---|---|---|
expect | string or string[] (1–20) | — | Document type or types you accept, from the catalogue. With expect you get verdict; without it, verdict is null. An unknown type returns 422. |
extract | boolean or object | true | true: fields of the type's template. false: no field extraction. A JSON Schema object: extracts the fields of your schema. |
checks | object | {} | Validation rules. See Checks. |
storage | none | temporary | persistent | account value, else none | What happens to the file. See Storage and privacy. |
ttl_hours | integer 1–720 | account value, else 24 | Hours the file is kept with storage: "temporary". |
keep_results | boolean | true | false: extracted data is returned once and not stored. A later GET returns 404. |
async | boolean | false | true: answers 202 immediately with status: "queued" and the result arrives by webhook. |
export | array of json, csv, xlsx, xml, vcard, pdf | [] | Generates exports and returns signed URLs in exports (they expire after 24 h). See Exports. |
metadata | object string → string | {} | Up to 20 keys (key ≤ 40 characters, value ≤ 500). Returned in the response and webhooks, and usable to filter the list. |
language | es | en | pt | fr | es | Language of verdict.reasons[].message, checks[].message and document.label. |
processing | sovereign | standard | account profile | Which AI providers may process the document. See Processing profile. |
precise_bboxes | boolean | false | Forces the ocr+llm mode (OCR + model) to get fine-grained source.bbox per field. Without it, boxes are approximate. |
Processing profile
processing chooses which AI providers may touch the document in this request:
sovereign: only providers headquartered and operated in the EU (a European or Constaia-hosted model, Mistral OCR in Paris and the local MRZ and PDF417 readers).standard: may also use Claude on AWS Bedrock (Frankfurt,eu-central-1) or Gemini on Google Vertex AI (EU region). Data is processed in EU regions, but AWS and Google are US-headquartered companies (US CLOUD Act exposure).- If you do not send it, your account profile is used. If the requested profile is not available, the API answers
422 processing_unavailablewithparam: "options.processing".
The response always tells you what was used in the processing object. Details in Data residency.
Checks
checks is strict too. Each check only applies if the detected type has the relevant field (for example, generic has no expiry date). Which checks each type supports is listed in the checks array of GET /v1/document-types/{type}. Detailed explanation in Checks.
| Check | Type | What it checks |
|---|---|---|
not_expired | boolean | The expiry date is after today (or after reference_date). On by default for types with an expiry date (expiry_check_default: true); pass false to disable it. |
reference_date | "YYYY-MM-DD" | Date used for expiry, issue age and holder age instead of today. |
max_age_days | integer | The issue date is no more than N days old (certificates, receipts). |
min_age_years | integer | The holder is at least N years old according to the birth date. |
max_age_years | integer | The holder is at most N years old. |
holder | object | The holder data matches: full_name, first_name, last_name, document_number, birth_date (all optional). Normalised comparison: accent and case insensitive, flexible surname order, tolerant to small typos. |
require_fields | string[] | These fields must have a value. |
require_signature | boolean | The document is signed (certificates). |
require_stamp | boolean | The document has a stamp (certificates). |
require_valid_signature | boolean | The PDF carries an intact PAdES electronic signature, not modified afterwards and from a trusted issuer. A photo or a scan never passes. See Digital signatures in PDF. |
expected_amount | number | The amount matches: amount in payment_receipt, total in invoice. |
expected_iban | string | The IBAN matches. |
expected_reference | string | The text appears in reference or concept. |
{
"expect": ["es_dni", "es_nie", "passport"],
"checks": {
"min_age_years": 18,
"holder": { "full_name": "María García López", "birth_date": "1990-05-14" }
},
"storage": "none",
"metadata": { "registration_id": "123" },
"language": "en"
}Extracting fields with your own schema
Pass a JSON Schema in extract and fields will contain the fields of your schema. Useful with generic or when you only want a few fields. Each property's description guides the extraction.
{
"expect": "generic",
"extract": {
"type": "object",
"properties": {
"club_name": { "type": "string", "description": "Name of the club issuing the document" },
"member_name": { "type": "string", "description": "Member's full name" },
"season": { "type": "string", "description": "Season, e.g. 2026-2027" }
}
},
"checks": { "require_fields": ["club_name", "member_name"] }
}Synchronous or asynchronous
- By default the request waits for the result for up to 30 seconds and answers
200withstatus: "completed"(or"failed"). - If the analysis takes longer than 30 s, the API answers
202with the analysis inqueuedorprocessing. The work continues and the result arrives in theanalysis.completedwebhook; you can also pollGET /v1/analyses/{id}. - With
async: truethe response is an immediate202withstatus: "queued". It allows PDFs of up to 200 pages.
Your code must always handle 202: check status before reading verdict or fields.
Response: the analysis object
{
"id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V2",
"object": "analysis",
"status": "completed",
"livemode": false,
"created_at": "2026-09-29T10:00:00Z",
"completed_at": "2026-09-29T10:00:02Z",
"file": { "name": "dni_valid.jpg", "mime_type": "image/jpeg", "pages": 1, "size_bytes": 482133 },
"document": { "type": "es_dni", "label": "Spanish ID card (DNI)", "confidence": 0.97, "side": "both", "country": "ESP" },
"verdict": {
"expected": ["es_dni"],
"match": true,
"status": "valid",
"reasons": [
{ "code": "type_match", "severity": "info", "message": "The document is Spanish ID card (DNI)." },
{ "code": "not_expired", "severity": "info", "message": "Valid until 12/03/2031." }
]
},
"fields": {
"document_number": {
"value": "12345678Z",
"confidence": 0.99,
"validated": true,
"source": { "page": 1, "bbox": [0.61, 0.12, 0.83, 0.16] }
}
},
"checks": [
{ "code": "nif_check_digit", "passed": true, "message": "The check letter of 12345678Z is correct." },
{ "code": "mrz_checksums", "passed": true, "message": "The MRZ check digits are correct." },
{ "code": "mrz_matches_visual", "passed": true, "message": "The MRZ matches the printed data." }
],
"warnings": [],
"signature": null,
"exports": {},
"storage": { "mode": "none", "file_deleted_at": "2026-09-29T10:00:02Z", "expires_at": null },
"processing": {
"profile": "sovereign",
"region": "eu",
"mode": "vlm",
"providers": [{ "name": "mock", "region": "local", "role": "llm", "model": "mock-llm-1" }]
},
"usage": { "credits": 0, "pages": 1 },
"metadata": {}
}In the example, fields is trimmed to one field. A DNI also returns first_name, last_name_1, last_name_2, sex, nationality, birth_date, expiry_date, issue_date, support_number, address, birth_place, parents and mrz, each with the same structure.
| Field | Type | Description |
|---|---|---|
id | string | Analysis id, prefix an_. |
object | "analysis" | Object type. |
status | queued | processing | completed | failed | Analysis status. |
livemode | boolean | true with a ck_live_ key, false with ck_test_. |
batch_id | string | Only if the analysis belongs to a batch. |
created_at | ISO 8601 string | Creation time. |
completed_at | string | null | End of the analysis; null until it finishes. |
file | object | name, mime_type, pages, size_bytes of the received file. |
document | object | null | Detected type: type, label (in the language you chose), confidence (0–1), side (front, back, both or null), country (ISO alpha-3 or null). |
verdict | object | null | Only with expect. expected (your list), match (the detected type is in expected), status and reasons. |
verdict.status | valid | invalid | review | invalid if any reason has severity: "error"; review if any has warning; valid otherwise. See Verdicts. |
verdict.reasons[] | object | code, severity (info, warning, error) and localised message. |
fields | object | One object per field: value, confidence (0–1), validated and source. |
fields.<field>.validated | boolean | null | true/false if a deterministic validator checked the field (NIF letter, IBAN…); null otherwise. |
fields.<field>.source | object | null | page and bbox [x0, y0, x1, y1] normalised from 0 to 1; bbox may be null. |
checks | array | Deterministic validators that ran: code, passed, message. |
warnings | string[] | Quality or possible tampering signals. They are hints, not proof. |
signature | object | null | Electronic signature of the PDF (PDF only; null for images). See below. |
exports | object | Format → signed download URL (24 h). Empty if you did not request export. |
storage | object | mode, file_deleted_at (when the file was deleted) and expires_at (with temporary). |
processing | object | null | Profile, region and providers that processed the document. See below. |
usage | object | credits spent (0 in test mode and for failed analyses) and pages processed. |
metadata | object | Your metadata, as sent. |
error | object | Only when status is failed: code (e.g. processing_failed) and message. Failed analyses are not charged. |
The processing object
It tells you which profile was applied and which providers touched the document, so you can prove it in an audit:
{
"profile": "sovereign",
"region": "eu",
"mode": "vlm",
"providers": [
{ "name": "tesseract", "region": "local", "role": "local", "model": "mrz" },
{ "name": "openai_compat", "region": "de-fra", "role": "llm", "model": "mistral-small-3.2" }
]
}| Field | Description |
|---|---|
profile | sovereign or standard: the one in the request or, if you did not send it, your account's. |
region | Data region: always eu today. |
mode | vlm (a single multimodal call; approximate boxes) or ocr+llm (OCR + model; fine boxes, with precise_bboxes or for long scanned PDFs). |
providers[] | Each provider that processed the document: name, region, role (ocr, llm or local) and model. local readers (MRZ, PDF417) run on Constaia's servers with no third party. |
In test mode the only provider is mock. While the analysis is queued, processing may be null.
The signature object
PDF only. Constaia verifies the PAdES signature of the original file (integrity, certificate chain and later changes) at no extra cost:
{
"status": "valid",
"reasons": ["signature_valid"],
"signer": "SELLO ELECTRONICO DEL MINISTERIO DE JUSTICIA",
"issuer": "AC Sector Público",
"signed_at": "2026-09-28T09:14:03Z",
"trusted": true,
"trust_anchor": "AC RAIZ FNMT-RCM",
"signatures": 1
}status is valid, invalid, missing, modified or untrusted. What each status guarantees and how to require it with require_valid_signature: Digital signatures in PDF.
Reasons (verdict.reasons[].code)
The same code can come with severity info (passes), warning (leads to review) or error (leads to invalid). For example, an expired DNI returns { "code": "not_expired", "severity": "error", "message": "Expired on 15/06/2020." } (with language: "en"; the default language is Spanish).
The first reason is always about the type:
type_match(info): the detected type is inexpect.type_mismatch(error, leads toinvalid): a catalogue type is recognised, but it is none of the expected ones.type_unknown(warning, leads toreview): the document could not be identified (typegeneric) or classification confidence is below 0.5.
Full list of codes: type_match, type_mismatch, type_unknown, not_expired, max_age_days, age, min_age_years, max_age_years, holder, required_field_missing, require_signature, require_stamp, expected_amount, expected_iban, expected_reference, not_fit_for_sport, has_records, low_quality, low_confidence, plus the code of any deterministic validator that fails (usually with severity error).
Also:
- PDF signature:
signature_valid(info),signature_invalid(alwayserror),signature_missing,document_modified_after_signinganduntrusted_signer. Withoutrequire_valid_signaturethe last three arewarning(andsignature_missingonly appears for types that are usually signed); with it, they areerror. See Digital signatures in PDF. - PDF metadata:
edited_suspected(warning): the PDF was produced by a known editor, its modification date is much later than its creation date, or it changed after being signed. - Form I-9 (US):
i9_list(info) tells you which list (A, B or C) the document belongs to. See US documents.
Validators (checks[].code)
| Code | What it checks |
|---|---|
nif_check_digit | Check letter of a DNI, NIE or NIF. |
mrz_checksums | Check digits of the MRZ. |
mrz_matches_visual | The MRZ matches the printed data. |
iban_checksum | IBAN check digits. |
invoice_totals | Invoice base, VAT and total add up. |
csv_format | Format of the Spanish secure verification code (CSV). Format only: Constaia does not query the Ministry's service. Verify it on the issuer's official site if you need to. |
date_consistency | Consistency between dates in the document. |
id_number_checksum | Check digit of a national identifier in the catalogue (CPF, codice fiscale, PESEL…). |
id_number_format | Format of an identifier without a check digit (each US state's license number, SSN, EIN, ZIP code…). |
id_number_matches_birth_date | The birth date encoded in the identifier matches the printed one. |
aamva_matches_visual | The PDF417 barcode (AAMVA) on the back of a US/Canadian license or ID matches the printed front. |
Warnings (warnings)
| Code | Meaning |
|---|---|
low_quality | Overall low quality. |
blurry | Blurry image. |
cropped | The document is cropped. |
glare | Glare hides data. |
screen_photo_suspected | Possible photo of a screen. |
photocopy_suspected | Possible photocopy. |
edited_suspected | Possible digital edit. |
multiple_documents | More than one document in the file. |
side_missing | One side is missing. |
language_mismatch | The language is not the expected one for the type. |
Warnings are signals, not proof of authenticity.
Constaia is not a biometric KYC and does no face matching.
Status codes and errors
| HTTP | When |
|---|---|
200 | Analysis finished (completed or failed). |
202 | Queued: async: true or the analysis went over 30 s. |
400 | invalid_json, invalid_multipart, invalid_options (the options field is not JSON), missing_file, empty_file, invalid_base64, invalid_file_url (not https or disallowed IP), invalid_idempotency_key. |
401 | missing_api_key, invalid_api_key. |
402 | insufficient_credits, monthly_cap_reached (the monthly spend cap would be exceeded) or email_not_verified (free credits in live mode without a verified email). Live keys only. |
409 | idempotency_in_progress: the original request with that Idempotency-Key is still running. |
413 | file_too_large: over 20 MB. |
415 | unsupported_file_type (not JPEG, PNG, WEBP, HEIC or PDF), unsupported_content_type. |
422 | invalid_parameter (with param, e.g. options.expect), too_many_pages, unreadable_image, unreadable_pdf, file_url_unreachable, idempotency_key_reused, processing_unavailable (the requested processing profile is not available). |
429 | rate_limited (requests/s), concurrency_limit (concurrent synchronous analyses) or pages_rate_limited (pages/min), with Retry-After. See Rate limits. |
503 | live_mode_unavailable: live mode is not available; use ck_test_ meanwhile. |
Error format and handling in Errors. To retry without double charges, send the Idempotency-Key header (Idempotency).
Complete examples
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-H "Idempotency-Key: registration-123-dni" \
-F file=@dni_valid.jpg \
-F 'options={
"expect": ["es_dni", "es_nie", "passport"],
"checks": { "min_age_years": 18, "holder": { "full_name": "María García López" } },
"metadata": { "registration_id": "123" },
"language": "en"
}'JSON with file_url and file_base64
import { readFile } from "node:fs/promises";
import { Constaia } from "@constaia/sdk";
const constaia = new Constaia();
const fromUrl = await constaia.analyze(
{ fileUrl: "https://example.com/uploads/receipt.pdf" },
{ expect: "payment_receipt", checks: { expectedAmount: 45, expectedReference: "INSCRIPCION 123" } },
);
const fromBase64 = await constaia.analyze(
{ base64: (await readFile("./invoice.pdf")).toString("base64"), filename: "invoice.pdf" },
{ expect: "invoice", export: ["xlsx"] },
);
console.log(fromUrl.verdict?.status, fromBase64.exports.xlsx);Next steps
Authentication
Authenticate Constaia API calls with ck_live_ and ck_test_ Bearer keys, where to create and revoke them, and why they never go in the browser.
POST /v1/classify
Reference for POST /v1/classify: identify a document's type for 0.2 credits, with candidates and a type verdict, to route it before analysing it.