Saltar al contenido principal

GET /v1/verify/:id

Obtiene el estado actual de una verificación, y el veredicto una vez que el pipeline terminó.

GET https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2

Autenticación

Este endpoint requiere una clave secreta. Una clave publicable se rechaza con 401 secret_key_required, aunque a esa misma clave se le haya permitido crear y enviar la verificación.

Authorization: Bearer qv_sec_YOUR_SECRET_KEY

En modo test el prefijo es qv_sect_.

El motivo no es arbitrario: una clave publicable viaja dentro del código fuente de tu página, donde cualquiera puede leerla. Si pudiera leer veredictos, el resultado KYC de cada cliente — incluyendo su número de documento y su fecha de nacimiento — estaría a un fetch de distancia para cualquiera con las herramientas de desarrollo abiertas. Así que los resultados son solo del lado del servidor, por construcción.

Si necesitás que un navegador sepa que el flujo terminó, el evento veridia:complete del widget le avisa que las imágenes fueron enviadas. Deliberadamente no lleva el veredicto. Decidí en el servidor.

Parámetro de path

ParámetroTipoDescripción
:idstringEl verificationId de /init, por ejemplo vf_AG07CDWRRFQV4T05ZXG2

status y verdict son ejes distintos

Leé esto antes de escribir cualquier lógica de ramificación. Es el error más caro que permite esta API.

CampoPregunta que respondeValores
status¿Corrió el pipeline?queued, processing, completed, failed
verdict¿Pasó la persona?approved, review, rejected

status: "completed" significa que la maquinaria terminó su trabajo. Todo solicitante rechazado también llega a completed — así se ve un rechazo exitoso.

// MAL — esto da de alta a todos los solicitantes que el sistema rechazó.
// No lanza ningún error, no registra nada inusual, y se ve bien en las pruebas
// mientras todos tus usuarios de prueba pasen.
if (data.status === 'completed') {
await enableAccount(userId);
}

// BIEN — los dos ejes verificados por separado
if (data.status === 'completed') {
if (data.verdict === 'approved') await enableAccount(userId);
else if (data.verdict === 'review') await queueForManualReview(userId);
else if (data.verdict === 'rejected') await blockOnboarding(userId);
}

Una segunda trampa de la misma familia: review es un veredicto final, no uno transitorio. El pipeline terminó; ahora tiene que actuar una persona. Hacer polling esperando que review se resuelva solo espera para siempre. La resolución llega como un evento de webhook nuevo cuando un revisor decide.

Ejemplo de request

curl

curl -X GET https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2 \
-H "Authorization: Bearer qv_sec_YOUR_SECRET_KEY"

JavaScript / Node.js

const response = await fetch(
`https://api.xxuxe.online/v1/verify/${verificationId}`,
{ headers: { 'Authorization': `Bearer ${process.env.VERIDIA_SECRET_KEY}` } }
);
const data = await response.json();
console.log(data.status, data.verdict);

Python

import os
import requests

response = requests.get(
f"https://api.xxuxe.online/v1/verify/{verification_id}",
headers={"Authorization": f"Bearer {os.environ['VERIDIA_SECRET_KEY']}"},
)
response.raise_for_status()
data = response.json()
print(data["status"], data.get("verdict"))

Respuesta

200 OK

Mientras procesa:

{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "processing",
"verdict": null,
"confidence": null,
"scores": null,
"flags": null,
"submittedAt": "2026-05-01T18:39:05Z",
"completedAt": null
}
Las claves están presentes con null, no ausentes

Mientras una verificación está en vuelo, verdict, confidence, scores, flags y completedAt se devuelven como null de JSON — las claves existen.

Así que 'verdict' in data es true desde el primerísimo poll, y data.verdict !== undefined también es true. Ninguna de las dos es una prueba válida de "¿ya terminó?". Verificá status, o verificá si el valor es null.

En clientes tipados esto importa todavía más: un campo modelado como string no opcional va a fallar al deserializar en el primer poll.

Cuando está completa:

{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "completed",
"verdict": "approved",
"confidence": 93.4,
"scores": {
"ocr_confidence": 78.0,
"face_match": 96.2,
"liveness": 91.5,
"doc_quality": 85.0,
"mrz_valid": 100.0,
"name_match": 97.0
},
"flags": [
{ "level": "ok", "text": "auto_approved_all_checks_passed" },
{ "level": "ok", "text": "mrz_checksums_valid" }
],
"submittedAt": "2026-05-01T18:39:05Z",
"completedAt": "2026-05-01T18:39:08Z"
}

Cuando necesita revisión manual:

{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "completed",
"verdict": "review",
"confidence": 64.5,
"scores": {
"ocr_confidence": 88.0,
"face_match": 71.2,
"liveness": null,
"doc_quality": 55.0,
"mrz_valid": 0.0,
"name_match": 62.0
},
"flags": [
{ "level": "warn", "text": "mrz_checksum_failed" },
{ "level": "warn", "text": "heavy_glare" }
],
"submittedAt": "2026-05-01T18:39:05Z",
"completedAt": "2026-05-01T18:39:08Z"
}

Campos de la respuesta

CampoTipoPresenteDescripción
verificationIdstringSiempreEl ID de la verificación
statusstringSiemprequeued, processing, completed, failed
verdictstring | nullnull hasta completedapproved, review, rejected
confidencenumber | nullnull hasta completedScore ponderado general, 0-100
scoresobject | nullnull hasta completedDesglose de señales — ver abajo
flagsarray | nullnull hasta completedObjetos { level, text }
submittedAtstringSiempreISO 8601 UTC, cuándo se llamó a /submit
completedAtstring | nullnull hasta el estado terminalISO 8601 UTC

scores — las claves son snake_case

Seis claves, todas en snake_case. Este es el campo que los integradores más seguido escriben mal, porque el error es silencioso.

ClaveRangoSignificado
ocr_confidence0-100La confianza autodeclarada del modelo de extracción sobre el texto del documento
face_match0-100Similitud biométrica entre la selfie y la foto del documento
liveness0-100 o nullSeñal anti-spoofing
doc_quality0-100Calidad de la imagen del documento (nitidez, reflejos, resolución, muaré)
mrz_valid0-100Validez del checksum de la zona de lectura mecánica (MRZ)
name_match0-100Coincidencia difusa entre submittedFullName y el nombre extraído por OCR

Dos cosas con las que hay que tener cuidado:

No existe faceMatch. En JavaScript, scores.faceMatch es undefined, y undefined < 70 evalúa a false. Así que un umbral escrito en camelCase no lanza error — simplemente nunca se dispara, en silencio, y todos los solicitantes pasan tu control. Si estás portando un umbral desde una versión vieja de esta documentación, esta es la línea a corregir.

liveness puede ser null. Es null cuando no se produjo señal de prueba de vida o el modelo de prueba de vida dio error. Es el único miembro anulable de scores. Protegelo antes de hacer aritmética:

const liveness = data.scores.liveness;
if (liveness !== null && liveness < 50) {
// ...
}

mrz_valid y name_match son las dos señales de fraude documental que más seguido se pasan por alto. name_match en particular es el score producido por el submittedFullName que mandaste a /init — si mandás ese campo, leé este score.

flags

flags es un array de objetos, no de strings:

{ "level": "warn", "text": "heavy_glare" }

Niveles

Hay exactamente tres, y uno de ellos significa bueno:

NivelSignificado
okUn control pasó. Es una señal positiva, no un problema
warnAlgo no cierra pero no es descalificante
errUna señal seria — falla dura, coincidencia con sanciones, o indicador de falsificación

No existe info ni critical. Dos consecuencias que vale la pena decir:

  • flags.some(f => f.level === 'critical') es siempre false. Las señales serias son err. Una regla de prioridad para revisores escrita contra critical nunca se dispara, y los casos que más necesitan una persona reciben prioridad normal.
  • Un array flags no vacío no significa que algo esté mal. Toda verificación aprobada lleva al menos { "level": "ok", "text": "auto_approved_all_checks_passed" }. Tratar flags.length > 0 como "problema" manda el 100% de tus aprobaciones limpias a revisión manual.

Filtrá por nivel:

const problems = data.flags.filter(f => f.level !== 'ok');
const serious = data.flags.filter(f => f.level === 'err');

Flags que vas a ver realmente

textNivel típicoSignificado
auto_approved_all_checks_passedokAprobada sin fallas duras. Siempre primera cuando está presente
image_blurrywarnNitidez por debajo del umbral
heavy_glarewarnReflejo que oscurece el documento
possible_screen_capturewarnPatrón de muaré — el "documento" puede ser una foto de una pantalla
low_resolutionwarnResolución de imagen demasiado baja
mrz_checksums_validokChecksums de MRZ verificados
mrz_checksum_failedwarn / errMRZ presente pero los checksums no verifican
mrz_viz_consistentokLa MRZ concuerda con los campos impresos
mrz_viz_mismatcherrMRZ válida que contradice los campos impresos — señal fuerte de falsificación
no_face_detected_on_documenterrNo se encontró cara en la foto del documento
no_face_detected_on_selfieerrNo se encontró cara en la selfie
face_match_below_critical_thresholderrEs muy improbable que la selfie y la foto del documento sean la misma persona
document_quality_unusableerrImagen del documento demasiado degradada para evaluar
active_liveness_spooferrEl reto de prueba de vida activa concluyó que la captura no era en vivo
active_liveness_liveokEl reto pasó
aml_sanctions_matcherrCoincidencia fuerte contra una lista oficial de sanciones
aml_possible_matchwarnCoincidencia de sanciones más débil, vale la pena mirarla
missing_imageserrLas imágenes esperadas no estaban presentes al momento del pipeline

Los tres que cargan peso regulatorio o de fraude y son más fáciles de pasar por alto: possible_screen_capture (inyección de imagen), mrz_viz_mismatch (documento fabricado) y aml_sanctions_match (la persona está en una lista de sanciones). Ruteá esos a algún lugar donde los vea una persona.

No muestres el texto de los flags a tu usuario final. Decirle a alguien qué control no pasó le dice a un atacante exactamente qué corregir.

Cómo se decide el veredicto

VeredictoCondición
approvedconfidence >= 90, y sin fallas duras, y sin retención por cumplimiento
reviewCualquier cosa en el medio — incluyendo todo caso con una falla dura que no sea catastrófica
rejectedconfidence < 60

Tres reglas que los números por sí solos no te dicen:

  1. Una falla dura nunca auto-aprueba, sea cual sea el score. Cualquier falla dura de nivel err limita el resultado a review, o a rejected si además la confianza está por debajo de 60.
  2. Una coincidencia fuerte con sanciones fuerza review. Degrada un caso que de otro modo sería aprobado; nunca auto-rechaza. Un hit de sanciones es una decisión de cumplimiento para una persona, no para una máquina.
  3. La banda 60-89 es review, no approved. Si estás acostumbrado a un umbral de 80, esta es la brecha que va a llenarte la cola manual sin que lo esperes.

Los umbrales son parámetros a nivel de despliegue, no configuración por tenant — hoy no hay una perilla por cliente para ellos.

Si reimplementás el umbral de tu lado leyendo confidence, vas a perder las reglas 1 y 2 y vas a auto-aprobar casos que el sistema retuvo deliberadamente, incluyendo coincidencias con sanciones. Leé verdict.

Máquina de estados de status

queued -> processing -> completed
-> failed
StatusSignificado
queuedEsperando en la cola del backend
processingPipeline corriendo
completedEl pipeline terminó. Ahora leé verdict
failedEl pipeline no pudo completarse. No hay veredicto — no leas uno

failed no es un rechazo. Significa que el sistema no pudo llegar a una conclusión. Tratalo como un caso de reintentar-o-escalar, no como una decisión sobre el solicitante.

Polling

Los webhooks son mejores: se disparan apenas el veredicto está listo, y además entregan el evento posterior de cuando una persona resuelve un review. El polling no puede ver ese segundo resultado a menos que sigas haciendo polling indefinidamente. Ver Webhooks.

Si tenés que hacer polling:

async function waitForVerdict(verificationId, timeoutMs = 30000) {
const start = Date.now();
let interval = 1000; // 1s — ver la nota sobre límites de tasa más abajo

while (Date.now() - start < timeoutMs) {
const response = await fetch(
`https://api.xxuxe.online/v1/verify/${verificationId}`,
{ headers: { 'Authorization': `Bearer ${process.env.VERIDIA_SECRET_KEY}` } }
);
const data = await response.json();

if (data.status === 'completed' || data.status === 'failed') {
return data; // quien llama todavía debe ramificar sobre data.verdict
}

await new Promise(r => setTimeout(r, interval));
interval = Math.min(interval * 1.5, 3000);
}

throw new Error('Verification timed out');
}

Notá que el loop retorna con cualquiera de los dos estados terminales. Quien llama debe manejar failed, donde verdict es null.

No hagas polling más rápido que 1 Hz

El límite por tenant acá es de 600/minuto, pero el límite por IP es de 100/minuto y se verifica primero. Hacer polling desde un servidor cada 500 ms son 120 requests por minuto y va a ser limitado mucho antes que el límite de tenant. Ver Límites de tasa.

Errores

HTTPCódigo de errorCuándo
401missing_api_keySin header Authorization
401invalid_api_keyClave revocada, malformada, o que nunca existió
401secret_key_requiredSe usó una clave publicable. El error más común en este endpoint
404verification_not_foundEl ID no existe, está malformado, o pertenece a otro tenant
429rate_limitedLímite por IP o por tenant
500internal_errorReportá el requestId
503backend_unavailableBackend inalcanzable — transitorio, reintentá con backoff

Si llegás acá desde /init y /submit reutilizando la misma clave y recibís un 401, mirá el código antes de tocar la clave. secret_key_required significa que la clave es perfectamente válida — simplemente es la familia equivocada para esta llamada. Rotarla no va a ayudar.

Una verificación que pertenece a otro tenant devuelve 404, no 403: quien no es dueño de una verificación no debería enterarse de que existe.

Catálogo completo: Errores.

Notas

  • Las verificaciones no se vuelven ilegibles con el tiempo. La pertenencia se verifica contra el tenant registrado con la verificación misma, así que este endpoint sigue funcionando mucho después de que expiró la intención de subida de una hora. Esa expiración aplica a las subidas y a /submit, no a la lectura de resultados.
  • Una vez en completed o failed, la respuesta de una corrida automatizada dada es estable — pero un caso en review puede cambiar más adelante cuando lo resuelve una persona. Si cacheás, invalidá con el webhook.
  • submittedAt y completedAt son ISO 8601 en UTC. (expiresAt en /init es distinto — ese es en segundos Unix.)
  • userRef no se devuelve acá. Viaja solo en los webhooks. Mantené tu propio mapeo verificationId → usuario.

Qué sigue

  • Webhooks — recibí veredictos empujados, incluyendo los resultados de revisión humana
  • Errores — referencia completa de códigos de error
  • Límites de tasa — por qué el polling toca un límite antes de lo que esperarías