Eventos del widget
El widget emite exactamente dos eventos sobre el elemento anfitrión. No hay otros: no existe veridia:start, ni veridia:step, ni veridia:cancel. La cancelación llega como un código de error, no como un evento propio.
Ambos son CustomEvent despachados con bubbles: true y composed: true, así que podés escuchar en el elemento mismo o en cualquier ancestro (incluido document).
veridia:complete
Se dispara después de que las imágenes fueron subidas y POST /v1/verify/submit respondió con éxito.
const widget = document.querySelector('veridia-widget');
widget.addEventListener('veridia:complete', (e) => {
console.log(e.detail);
// { verificationId: "vf_AG07CDWRRFQV4T05ZXG2", status: "queued" }
});
e.detail tiene exactamente estos campos:
| Campo | Tipo | Siempre | Descripción |
|---|---|---|---|
verificationId | string | Sí | El id vf_* de esta verificación |
status | "queued" | "processing" | "completed" | Sí | Estado del pipeline al momento del envío |
verdict | "approved" | "review" | "rejected" | No | El widget nunca lo define |
En este evento no hay userRef
Si pasaste user-ref, no se devuelve acá. Versiones anteriores de esta página afirmaban que sí; era falso, y el código escrito contra eso asociaba veredictos con undefined en silencio.
userRef viaja en el payload del webhook. Tampoco lo devuelve GET /v1/verify/{id}. Si necesitás mapear una verificación de vuelta a tu propio usuario desde el navegador, guardá vos mismo el mapeo en el momento en que se dispara este evento:
widget.addEventListener('veridia:complete', async (e) => {
await fetch('/api/kyc/started', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// ESTA asociación es tuya. El widget no te la va a devolver.
body: JSON.stringify({ verificationId: e.detail.verificationId, userId: currentUser.id }),
});
});
status no es un veredicto
status es el eje del pipeline: queued → processing → completed (o failed). verdict es el eje del resultado: approved / review / rejected.
status: "completed" significa que el pipeline corrió. No dice nada sobre si la persona aprobó. Activar una cuenta cuando status llega a completed admite a todos los solicitantes rechazados: es el error más caro que la API deja disponible, y es fácil de cometer porque la palabra suena a éxito.
Obtené el veredicto desde el webhook o desde un GET /v1/verify/{id} del lado del servidor con una clave secreta.
veridia:error
Se dispara cuando el flujo se detiene de una forma de la que el usuario no puede recuperarse dentro del widget. El widget cambia a su pantalla de error y se detiene.
widget.addEventListener('veridia:error', (e) => {
const { code, message, detail } = e.detail;
console.error(`[veridia] ${code}: ${message}`, detail);
});
e.detail tiene exactamente estos campos:
| Campo | Tipo | Siempre | Descripción |
|---|---|---|---|
code | string | Sí | Código legible por máquina, de la lista de abajo |
message | string | Sí | Texto diagnóstico en inglés — para tus logs, no para tu interfaz |
detail | object | No | Presente solo cuando el error vino de la API |
message no está traducido y no está escrito para usuarios finales. El widget ya le muestra al usuario un mensaje localizado; usá message para logging y soporte.
Códigos de error
Estos son los valores reales de code. Ramificá sobre estas cadenas.
| Código | Causa | Se emite como evento |
|---|---|---|
camera_denied | El usuario denegó el permiso de cámara (NotAllowedError) | Sí |
camera_unavailable | No hay dispositivo de cámara, o getUserMedia falló por cualquier otro motivo | Sí |
upload_failed | Falló la subida de una imagen después de todos los reintentos | Sí |
api_unreachable | Falla de red, 5xx, o backend_unavailable de la API | Sí |
invalid_api_key | Clave publicable faltante, malformada, desconocida o revocada | Sí |
insufficient_credits | El saldo del tenant es 0 (la API devuelve 402) | Sí |
rate_limited | Se alcanzó el límite de tasa (la API devuelve 429) | Sí |
user_cancelled | El usuario presionó Cancelar | Sí |
internal_error | Cualquier otra cosa, incluido invalid_body de la API | Sí |
blurry_image | Cuadro demasiado desenfocado | No — ver abajo |
no_face_in_selfie | No se encontró rostro en la selfie | No — ver abajo |
blurry_image y no_face_in_selfie existen en el tipo público VeridiaErrorCode, pero el widget nunca los despacha. Son motivos de control de calidad que se muestran inline en la pantalla de revisión, pidiendo al usuario que repita la toma. No construyas telemetría que los espere: va a quedar vacía para siempre.
Dos de los códigos alcanzables dominan el tráfico real y son los que más integraciones olvidan:
camera_denied— un desenlace de rutina, no una anomalía. En Safari móvil la denegación queda pegada: volver a montar el widget no vuelve a pedir permiso. Mostrá tus propias instrucciones para reactivar el permiso en la configuración del navegador, y ofrecé un camino alternativo.user_cancelled— el usuario presionó Cancelar. Esto es abandono, no una falla. No lo registres como error, no dispares alertas y no muestres una pantalla de fracaso. Contalo: es tu métrica de abandono del embudo.
Sobre upload_failed
El widget ya reintenta una subida fallida hasta 3 veces con backoff (400 ms, 1200 ms), y solo ante fallas transitorias: caída de red, timeout, abort, 5xx. Un 4xx permanente falla de inmediato.
Así que cuando upload_failed llega a tu handler, los reintentos ya se agotaron. Este código significa un problema de red real y persistente para ese usuario. Instrumentalo: es la señal que te avisa que una región o una operadora entera está fallando.
detail solo está presente en errores de la API
Cuando la falla vino de la API de Veridia, detail lleva el objeto detail de la API tal cual (por ejemplo retry_after en un 429). Cuando la falla es del lado del cliente (camera_denied, user_cancelled), no existe la clave detail en absoluto. Verificá antes de leerla.
Un handler completo
const widget = document.querySelector('veridia-widget');
widget.addEventListener('veridia:complete', async (e) => {
// Envío aceptado. NO es un veredicto. Pasale el id a tu backend.
await fetch('/api/kyc/submitted', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ verificationId: e.detail.verificationId }),
});
showPendingScreen();
});
widget.addEventListener('veridia:error', (e) => {
const { code, message, detail } = e.detail;
switch (code) {
case 'user_cancelled':
// Abandono, no una falla. Métrica, sin alerta, sin interfaz de error.
analytics.track('kyc_abandoned');
showRestartPrompt();
break;
case 'camera_denied':
// Queda pegado en iOS Safari: volver a montar no vuelve a pedir permiso.
showCameraPermissionHelp();
break;
case 'camera_unavailable':
showMessage('No pudimos encontrar una cámara en este dispositivo.');
offerDesktopToMobileHandoff();
break;
case 'upload_failed':
case 'api_unreachable':
// Reintentos ya agotados. Problema real de conectividad.
alerting.warn('veridia_network', { code, message });
showMessage('Problema de conexión. Intentá de nuevo.');
break;
case 'rate_limited':
// detail.retry_after viene en segundos, cuando la API lo proveyó.
showMessage('Demasiados intentos. Esperá un momento.');
alerting.warn('veridia_rate_limited', { retryAfter: detail?.retry_after });
break;
case 'invalid_api_key':
case 'insufficient_credits':
// Problema TUYO, no del usuario. Avisale a alguien.
alerting.critical('veridia_config', { code, message });
showMessage('La verificación no está disponible temporalmente.');
break;
default:
// internal_error y cualquier cosa que se agregue en versiones futuras.
alerting.error('veridia_unknown', { code, message });
showMessage('Algo salió mal. Intentá de nuevo.');
}
});
Dejá la rama default. Si una versión futura del widget agrega un código, este handler degrada a un mensaje genérico en lugar de no hacer nada en silencio.
Qué sigue
- Configuración — cada atributo y su valor por defecto real.
- Ejemplos — estos handlers conectados en integraciones completas.
- Webhooks — donde el veredicto llega realmente.