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ámetro | Tipo | Descripción |
|---|---|---|
:id | string | El 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.
| Campo | Pregunta que responde | Valores |
|---|---|---|
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
}
null, no ausentesMientras 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
| Campo | Tipo | Presente | Descripción |
|---|---|---|---|
verificationId | string | Siempre | El ID de la verificación |
status | string | Siempre | queued, processing, completed, failed |
verdict | string | null | null hasta completed | approved, review, rejected |
confidence | number | null | null hasta completed | Score ponderado general, 0-100 |
scores | object | null | null hasta completed | Desglose de señales — ver abajo |
flags | array | null | null hasta completed | Objetos { level, text } |
submittedAt | string | Siempre | ISO 8601 UTC, cuándo se llamó a /submit |
completedAt | string | null | null hasta el estado terminal | ISO 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.
| Clave | Rango | Significado |
|---|---|---|
ocr_confidence | 0-100 | La confianza autodeclarada del modelo de extracción sobre el texto del documento |
face_match | 0-100 | Similitud biométrica entre la selfie y la foto del documento |
liveness | 0-100 o null | Señal anti-spoofing |
doc_quality | 0-100 | Calidad de la imagen del documento (nitidez, reflejos, resolución, muaré) |
mrz_valid | 0-100 | Validez del checksum de la zona de lectura mecánica (MRZ) |
name_match | 0-100 | Coincidencia 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:
| Nivel | Significado |
|---|---|
ok | Un control pasó. Es una señal positiva, no un problema |
warn | Algo no cierra pero no es descalificante |
err | Una 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 siemprefalse. Las señales serias sonerr. Una regla de prioridad para revisores escrita contracriticalnunca se dispara, y los casos que más necesitan una persona reciben prioridad normal.- Un array
flagsno vacío no significa que algo esté mal. Toda verificación aprobada lleva al menos{ "level": "ok", "text": "auto_approved_all_checks_passed" }. Tratarflags.length > 0como "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
text | Nivel típico | Significado |
|---|---|---|
auto_approved_all_checks_passed | ok | Aprobada sin fallas duras. Siempre primera cuando está presente |
image_blurry | warn | Nitidez por debajo del umbral |
heavy_glare | warn | Reflejo que oscurece el documento |
possible_screen_capture | warn | Patrón de muaré — el "documento" puede ser una foto de una pantalla |
low_resolution | warn | Resolución de imagen demasiado baja |
mrz_checksums_valid | ok | Checksums de MRZ verificados |
mrz_checksum_failed | warn / err | MRZ presente pero los checksums no verifican |
mrz_viz_consistent | ok | La MRZ concuerda con los campos impresos |
mrz_viz_mismatch | err | MRZ válida que contradice los campos impresos — señal fuerte de falsificación |
no_face_detected_on_document | err | No se encontró cara en la foto del documento |
no_face_detected_on_selfie | err | No se encontró cara en la selfie |
face_match_below_critical_threshold | err | Es muy improbable que la selfie y la foto del documento sean la misma persona |
document_quality_unusable | err | Imagen del documento demasiado degradada para evaluar |
active_liveness_spoof | err | El reto de prueba de vida activa concluyó que la captura no era en vivo |
active_liveness_live | ok | El reto pasó |
aml_sanctions_match | err | Coincidencia fuerte contra una lista oficial de sanciones |
aml_possible_match | warn | Coincidencia de sanciones más débil, vale la pena mirarla |
missing_images | err | Las 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
| Veredicto | Condición |
|---|---|
approved | confidence >= 90, y sin fallas duras, y sin retención por cumplimiento |
review | Cualquier cosa en el medio — incluyendo todo caso con una falla dura que no sea catastrófica |
rejected | confidence < 60 |
Tres reglas que los números por sí solos no te dicen:
- Una falla dura nunca auto-aprueba, sea cual sea el score. Cualquier falla dura de nivel
errlimita el resultado areview, o arejectedsi además la confianza está por debajo de 60. - 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. - La banda 60-89 es
review, noapproved. 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
| Status | Significado |
|---|---|
queued | Esperando en la cola del backend |
processing | Pipeline corriendo |
completed | El pipeline terminó. Ahora leé verdict |
failed | El 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.
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
| HTTP | Código de error | Cuándo |
|---|---|---|
401 | missing_api_key | Sin header Authorization |
401 | invalid_api_key | Clave revocada, malformada, o que nunca existió |
401 | secret_key_required | Se usó una clave publicable. El error más común en este endpoint |
404 | verification_not_found | El ID no existe, está malformado, o pertenece a otro tenant |
429 | rate_limited | Límite por IP o por tenant |
500 | internal_error | Reportá el requestId |
503 | backend_unavailable | Backend 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
completedofailed, la respuesta de una corrida automatizada dada es estable — pero un caso enreviewpuede cambiar más adelante cuando lo resuelve una persona. Si cacheás, invalidá con el webhook. submittedAtycompletedAtson ISO 8601 en UTC. (expiresAten/inites distinto — ese es en segundos Unix.)userRefno se devuelve acá. Viaja solo en los webhooks. Mantené tu propio mapeoverificationId→ 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