Saltar al contenido principal

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:complete y veridia: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:

  1. Permisos de cámara: la app anfitriona debe conceder acceso a la cámara. En iOS, poné WKWebView.allowsInlineMediaPlayback = true. En Android, sobreescribí onPermissionRequest para conceder RESOURCE_VIDEO_CAPTURE.
  2. 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:

codeQué revisar
invalid_api_keyLa 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_creditsEl 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_deniedEl usuario denegó la cámara. En iOS Safari esto queda pegado: volver a montar el widget no vuelve a pedir permiso
api_unreachableapi-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.js debería devolver 200
  • https://widget.xxuxe.online/veridia-widget.min.js debería devolver 200

"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-src necesita api.xxuxe.online porque las imágenes se suben al Worker de Veridia, no a una URL prefirmada de R2. Poner en la lista permitida *.r2.cloudflarestorage.com no sirve de nada: nunca se hace un request ahí.
  • connect-src también necesita widget.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 un console.warn y 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-src debe permitir estilos inline. El widget inyecta un elemento <style> dentro de su shadow root en cada render. Bajo un default-src 'self' estricto sin style-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:

ControlCómo verificarlo
Los scripts carganPestaña Network de DevTools — ambos en 200
Custom element registradodocument.querySelector('veridia-widget') devuelve un elemento
Aparece el botón de inicioBotón "Comenzar" / "Start" visible
Aparece el pedido de cámaraDespués de hacer clic en Comenzar
La captura tiene éxitoEl evento veridia:complete se dispara con un verificationId

Qué sigue