Constaia
Conceptos

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

Respuesta de POST /v1/analyze (extracto)
"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." }
  ]
}
CampoTipoDescripción
expectedstring[]Los tipos que pediste en expect, siempre como lista.
matchbooleantrue si el tipo detectado (document.type) está entre los esperados.
statusvalid | invalid | reviewLa decisión.
reasonsobjeto[]Cada regla evaluada, con code estable, severity y message legible.

Cada motivo tiene esta forma:

CampoTipoDescripción
codestringCódigo estable. Programa tu lógica contra él.
severityinfo | warning | errorinfo = la regla se cumple; warning = no se puede confirmar; error = la regla falla.
messagestringTexto para personas en el idioma de language (es, en, pt, fr). Puede cambiar de redacción: no lo compares como texto.

Los tres estados

EstadoCuándo
VálidoNingún motivo tiene severidad warning ni error.
No válidoAl menos un motivo tiene severidad error: tipo distinto, caducado, titular que no coincide, letra del NIF incorrecta, importe distinto…
RevisarNo 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:

Cálculo de verdict.status
si algún motivo tiene severity "error"   → invalid
si no, si alguno tiene severity "warning" → review
si no                                      → valid

error 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

  1. Tipo: siempre hay exactamente uno entre type_match, type_mismatch o type_unknown.
  2. Checks que pediste (y los que están activos por defecto, como not_expired en documentos con caducidad): uno o varios motivos por cada uno. Detalle en checks.
  3. Reglas propias del tipo: not_fit_for_sport (certificado médico con "no apto"), has_records (certificados de antecedentes).
  4. Comprobaciones deterministas que fallan: si un elemento de checks[] tiene passed: false, se añade un motivo con su mismo code y severidad error. Las que pasan solo aparecen en checks[], no en reasons.
  5. Calidad y confianza: low_quality y low_confidence, con severidad warning.
  6. PDF: el resultado de la firma electrónica (signature_valid, signature_invalid…) y edited_suspected si los metadatos apuntan a una edición. Ver firmas digitales en PDF.
  7. 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ódigoSeveridades posiblesCuándo aparece
type_matchinfoEl tipo detectado es uno de los de expect.
type_mismatcherrorSe detectó con confianza otro tipo del catálogo.
type_unknownwarningNo se reconoce el tipo (generic) o la confianza de la clasificación es menor de 0,5.
not_expiredinfo, warning, errorVigencia. warning si no se pudo leer la fecha de caducidad.
max_age_daysinfo, warning, errorAntigüedad de la fecha de emisión. warning si no hay fecha de emisión.
ageinfo, warningEdad dentro de los límites pedidos (info) o fecha de nacimiento ilegible (warning).
min_age_yearserrorEl titular es más joven que min_age_years.
max_age_yearserrorEl titular es mayor que max_age_years.
holderinfo, warning, errorCoincidencia con los datos del titular. warning si el documento no muestra ese dato.
required_field_missingerrorFalta un campo de require_fields (un motivo por campo).
require_signatureinfo, errorFirma presente o ausente.
require_stampinfo, errorSello presente o ausente.
expected_amountinfo, errorImporte (amount, o total en invoice) igual o distinto al esperado.
expected_ibaninfo, errorEl IBAN esperado aparece o no en el documento.
expected_referenceinfo, errorLa referencia esperada aparece o no en reference o concept.
not_fit_for_sporterrorCertificado médico deportivo que declara "no apto".
has_recordsinfo, errorCertificado de antecedentes: sin antecedentes (info) o con antecedentes (error).
low_qualitywarningImagen borrosa, recortada, con reflejos, foto de pantalla, fotocopia, posible edición o varios documentos.
low_confidencewarningLa confianza global (clasificación y campos clave) está por debajo del umbral del tipo.
nif_check_digiterrorLetra de control de DNI, NIE o CIF incorrecta.
mrz_checksumserrorDígitos de control de la MRZ incorrectos.
mrz_matches_visualerrorLa MRZ no coincide con los datos impresos.
iban_checksumerrorIBAN con dígitos de control incorrectos.
invoice_totalserrorBase, IVA, retención y total de la factura no cuadran.
csv_formaterrorEl código seguro de verificación (CSV) no tiene un formato válido.
date_consistencyerrorFechas incoherentes (nacimiento posterior a la emisión, emisión posterior a la caducidad…).
id_number_checksum, id_number_formatwarning, errorUn identificador nacional no supera su dígito de control o su formato. warning en los identificadores blandos del catálogo.
id_number_matches_birth_dateerrorLa fecha de nacimiento codificada en el identificador no coincide con la impresa.
aamva_matches_visualerrorEl código PDF417 del reverso de un permiso o ID de EE. UU./Canadá no coincide con el anverso.
signature_validinfoEl PDF lleva una firma electrónica íntegra y de confianza. Ver firmas digitales en PDF.
signature_invaliderrorLa firma del PDF está rota: el contenido no coincide con lo firmado.
signature_missingwarning, errorEl 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_signingwarning, errorEl PDF se modificó después de firmarse. error con require_valid_signature.
untrusted_signerwarning, errorLa firma es íntegra pero el certificado no llega a la lista de confianza. error con require_valid_signature.
edited_suspectedwarningLos metadatos del PDF apuntan a un editor (iLovePDF, Word, Canva…) o a cambios posteriores.
i9_listinfoDocumento 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:

DNI vigente
{ "code": "not_expired", "severity": "info", "message": "Vigente hasta el 12/03/2031." }
DNI caducado → invalid
{ "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ó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.

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.

Foto borrosa → review
"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

VeredictoAcción recomendada
validAceptar automáticamente. Guarda id y los campos que necesites.
invalidRechazar y enseñar al usuario los message de los motivos con severity: "error" para que suba otro documento.
reviewAceptar 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.

src/check-dni.ts
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.

Petición
{ "file_url": "https://example.com/nie.jpg", "expect": ["es_dni", "es_nie", "passport"] }
Respuesta (extracto)
"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:

Respuesta sin expect (extracto)
"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

En esta página