Veredictos y motivos
Cómo se calcula el veredicto valid, invalid o review a partir de la severidad de los motivos, tabla completa de códigos estables y qué hacer en cada caso.
El veredicto responde a una pregunta concreta: "¿es este documento un X válido según mis reglas?". Aparece en
verdict cuando indicas qué esperas con expect. Sin expect, verdict es null y solo recibes la
clasificación (document), los campos (fields), las comprobaciones deterministas (checks) y los avisos
(warnings).
El objeto verdict
"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." }
]
}| Campo | Tipo | Descripción |
|---|---|---|
expected | string[] | Los tipos que pediste en expect, siempre como lista. |
match | boolean | true si el tipo detectado (document.type) está entre los esperados. |
status | valid | invalid | review | La decisión. |
reasons | objeto[] | Cada regla evaluada, con code estable, severity y message legible. |
Cada motivo tiene esta forma:
| Campo | Tipo | Descripción |
|---|---|---|
code | string | Código estable. Programa tu lógica contra él. |
severity | info | warning | error | info = la regla se cumple; warning = no se puede confirmar; error = la regla falla. |
message | string | Texto para personas en el idioma de language (es, en, pt, fr). Puede cambiar de redacción: no lo compares como texto. |
Los tres estados
| Estado | Cuándo |
|---|---|
| Válido | Ningún motivo tiene severidad warning ni error. |
| No válido | Al menos un motivo tiene severidad error: tipo distinto, caducado, titular que no coincide, letra del NIF incorrecta, importe distinto… |
| Revisar | No hay ningún error, pero sí algún warning: calidad baja, confianza por debajo del umbral del tipo, un dato que no se pudo leer… |
La regla es mecánica y siempre la misma:
si algún motivo tiene severity "error" → invalid
si no, si alguno tiene severity "warning" → review
si no → validerror gana a warning: un DNI caducado y además borroso es invalid, no review.
review no es un fallo de la API. Significa que Constaia no tiene datos suficientes para decidir con seguridad y que
debe mirarlo una persona o pedirse otra foto. Consulta la guía revisión humana.
Qué motivos aparecen
- Tipo: siempre hay exactamente uno entre
type_match,type_mismatchotype_unknown. - Checks que pediste (y los que están activos por defecto, como
not_expireden documentos con caducidad): uno o varios motivos por cada uno. Detalle en checks. - Reglas propias del tipo:
not_fit_for_sport(certificado médico con "no apto"),has_records(certificados de antecedentes). - Comprobaciones deterministas que fallan: si un elemento de
checks[]tienepassed: false, se añade un motivo con su mismocodey severidaderror. Las que pasan solo aparecen enchecks[], no enreasons. - Calidad y confianza:
low_qualityylow_confidence, con severidadwarning. - PDF: el resultado de la firma electrónica (
signature_valid,signature_invalid…) yedited_suspectedsi los metadatos apuntan a una edición. Ver firmas digitales en PDF. - Formulario I-9:
i9_list(info) en los documentos de EE. UU. de las listas A, B o C.
Tabla de códigos de motivo
Estos códigos son estables. Podemos añadir códigos nuevos sin previo aviso (ver versionado):
trata cualquier código desconocido según su severity.
| Código | Severidades posibles | Cuándo aparece |
|---|---|---|
type_match | info | El tipo detectado es uno de los de expect. |
type_mismatch | error | Se detectó con confianza otro tipo del catálogo. |
type_unknown | warning | No se reconoce el tipo (generic) o la confianza de la clasificación es menor de 0,5. |
not_expired | info, warning, error | Vigencia. warning si no se pudo leer la fecha de caducidad. |
max_age_days | info, warning, error | Antigüedad de la fecha de emisión. warning si no hay fecha de emisión. |
age | info, warning | Edad dentro de los límites pedidos (info) o fecha de nacimiento ilegible (warning). |
min_age_years | error | El titular es más joven que min_age_years. |
max_age_years | error | El titular es mayor que max_age_years. |
holder | info, warning, error | Coincidencia con los datos del titular. warning si el documento no muestra ese dato. |
required_field_missing | error | Falta un campo de require_fields (un motivo por campo). |
require_signature | info, error | Firma presente o ausente. |
require_stamp | info, error | Sello presente o ausente. |
expected_amount | info, error | Importe (amount, o total en invoice) igual o distinto al esperado. |
expected_iban | info, error | El IBAN esperado aparece o no en el documento. |
expected_reference | info, error | La referencia esperada aparece o no en reference o concept. |
not_fit_for_sport | error | Certificado médico deportivo que declara "no apto". |
has_records | info, error | Certificado de antecedentes: sin antecedentes (info) o con antecedentes (error). |
low_quality | warning | Imagen borrosa, recortada, con reflejos, foto de pantalla, fotocopia, posible edición o varios documentos. |
low_confidence | warning | La confianza global (clasificación y campos clave) está por debajo del umbral del tipo. |
nif_check_digit | error | Letra de control de DNI, NIE o CIF incorrecta. |
mrz_checksums | error | Dígitos de control de la MRZ incorrectos. |
mrz_matches_visual | error | La MRZ no coincide con los datos impresos. |
iban_checksum | error | IBAN con dígitos de control incorrectos. |
invoice_totals | error | Base, IVA, retención y total de la factura no cuadran. |
csv_format | error | El código seguro de verificación (CSV) no tiene un formato válido. |
date_consistency | error | Fechas incoherentes (nacimiento posterior a la emisión, emisión posterior a la caducidad…). |
id_number_checksum, id_number_format | warning, error | Un identificador nacional no supera su dígito de control o su formato. warning en los identificadores blandos del catálogo. |
id_number_matches_birth_date | error | La fecha de nacimiento codificada en el identificador no coincide con la impresa. |
aamva_matches_visual | error | El código PDF417 del reverso de un permiso o ID de EE. UU./Canadá no coincide con el anverso. |
signature_valid | info | El PDF lleva una firma electrónica íntegra y de confianza. Ver firmas digitales en PDF. |
signature_invalid | error | La firma del PDF está rota: el contenido no coincide con lo firmado. |
signature_missing | warning, error | El PDF no lleva firma electrónica (en tipos que suelen ir firmados), o se pidió require_valid_signature y no es un PDF firmado. |
document_modified_after_signing | warning, error | El PDF se modificó después de firmarse. error con require_valid_signature. |
untrusted_signer | warning, error | La firma es íntegra pero el certificado no llega a la lista de confianza. error con require_valid_signature. |
edited_suspected | warning | Los metadatos del PDF apuntan a un editor (iLovePDF, Word, Canva…) o a cambios posteriores. |
i9_list | info | Documento de EE. UU. aceptable para el formulario I-9: indica la lista (A, B o C). |
Mismo código, distinta severidad
El code dice qué regla se evaluó; la severity dice cómo salió. Un DNI vigente y uno caducado producen
el mismo código:
{ "code": "not_expired", "severity": "info", "message": "Vigente hasta el 12/03/2031." }{ "code": "not_expired", "severity": "error", "message": "Caducado el 15/06/2020." }Por eso tu código debe mirar siempre el par code + severity, nunca solo la presencia del código.
Avisos (warnings)
warnings es una lista de señales sobre la imagen o el documento. No son prueba de fraude: son indicios para
decidir si pedir otra foto o revisar a mano. Constaia no es un KYC biométrico y no compara caras.
| Código | Significado |
|---|---|
low_quality | Calidad baja en general. |
blurry | Imagen desenfocada. |
cropped | El documento está recortado. |
glare | Reflejos que tapan datos. |
screen_photo_suspected | Posible foto de una pantalla. |
photocopy_suspected | Posible fotocopia. |
edited_suspected | Posible edición digital. |
multiple_documents | Hay más de un documento en el fichero. |
side_missing | Falta una cara. |
language_mismatch | El idioma no es el esperado para el tipo. |
Los warnings son indicios, no prueba de autenticidad.
Las señales de calidad (low_quality, blurry, cropped, glare, screen_photo_suspected,
photocopy_suspected, edited_suspected, multiple_documents) se resumen en un único motivo low_quality con
severidad warning, así que llevan el veredicto a review (si no hay un error). side_missing y
language_mismatch aparecen en warnings pero no cambian el veredicto por sí solas.
"verdict": {
"expected": ["es_dni"],
"match": true,
"status": "review",
"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." },
{ "code": "low_quality", "severity": "warning", "message": "La calidad de la imagen es insuficiente (blurry, low_quality)." }
]
},
"warnings": ["blurry", "low_quality"]Si la imagen es tan mala que se rechaza antes del OCR, document es null, match es false, el único motivo es
low_quality (warning) y el análisis no se cobra.
Qué hacer con cada veredicto
| Veredicto | Acción recomendada |
|---|---|
valid | Aceptar automáticamente. Guarda id y los campos que necesites. |
invalid | Rechazar y enseñar al usuario los message de los motivos con severity: "error" para que suba otro documento. |
review | Aceptar de forma provisional o pedir otra foto, y encolar para revisión humana. |
Recuerda además que el análisis puede no haber terminado: con async: true, o si tarda más de 30 s, recibes 202
con status: "queued" o "processing" y el resultado llega por webhook.
import { Constaia, InvalidRequestError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia(); // lee CONSTAIA_API_KEY
type Decision =
| { action: "accept"; documentNumber: string | undefined }
| { action: "reject"; errors: string[] }
| { action: "review"; warnings: string[] }
| { action: "pending"; id: string };
export async function checkDni(path: string, fullName: string): Promise<Decision> {
const analysis = await constaia.analyze(await fromPath(path), {
expect: "es_dni",
checks: { holder: { fullName } },
});
if (analysis.status !== "completed") {
return { action: "pending", id: analysis.id }; // llegará por webhook
}
const reasons = analysis.verdict?.reasons ?? [];
switch (analysis.verdict?.status) {
case "valid":
return { action: "accept", documentNumber: analysis.fields.document_number?.value as string | undefined };
case "invalid":
return {
action: "reject",
errors: reasons.filter((r) => r.severity === "error").map((r) => r.message),
};
default:
return {
action: "review",
warnings: reasons.filter((r) => r.severity === "warning").map((r) => r.message),
};
}
}
try {
console.log(await checkDni("./dni_valid.jpg", "María García López"));
} catch (err) {
if (err instanceof InvalidRequestError) console.error(err.code, err.param, err.message);
else throw err;
}Reacciona a códigos concretos si lo necesitas
Puedes afinar la decisión por código. Por ejemplo, ante not_expired con error pide un documento en vigor; ante
holder con error, avisa de que el documento no es de la persona inscrita. Para cualquier código que no conozcas,
decide solo por su severity.
Varios tipos aceptados
Con expect como lista, match es true si el documento es cualquiera de ellos. Es lo habitual para "documento de
identidad": DNI, NIE/TIE o pasaporte.
{ "file_url": "https://example.com/nie.jpg", "expect": ["es_dni", "es_nie", "passport"] }"document": { "type": "es_nie", "label": "NIE / TIE (España)", "confidence": 0.97, "side": "both", "country": "ESP" },
"verdict": {
"expected": ["es_dni", "es_nie", "passport"],
"match": true,
"status": "valid",
"reasons": [
{ "code": "type_match", "severity": "info", "message": "El documento es NIE / TIE (España)." },
{ "code": "not_expired", "severity": "info", "message": "Vigente hasta el 30/11/2029." }
]
}Los checks se evalúan contra el tipo detectado, así que mira document.type para saber de qué campo leer el número
(document_number en DNI y pasaporte, nie_number en NIE). Puedes consultar los campos de cada tipo con
GET /v1/document-types/{type} o en el catálogo.
Sin expect: verdict es null
Si no mandas expect, Constaia clasifica y extrae, pero no decide:
"document": { "type": "generic", "label": "Otro documento", "confidence": 0.64, "side": null, "country": null },
"verdict": null,
"checks": [],
"warnings": []Úsalo cuando no sabes qué documento te van a subir y quieres enrutar por document.type, o cuando aplicas tus
propias reglas sobre fields. Si solo necesitas saber el tipo, POST /v1/classify cuesta
0,2 créditos; su veredicto solo contiene type_match o type_mismatch.
Siguientes pasos
Webhooks
Recibe eventos firmados de Constaia: cuerpos de cada evento, verificación de la firma Standard Webhooks en siete lenguajes, reintentos, idempotencia y pruebas.
Checks
Referencia de las opciones de checks (vigencia, antigüedad, edad, titular, firma, importes) y de las validaciones deterministas de NIF, MRZ, IBAN y facturas.