Constaia
Endpoints

POST /v1/analyze

Referencia completa de POST /v1/analyze: formas de envío, todas las opciones y checks, objeto analysis campo a campo, códigos de estado y ejemplos.

POST https://api.constaia.com/v1/analyze recibe un documento, lo clasifica, extrae sus campos y, si le dices qué esperas con expect, lo valida y devuelve un veredicto valid, invalid o review. Es el endpoint principal de la API.

Si solo necesitas saber qué documento es (por ejemplo, para enrutarlo), usa POST /v1/classify, que cuesta 0,2 créditos. Para muchos documentos a la vez, POST /v1/batches. Si dudas, mira Qué endpoint usar.

Enviar el documento

Hay dos formas de enviar la petición. Cualquier otro Content-Type devuelve 415 unsupported_content_type.

1. multipart/form-data

CampoTipoDescripción
filebinarioEl documento. Obligatorio.
optionsstring (JSON)Las opciones serializadas en JSON. Opcional.
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}}'

Atajo para pruebas rápidas: si no envías options, puedes pasar expect como campo del formulario, repetible para varios tipos:

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

Si envías options, el campo expect suelto se ignora.

2. application/json

Con una URL pública o con el fichero en base64:

CampoTipoDescripción
file_urlstringURL https:// del documento. Constaia la descarga con un límite de 20 MB y 15 s, sin seguir redirecciones y sin acceder a IPs privadas.
file_base64stringContenido del fichero en base64. Se tolera el prefijo data:…;base64,.
filenamestringNombre del fichero. Recomendado con file_base64; con file_url sustituye al nombre que sale de la URL.
optionsobjetoLas opciones. También puedes ponerlas directamente en la raíz del cuerpo.
Con 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/justificante.pdf",
    "options": {
      "expect": "payment_receipt",
      "checks": { "expected_amount": 45, "expected_reference": "INSCRIPCION 123" }
    }
  }'
Con file_base64 y opciones en la raíz
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\"
  }"

Usa una de las dos formas para las opciones: si el cuerpo trae options, las claves de la raíz (salvo file_url, file_base64 y filename) no se leen.

Ficheros admitidos

  • Formatos: JPEG, PNG, WEBP, HEIC y PDF. El tipo se detecta por el contenido (magic bytes), no por la extensión.
  • Tamaño: hasta 20 MB (413 file_too_large).
  • Páginas: un PDF admite hasta 30 páginas en modo síncrono y hasta 200 con async: true o dentro de un lote. Por encima, 422 too_many_pages.
  • Un DNI con anverso y reverso puede ir en un solo fichero (una imagen con las dos caras o un PDF de dos páginas).
  • En modo test el nombre del fichero decide la respuesta simulada, así que cuida filename.

Opciones

Las opciones son estrictas: una clave desconocida devuelve 422 invalid_parameter con param: "options.<clave>".

OpciónTipoPor defectoDescripción
expectstring o string[] (1–20)—Tipo o tipos de documento que aceptas, del catálogo. Con expect recibes verdict; sin él, verdict es null. Un tipo desconocido devuelve 422.
extractboolean u objetotruetrue: campos de la plantilla del tipo. false: sin extracción de campos. Un objeto JSON Schema: extrae los campos de tu esquema.
checksobjeto{}Reglas de validación. Ver Checks.
storagenone | temporary | persistentvalor de la cuenta, si no noneQué se hace con el fichero. Ver Almacenamiento y privacidad.
ttl_hoursentero 1–720valor de la cuenta, si no 24Horas que se conserva el fichero con storage: "temporary".
keep_resultsbooleantruefalse: los datos extraídos se devuelven una vez y no se guardan. Un GET posterior devuelve 404.
asyncbooleanfalsetrue: responde 202 al momento con status: "queued" y el resultado llega por webhook.
exportarray de json, csv, xlsx, xml, vcard, pdf[]Genera exportaciones y devuelve URLs firmadas en exports (caducan a las 24 h). Ver Exportaciones.
metadataobjeto string → string{}Hasta 20 claves (clave ≤ 40 caracteres, valor ≤ 500). Vuelve en la respuesta y en los webhooks, y sirve para filtrar el listado.
languagees | en | pt | fresIdioma de verdict.reasons[].message, de checks[].message y de document.label.
processingsovereign | standardperfil de la cuentaQué proveedores de IA pueden procesar el documento. Ver Perfil de procesamiento.
precise_bboxesbooleanfalseFuerza el modo ocr+llm (OCR + modelo) para obtener source.bbox finos por campo. Sin él, las cajas son aproximadas.

Perfil de procesamiento

processing elige qué proveedores de IA pueden tocar el documento en esta petición:

  • sovereign: solo proveedores con sede y operación en la UE (un modelo europeo o alojado por Constaia, OCR de Mistral en París y los lectores locales de MRZ y PDF417).
  • standard: además puede usar Claude en AWS Bedrock (Fráncfort, eu-central-1) o Gemini en Google Vertex AI (región UE). Los datos se procesan en regiones de la UE, pero AWS y Google son empresas con sede en EE. UU. (exposición a la CLOUD Act).
  • Si no la envías, se usa el perfil de tu cuenta. Si el perfil pedido no está disponible, la API responde 422 processing_unavailable con param: "options.processing".

La respuesta dice siempre qué se usó en el objeto processing. Detalles en Residencia de datos.

Checks

checks también es estricto. Cada check solo se aplica si el tipo detectado tiene el campo correspondiente (por ejemplo, generic no tiene fecha de caducidad). Qué checks admite cada tipo lo indica GET /v1/document-types/{type} en su lista checks. Explicación detallada en Checks.

CheckTipoQué comprueba
not_expiredbooleanLa fecha de caducidad es posterior a hoy (o a reference_date). Activado por defecto en los tipos con caducidad (expiry_check_default: true); pasa false para desactivarlo.
reference_date"YYYY-MM-DD"Fecha con la que se comparan caducidad, antigüedad y edad, en lugar de hoy.
max_age_daysenteroLa fecha de emisión no tiene más de N días (certificados, justificantes).
min_age_yearsenteroEl titular tiene al menos N años según la fecha de nacimiento.
max_age_yearsenteroEl titular tiene como mucho N años.
holderobjetoLos datos del titular coinciden: full_name, first_name, last_name, document_number, birth_date (todos opcionales). Comparación normalizada: sin tildes ni mayúsculas, orden de apellidos flexible y tolerante a erratas pequeñas.
require_fieldsstring[]Estos campos deben venir con valor.
require_signaturebooleanEl documento está firmado (certificados).
require_stampbooleanEl documento tiene sello (certificados).
require_valid_signaturebooleanEl PDF lleva una firma electrónica PAdES íntegra, sin cambios posteriores y de un emisor de confianza. Una foto o un escaneo nunca la cumplen. Ver Firmas digitales en PDF.
expected_amountnúmeroEl importe coincide: amount en payment_receipt, total en invoice.
expected_ibanstringEl IBAN coincide.
expected_referencestringEl texto aparece en reference o en concept.
Ejemplo de 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": "es"
}

Extraer campos con tu propio esquema

Pasa un JSON Schema en extract y los campos de fields serán los de tu esquema. Útil con generic o cuando solo quieres unos pocos campos. Las description de cada propiedad guían la extracción.

{
  "expect": "generic",
  "extract": {
    "type": "object",
    "properties": {
      "club_name": { "type": "string", "description": "Nombre del club que emite el documento" },
      "member_name": { "type": "string", "description": "Nombre completo del socio" },
      "season": { "type": "string", "description": "Temporada, p. ej. 2026-2027" }
    }
  },
  "checks": { "require_fields": ["club_name", "member_name"] }
}

Síncrono o asíncrono

  • Por defecto la petición espera el resultado hasta 30 segundos y responde 200 con status: "completed" (o "failed").
  • Si el análisis tarda más de 30 s, la API responde 202 con el análisis en queued o processing. El trabajo sigue y el resultado llega por el webhook analysis.completed; también puedes consultar GET /v1/analyses/{id}.
  • Con async: true la respuesta es 202 inmediata con status: "queued". Permite PDF de hasta 200 páginas.

Tu código debe tratar siempre el 202: comprueba status antes de leer verdict o fields.

Respuesta: el objeto analysis

200 OK (dni_valid.jpg, modo test)
{
  "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": "DNI (España)", "confidence": 0.97, "side": "both", "country": "ESP" },
  "verdict": {
    "expected": ["es_dni"],
    "match": true,
    "status": "valid",
    "reasons": [
      { "code": "type_match", "severity": "info", "message": "El documento es DNI (España)." },
      { "code": "not_expired", "severity": "info", "message": "Vigente hasta el 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": "La letra del documento 12345678Z es correcta." },
    { "code": "mrz_checksums", "passed": true, "message": "Los dígitos de control de la MRZ son correctos." },
    { "code": "mrz_matches_visual", "passed": true, "message": "La MRZ coincide con los datos impresos." }
  ],
  "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": {}
}

En el ejemplo, fields está recortado a un campo. Un DNI devuelve también first_name, last_name_1, last_name_2, sex, nationality, birth_date, expiry_date, issue_date, support_number, address, birth_place, parents y mrz, cada uno con la misma estructura.

CampoTipoDescripción
idstringId del análisis, prefijo an_.
object"analysis"Tipo de objeto.
statusqueued | processing | completed | failedEstado del análisis.
livemodebooleantrue con clave ck_live_, false con ck_test_.
batch_idstringSolo si el análisis pertenece a un lote.
created_atstring ISO 8601Creación.
completed_atstring | nullFin del análisis; null mientras no termina.
fileobjetoname, mime_type, pages, size_bytes del fichero recibido.
documentobjeto | nullTipo detectado: type, label (en el idioma de language), confidence (0–1), side (front, back, both o null), country (ISO alfa-3 o null).
verdictobjeto | nullSolo con expect. expected (tu lista), match (el tipo detectado está en expected), status y reasons.
verdict.statusvalid | invalid | reviewinvalid si algún motivo tiene severity: "error"; review si alguno tiene warning; valid en otro caso. Ver Veredictos.
verdict.reasons[]objetocode, severity (info, warning, error) y message localizado.
fieldsobjetoUn objeto por campo: value, confidence (0–1), validated y source.
fields.<campo>.validatedboolean | nulltrue/false si un validador determinista revisó el campo (letra del NIF, IBAN…); null si no aplica.
fields.<campo>.sourceobjeto | nullpage y bbox [x0, y0, x1, y1] normalizado de 0 a 1; bbox puede ser null.
checksarrayValidadores deterministas ejecutados: code, passed, message.
warningsstring[]Señales de calidad o posible manipulación. Son indicios, no pruebas.
signatureobjeto | nullFirma electrónica del PDF (solo PDF; null en imágenes). Ver abajo.
exportsobjetoFormato → URL firmada de descarga (24 h). Vacío si no pediste export.
storageobjetomode, file_deleted_at (cuándo se borró el fichero) y expires_at (con temporary).
processingobjeto | nullPerfil, región y proveedores que procesaron el documento. Ver abajo.
usageobjetocredits consumidos (0 en test y en análisis fallidos) y pages procesadas.
metadataobjetoTu metadata, tal cual.
errorobjetoSolo si status es failed: code (p. ej. processing_failed) y message. Los análisis fallidos no se cobran.

El objeto processing

Dice qué perfil se aplicó y qué proveedores tocaron el documento, para que puedas demostrarlo en una auditoría:

processing (modo live)
{
  "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" }
  ]
}
CampoDescripción
profilesovereign o standard: el de la petición o, si no lo enviaste, el de tu cuenta.
regionRegión de datos: hoy siempre eu.
modevlm (una llamada multimodal; cajas aproximadas) u ocr+llm (OCR + modelo; cajas finas, con precise_bboxes o en PDF escaneados largos).
providers[]Cada proveedor que procesó el documento: name, region, role (ocr, llm o local) y model. Los lectores local (MRZ, PDF417) se ejecutan en los servidores de Constaia sin terceros.

En modo test el único proveedor es mock. Mientras el análisis está en cola, processing puede ser null.

El objeto signature

Solo en PDF. Constaia verifica la firma PAdES del fichero original (integridad, cadena del certificado y cambios posteriores) sin coste adicional:

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 es valid, invalid, missing, modified o untrusted. Qué garantiza cada estado y cómo exigirlo con require_valid_signature: Firmas digitales en PDF.

Motivos (verdict.reasons[].code)

El mismo código puede venir con severidad info (se cumple), warning (lleva a review) o error (lleva a invalid). Por ejemplo, un DNI caducado devuelve { "code": "not_expired", "severity": "error", "message": "Caducado el 15/06/2020." }.

El primer motivo siempre es sobre el tipo:

  • type_match (info): el tipo detectado está en expect.
  • type_mismatch (error, lleva a invalid): se reconoce un tipo del catálogo, pero no es ninguno de los esperados.
  • type_unknown (warning, lleva a review): el documento no se ha podido identificar (tipo generic) o la confianza de la clasificación es menor de 0,5.

Lista completa de códigos: 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, y el código de cualquier validador determinista que falle (normalmente con severidad error).

Además:

  • Firma del PDF: signature_valid (info), signature_invalid (siempre error), signature_missing, document_modified_after_signing y untrusted_signer. Sin require_valid_signature estos tres últimos son warning (y signature_missing solo aparece en tipos que suelen ir firmados); con él, son error. Ver Firmas digitales en PDF.
  • Metadatos del PDF: edited_suspected (warning): el PDF lo generó un editor conocido, su fecha de modificación es muy posterior a la de creación o cambió después de firmarse.
  • Formulario I-9 (EE. UU.): i9_list (info) indica a qué lista (A, B o C) pertenece el documento. Ver Documentos de EE. UU..

Validadores (checks[].code)

CódigoQué comprueba
nif_check_digitLetra de control de DNI, NIE o NIF.
mrz_checksumsDígitos de control de la zona MRZ.
mrz_matches_visualLa MRZ coincide con los datos impresos.
iban_checksumDígitos de control del IBAN.
invoice_totalsBase, IVA y total de la factura cuadran.
csv_formatFormato del código seguro de verificación (CSV). Solo el formato: Constaia no consulta el servicio del Ministerio. Verifícalo en la sede del emisor si lo necesitas.
date_consistencyCoherencia entre fechas del documento.
id_number_checksumDígito de control de un identificador nacional del catálogo (CPF, codice fiscale, PESEL…).
id_number_formatFormato de un identificador sin dígito de control (número de permiso de cada estado de EE. UU., SSN, EIN, código ZIP…).
id_number_matches_birth_dateLa fecha de nacimiento codificada en el identificador coincide con la impresa.
aamva_matches_visualEl código de barras PDF417 (AAMVA) del reverso de un permiso o ID de EE. UU./Canadá coincide con el anverso impreso.

Avisos (warnings)

CódigoSignificado
low_qualityCalidad baja en general.
blurryImagen desenfocada.
croppedEl documento está recortado.
glareReflejos que tapan datos.
screen_photo_suspectedPosible foto de una pantalla.
photocopy_suspectedPosible fotocopia.
edited_suspectedPosible edición digital.
multiple_documentsHay más de un documento en el fichero.
side_missingFalta una cara.
language_mismatchEl idioma no es el esperado para el tipo.

Los warnings son indicios, no prueba de autenticidad.

Constaia no es un KYC biométrico y no compara caras.

Códigos de estado y errores

HTTPCuándo
200Análisis terminado (completed o failed).
202En cola: async: true o el análisis superó los 30 s.
400invalid_json, invalid_multipart, invalid_options (el campo options no es JSON), missing_file, empty_file, invalid_base64, invalid_file_url (no https o IP no permitida), invalid_idempotency_key.
401missing_api_key, invalid_api_key.
402insufficient_credits, monthly_cap_reached (se alcanzaría el tope de gasto mensual) o email_not_verified (créditos gratis en live sin email verificado). Solo claves live.
409idempotency_in_progress: la petición original con esa Idempotency-Key aún está en curso.
413file_too_large: más de 20 MB.
415unsupported_file_type (no es JPEG, PNG, WEBP, HEIC ni PDF), unsupported_content_type.
422invalid_parameter (con param, p. ej. options.expect), too_many_pages, unreadable_image, unreadable_pdf, file_url_unreachable, idempotency_key_reused, processing_unavailable (el perfil de processing pedido no está disponible).
429rate_limited (peticiones/s), concurrency_limit (análisis síncronos simultáneos) o pages_rate_limited (páginas/min), con Retry-After. Ver Límites.
503live_mode_unavailable: el modo live no está disponible; usa ck_test_ mientras tanto.

Formato y tratamiento de errores en Errores. Para reintentar sin cobros dobles, envía la cabecera Idempotency-Key (Idempotencia).

Ejemplos completos

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" }
  }'

JSON con file_url y 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/justificante.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);

Siguientes pasos

En esta página