Constaia
Endpoints

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.

Cette page n'est pas encore traduite dans votre langue. Voici la version anglaise.

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

FieldTypeDescription
filebinaryThe document. Required.
optionsstring (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=passport

If you send options, the loose expect field is ignored.

2. application/json

With a public URL or with the file in base64:

FieldTypeDescription
file_urlstringhttps:// URL of the document. Constaia downloads it with a 20 MB and 15 s limit, without following redirects and without reaching private IPs.
file_base64stringFile content in base64. A data:…;base64, prefix is tolerated.
filenamestringFile name. Recommended with file_base64; with file_url it replaces the name taken from the URL.
optionsobjectThe options. You can also put them directly at the root of the body.
With file_url
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" }
    }
  }'
With file_base64 and options at the root
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: true or 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>".

OptionTypeDefaultDescription
expectstring 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.
extractboolean or objecttruetrue: fields of the type's template. false: no field extraction. A JSON Schema object: extracts the fields of your schema.
checksobject{}Validation rules. See Checks.
storagenone | temporary | persistentaccount value, else noneWhat happens to the file. See Storage and privacy.
ttl_hoursinteger 1–720account value, else 24Hours the file is kept with storage: "temporary".
keep_resultsbooleantruefalse: extracted data is returned once and not stored. A later GET returns 404.
asyncbooleanfalsetrue: answers 202 immediately with status: "queued" and the result arrives by webhook.
exportarray of json, csv, xlsx, xml, vcard, pdf[]Generates exports and returns signed URLs in exports (they expire after 24 h). See Exports.
metadataobject string → string{}Up to 20 keys (key ≤ 40 characters, value ≤ 500). Returned in the response and webhooks, and usable to filter the list.
languagees | en | pt | fresLanguage of verdict.reasons[].message, checks[].message and document.label.
processingsovereign | standardaccount profileWhich AI providers may process the document. See Processing profile.
precise_bboxesbooleanfalseForces 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_unavailable with param: "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.

CheckTypeWhat it checks
not_expiredbooleanThe 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_daysintegerThe issue date is no more than N days old (certificates, receipts).
min_age_yearsintegerThe holder is at least N years old according to the birth date.
max_age_yearsintegerThe holder is at most N years old.
holderobjectThe 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_fieldsstring[]These fields must have a value.
require_signaturebooleanThe document is signed (certificates).
require_stampbooleanThe document has a stamp (certificates).
require_valid_signaturebooleanThe 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_amountnumberThe amount matches: amount in payment_receipt, total in invoice.
expected_ibanstringThe IBAN matches.
expected_referencestringThe text appears in reference or concept.
Example options
{
  "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 200 with status: "completed" (or "failed").
  • If the analysis takes longer than 30 s, the API answers 202 with the analysis in queued or processing. The work continues and the result arrives in the analysis.completed webhook; you can also poll GET /v1/analyses/{id}.
  • With async: true the response is an immediate 202 with status: "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

200 OK (dni_valid.jpg, test mode, language en)
{
  "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.

FieldTypeDescription
idstringAnalysis id, prefix an_.
object"analysis"Object type.
statusqueued | processing | completed | failedAnalysis status.
livemodebooleantrue with a ck_live_ key, false with ck_test_.
batch_idstringOnly if the analysis belongs to a batch.
created_atISO 8601 stringCreation time.
completed_atstring | nullEnd of the analysis; null until it finishes.
fileobjectname, mime_type, pages, size_bytes of the received file.
documentobject | nullDetected type: type, label (in the language you chose), confidence (0–1), side (front, back, both or null), country (ISO alpha-3 or null).
verdictobject | nullOnly with expect. expected (your list), match (the detected type is in expected), status and reasons.
verdict.statusvalid | invalid | reviewinvalid if any reason has severity: "error"; review if any has warning; valid otherwise. See Verdicts.
verdict.reasons[]objectcode, severity (info, warning, error) and localised message.
fieldsobjectOne object per field: value, confidence (0–1), validated and source.
fields.<field>.validatedboolean | nulltrue/false if a deterministic validator checked the field (NIF letter, IBAN…); null otherwise.
fields.<field>.sourceobject | nullpage and bbox [x0, y0, x1, y1] normalised from 0 to 1; bbox may be null.
checksarrayDeterministic validators that ran: code, passed, message.
warningsstring[]Quality or possible tampering signals. They are hints, not proof.
signatureobject | nullElectronic signature of the PDF (PDF only; null for images). See below.
exportsobjectFormat → signed download URL (24 h). Empty if you did not request export.
storageobjectmode, file_deleted_at (when the file was deleted) and expires_at (with temporary).
processingobject | nullProfile, region and providers that processed the document. See below.
usageobjectcredits spent (0 in test mode and for failed analyses) and pages processed.
metadataobjectYour metadata, as sent.
errorobjectOnly 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:

processing (live mode)
{
  "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" }
  ]
}
FieldDescription
profilesovereign or standard: the one in the request or, if you did not send it, your account's.
regionData region: always eu today.
modevlm (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:

signature
{
  "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 in expect.
  • type_mismatch (error, leads to invalid): a catalogue type is recognised, but it is none of the expected ones.
  • type_unknown (warning, leads to review): the document could not be identified (type generic) 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 (always error), signature_missing, document_modified_after_signing and untrusted_signer. Without require_valid_signature the last three are warning (and signature_missing only appears for types that are usually signed); with it, they are error. 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)

CodeWhat it checks
nif_check_digitCheck letter of a DNI, NIE or NIF.
mrz_checksumsCheck digits of the MRZ.
mrz_matches_visualThe MRZ matches the printed data.
iban_checksumIBAN check digits.
invoice_totalsInvoice base, VAT and total add up.
csv_formatFormat 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_consistencyConsistency between dates in the document.
id_number_checksumCheck digit of a national identifier in the catalogue (CPF, codice fiscale, PESEL…).
id_number_formatFormat of an identifier without a check digit (each US state's license number, SSN, EIN, ZIP code…).
id_number_matches_birth_dateThe birth date encoded in the identifier matches the printed one.
aamva_matches_visualThe PDF417 barcode (AAMVA) on the back of a US/Canadian license or ID matches the printed front.

Warnings (warnings)

CodeSignification
low_qualityQualité globalement faible.
blurryImage floue.
croppedLe document est rogné.
glareDes reflets masquent des données.
screen_photo_suspectedPhoto d'écran possible.
photocopy_suspectedPhotocopie possible.
edited_suspectedRetouche numérique possible.
multiple_documentsPlusieurs documents dans le fichier.
side_missingUne face manque.
language_mismatchLa langue n'est pas celle attendue pour ce type.

Les warnings sont des indices, pas une preuve d'authenticité.

Constaia is not a biometric KYC and does no face matching.

Status codes and errors

HTTPWhen
200Analysis finished (completed or failed).
202Queued: async: true or the analysis went over 30 s.
400invalid_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.
401missing_api_key, invalid_api_key.
402insufficient_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.
409idempotency_in_progress: the original request with that Idempotency-Key is still running.
413file_too_large: over 20 MB.
415unsupported_file_type (not JPEG, PNG, WEBP, HEIC or PDF), unsupported_content_type.
422invalid_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).
429rate_limited (requests/s), concurrency_limit (concurrent synchronous analyses) or pages_rate_limited (pages/min), with Retry-After. See Rate limits.
503live_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

analyze-json.ts
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

Sur cette page