Instalación del widget
El widget de Veridia es un único custom element de HTML. Dos etiquetas <script> y una etiqueta <veridia-widget>: eso es toda la instalación.
Qué se instala
Cuando cargás el widget, tu página gana:
- Un custom element
<veridia-widget>que podés poner en cualquier lado - La librería
face-api.js(~1,33 MB, usada para calidad de la selfie + detección de rostro) - El bundle del widget de Veridia (~45 KB minificado)
- Dos CustomEvents:
veridia:completeyveridia:error
Un modelo de detección de rostro (tiny_face_detector, ~196 KB) se carga de forma diferida en la primera captura y queda cacheado después. El costo total con caché fría es de unos 1,6 MB.
HTML plano
La integración más simple posible:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Verificación KYC</title>
<!-- El orden importa: face-api primero, el widget segundo -->
<script src="https://widget.xxuxe.online/face-api.js"></script>
<script type="module" src="https://widget.xxuxe.online/veridia-widget.min.js"></script>
</head>
<body>
<main>
<h1>Verificá tu identidad</h1>
<veridia-widget
id="kyc"
publishable-key="qv_pubt_YOUR_KEY"
api-base="https://api.xxuxe.online"
user-ref="customer-12345"
country="PY"
document-type="dni"
locale="es">
</veridia-widget>
</main>
<script>
document.getElementById('kyc').addEventListener('veridia:complete', (e) => {
console.log('ID de verificación:', e.detail.verificationId);
// Enviá el ID a tu backend para obtener el veredicto
});
</script>
</body>
</html>
React (Create React App, Vite, etc.)
import { useEffect, useRef } from 'react';
export function VeridiaVerification({ userRef, onComplete, onError }) {
const widgetRef = useRef(null);
useEffect(() => {
const node = widgetRef.current;
if (!node) return;
const completeHandler = (e) => onComplete?.(e.detail);
const errorHandler = (e) => onError?.(e.detail);
node.addEventListener('veridia:complete', completeHandler);
node.addEventListener('veridia:error', errorHandler);
return () => {
node.removeEventListener('veridia:complete', completeHandler);
node.removeEventListener('veridia:error', errorHandler);
};
}, [onComplete, onError]);
return (
<veridia-widget
ref={widgetRef}
publishable-key={import.meta.env.VITE_VERIDIA_KEY}
api-base="https://api.xxuxe.online"
user-ref={userRef}
country="PY"
document-type="dni"
locale="es"
/>
);
}
Cargá los scripts una sola vez en index.html:
<script src="https://widget.xxuxe.online/face-api.js"></script>
<script type="module" src="https://widget.xxuxe.online/veridia-widget.min.js"></script>
TypeScript: agregá esto a un archivo *.d.ts para que JSX acepte el custom element:
declare namespace JSX {
interface IntrinsicElements {
'veridia-widget': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'publishable-key': string;
'api-base'?: string;
'user-ref'?: string;
'country'?: string;
'document-type'?: string;
'submitted-full-name'?: string;
'require-doc-back'?: string;
'locale'?: string;
'accent-color'?: string;
'active-liveness'?: string;
},
HTMLElement
>;
}
}
Next.js
En Next.js, los custom elements tienen que cargarse después de la hidratación. Usá next/script con strategy="afterInteractive":
// app/layout.tsx (App Router)
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html lang="es">
<body>
{children}
<Script
src="https://widget.xxuxe.online/face-api.js"
strategy="afterInteractive"
/>
<Script
src="https://widget.xxuxe.online/veridia-widget.min.js"
type="module"
strategy="afterInteractive"
/>
</body>
</html>
);
}
// app/onboarding/kyc/page.tsx
'use client';
import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
export default function KycPage() {
const widgetRef = useRef(null);
const router = useRouter();
useEffect(() => {
const node = widgetRef.current;
if (!node) return;
const handler = async (e) => {
const { verificationId } = e.detail;
// Enviar a tu backend
await fetch('/api/kyc/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ verificationId }),
});
router.push('/onboarding/success');
};
node.addEventListener('veridia:complete', handler);
return () => node.removeEventListener('veridia:complete', handler);
}, [router]);
return (
<main>
<h1>Verificá tu identidad</h1>
<veridia-widget
ref={widgetRef}
publishable-key={process.env.NEXT_PUBLIC_VERIDIA_KEY}
api-base="https://api.xxuxe.online"
locale="es"
/>
</main>
);
}
Vue 3
Vue maneja los custom elements automáticamente. No hace falta configuración especial:
<template>
<veridia-widget
ref="widget"
:publishable-key="publishableKey"
api-base="https://api.xxuxe.online"
:user-ref="userRef"
locale="es"
/>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps({
publishableKey: { type: String, required: true },
userRef: { type: String, required: true },
});
const emit = defineEmits(['complete', 'error']);
const widget = ref(null);
const completeHandler = (e) => emit('complete', e.detail);
const errorHandler = (e) => emit('error', e.detail);
onMounted(() => {
widget.value?.addEventListener('veridia:complete', completeHandler);
widget.value?.addEventListener('veridia:error', errorHandler);
});
onUnmounted(() => {
widget.value?.removeEventListener('veridia:complete', completeHandler);
widget.value?.removeEventListener('veridia:error', errorHandler);
});
</script>
Cargá los scripts en index.html:
<script src="https://widget.xxuxe.online/face-api.js"></script>
<script type="module" src="https://widget.xxuxe.online/veridia-widget.min.js"></script>
Angular
En Angular tenés que permitir los custom elements en tu módulo:
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
// ...
})
export class AppModule {}
Después usalo en los templates:
<veridia-widget
#widget
[attr.publishable-key]="publishableKey"
api-base="https://api.xxuxe.online"
[attr.user-ref]="userRef"
locale="es"
(veridia:complete)="onComplete($event)"
(veridia:error)="onError($event)">
</veridia-widget>
WebView móvil
El widget funciona dentro de iOS WebView y Android WebView. Dos requisitos:
- Permisos de cámara: la app anfitriona debe conceder acceso a la cámara. En iOS, poné
WKWebView.allowsInlineMediaPlayback = true. En Android, sobreescribíonPermissionRequestpara concederRESOURCE_VIDEO_CAPTURE. - HTTPS: incluso dentro de un WebView, el widget se niega a correr sobre orígenes inseguros.
Troubleshooting
El widget muestra una pantalla de error genérica
La pantalla de error del widget siempre muestra el mismo texto genérico, sea cual sea la falla. Para saber qué pasó realmente, escuchá veridia:error y leé e.detail.code:
document.querySelector('veridia-widget')
.addEventListener('veridia:error', (e) => console.error(e.detail.code, e.detail.message));
Las causas probables, en el orden en que conviene revisarlas:
code | Qué revisar |
|---|---|
invalid_api_key | La clave falta en el elemento, está mal escrita, fue revocada, o pertenece al otro entorno (qv_pubt_ es test, qv_pub_ es producción) |
insufficient_credits | El saldo de tu tenant es 0. Común en una cuenta de prueba que se quedó sin crédito, y fácil de confundir con un problema de configuración |
camera_denied | El usuario denegó la cámara. En iOS Safari esto queda pegado: volver a montar el widget no vuelve a pedir permiso |
api_unreachable | api-base incorrecto, falla de red, o un CSP bloqueando connect-src |
No es una causa probable: los orígenes permitidos. Toda clave se crea con la lista de orígenes vacía, y una lista vacía permite todos los orígenes, así que por defecto no se está bloqueando nada por ese motivo. El panel tampoco expone hoy ese campo. Ver Autenticación.
Tené en cuenta además que los orígenes se comparan contra el hostname pelado (yourapp.com, localhost), nunca contra una URL completa. https://yourapp.com y http://localhost:3000 no van a coincidir con nada.
El widget carga pero nunca aparece el botón de inicio
Normalmente es que face-api.js no cargó. Revisá la consola del navegador por 404 o errores de CSP.
Solución: verificá las URLs de los scripts:
https://widget.xxuxe.online/face-api.jsdebería devolver200https://widget.xxuxe.online/veridia-widget.min.jsdebería devolver200
"Cámara no disponible"
El usuario denegó el permiso de cámara, o la página no está en HTTPS.
Solución: asegurate de servir tu página sobre HTTPS. Los navegadores bloquean la cámara en http:// (excepto localhost).
Las capturas se ven borrosas / el botón de confirmar queda deshabilitado
Los dispositivos móviles enfocan mejor que las laptops. Decile a los usuarios que mantengan el pulso firme y se tomen su tiempo.
Esto nunca emite un evento: el desenfoque se maneja inline, en la pantalla de revisión, con un pedido de repetir la toma. Y no bloquea indefinidamente: después de dos cuadros rechazados consecutivos el widget acepta la foto igual, porque un control de calidad equivocado sobre un documento perfectamente legible dejaría al usuario atrapado para siempre. Un documento genuinamente inutilizable igual falla en los controles propios del backend.
En los pasos de documento el usuario también tiene la opción de subir la foto desde su galería, muchas veces la forma más rápida de pasar una webcam de escritorio testaruda. Ver Personalización.
React: advertencia sobre atributo desconocido
React no reconoce los custom elements por defecto. Agregá la declaración de TypeScript de arriba, o poné suppressHydrationWarning en el padre.
Next.js: document is not defined durante el build
El widget usa APIs del navegador. Marcá tu página como client component ('use client') y asegurate de que los scripts usen strategy="afterInteractive".
Errores de CSP (Content Security Policy)
Si tu sitio tiene un CSP estricto, agregá estas directivas:
script-src 'self' https://widget.xxuxe.online;
connect-src 'self' https://api.xxuxe.online https://widget.xxuxe.online;
img-src 'self' blob: data: https://widget.xxuxe.online;
media-src 'self' blob:;
style-src 'self' 'unsafe-inline';
worker-src blob:;
Tres de esas líneas son fáciles de equivocar, y dos de los errores fallan en silencio:
connect-srcnecesitaapi.xxuxe.onlineporque las imágenes se suben al Worker de Veridia, no a una URL prefirmada de R2. Poner en la lista permitida*.r2.cloudflarestorage.comno sirve de nada: nunca se hace un request ahí.connect-srctambién necesitawidget.xxuxe.online. El modelo de detección de rostro se descarga desde donde se sirvió el bundle del widget. Si falta esto, la descarga del modelo queda bloqueada, el widget captura la falla con unconsole.warny sigue funcionando sin detección de rostro: ningún error visible, pero perdés el control de "no hay rostro en la selfie" y el score de prueba de vida que se envía a/submit. Una degradación silenciosa de una señal antifraude, causada por un CSP que parece correcto.style-srcdebe permitir estilos inline. El widget inyecta un elemento<style>dentro de su shadow root en cada render. Bajo undefault-src 'self'estricto sinstyle-src, el widget se renderiza completamente sin estilos — sin colores de botón, sin guía de encuadre, layout roto — y nada aparece en la pestaña de red que lo explique.
El widget muestra el idioma equivocado
Definí el atributo locale de forma explícita:
<veridia-widget locale="es">
El valor por defecto es el idioma del navegador, con fallback a inglés.
Verificar la instalación
Una instalación funcionando debería pasar esta checklist:
| Control | Cómo verificarlo |
|---|---|
| Los scripts cargan | Pestaña Network de DevTools — ambos en 200 |
| Custom element registrado | document.querySelector('veridia-widget') devuelve un elemento |
| Aparece el botón de inicio | Botón "Comenzar" / "Start" visible |
| Aparece el pedido de cámara | Después de hacer clic en Comenzar |
| La captura tiene éxito | El evento veridia:complete se dispara con un verificationId |
Qué sigue
- Configuración del widget — cada atributo y su valor por defecto real
- Eventos — los dos eventos y la tabla completa de códigos de error
- Ejemplos — integraciones completas, incluyendo el traspaso al backend
- Referencia de la API — la API del lado del servidor para obtener veredictos