Skip to main content

Widget installation

The Veridia widget is a single HTML custom element. Two <script> tags and a <veridia-widget> tag — that's the entire installation.

What gets installed

When you load the widget, your page gains:

  • A <veridia-widget> custom element you can drop anywhere
  • The face-api.js library (~1.33 MB, used for selfie quality + face detection)
  • The Veridia widget bundle (~45 KB minified)
  • Two CustomEvents: veridia:complete and veridia:error

One face detection model (tiny_face_detector, ~196 KB) is loaded lazily on first capture and cached after that. Total cold-cache cost is about 1.6 MB.

Plain HTML

The simplest possible integration:

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KYC Verification</title>

<!-- Order matters: face-api first, widget second -->
<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>Verify your identity</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('Verification ID:', e.detail.verificationId);
// Send the ID to your backend to fetch the verdict
});
</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"
/>
);
}

Load the scripts once in 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: add this to a *.d.ts file so JSX accepts the 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

In Next.js, custom elements need to load after hydration. Use next/script with 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;

// Send to your 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>Verify your identity</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 treats custom elements automatically. No special config needed:

<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>

Load the scripts in 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

In Angular, you need to allow custom elements in your module:

// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';

@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
// ...
})
export class AppModule {}

Then use in 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>

Mobile webview

The widget works inside iOS WebView and Android WebView. Two requirements:

  1. Camera permissions: the host app must grant camera access. On iOS, set WKWebView.allowsInlineMediaPlayback = true. On Android, override onPermissionRequest to grant RESOURCE_VIDEO_CAPTURE.
  2. HTTPS: even in WebView, the widget refuses to run on insecure origins.

Troubleshooting

The widget shows a generic error screen

The widget's error screen always shows the same generic text, whatever went wrong. To find out what actually happened, listen for veridia:error and read e.detail.code:

document.querySelector('veridia-widget')
.addEventListener('veridia:error', (e) => console.error(e.detail.code, e.detail.message));

The likely causes, in the order you should check them:

codeWhat to check
invalid_api_keyThe key is missing from the element, mistyped, revoked, or belongs to the other environment (qv_pubt_ is test, qv_pub_ is live)
insufficient_creditsYour tenant balance is 0. Common on a test account that ran out — and easy to mistake for a configuration problem
camera_deniedThe user denied the camera. On iOS Safari this is sticky: re-mounting will not re-prompt
api_unreachableWrong api-base, network failure, or CSP blocking connect-src

Not a likely cause: allowed origins. Every key is created with an empty origin list, and an empty list permits all origins — so by default nothing is being blocked on that basis. The dashboard does not currently expose the field either. See Authentication.

Note also that origins are matched against the bare hostname (yourapp.com, localhost), never a full URL. https://yourapp.com and http://localhost:3000 will not match anything.

Widget loads but never shows the Start button

Usually face-api.js failed to load. Check the browser console for 404s or CSP errors.

Fix: verify the script URLs:

  • https://widget.xxuxe.online/face-api.js should return 200
  • https://widget.xxuxe.online/veridia-widget.min.js should return 200

"Camera not available"

The user denied camera permission, or the page isn't HTTPS.

Fix: ensure your page is served over HTTPS. Browsers block camera on http:// (except localhost).

Captures look blurry / the confirm button stays disabled

Mobile devices auto-focus better than laptops. Tell users to hold steady and take their time.

This never emits an event — blur is handled inline, on the review screen, with a retake prompt. And it does not block indefinitely: after two consecutive rejected frames the widget accepts the photo anyway, because a quality check that is wrong about a perfectly readable document would otherwise trap the user forever. A genuinely unusable document still fails on the backend's own gates.

On document steps the user also has the option to upload the photo from their gallery instead — often the fastest way past a stubborn desktop webcam. See Customization.

React: warning about unknown attribute

React doesn't recognize custom elements by default. Add the TypeScript declaration above, or set suppressHydrationWarning on the parent.

Next.js: document is not defined during build

The widget uses browser APIs. Mark your page as a client component ('use client') and ensure scripts use strategy="afterInteractive".

CSP (Content Security Policy) errors

If your site has a strict CSP, add these directives:

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:;

Three of those lines are easy to get wrong, and two of the mistakes fail silently:

  • connect-src needs api.xxuxe.online because images are uploaded to the Veridia Worker, not to a presigned R2 URL. Allow-listing *.r2.cloudflarestorage.com does nothing — no request is ever made there.
  • connect-src also needs widget.xxuxe.online. The face detection model is fetched from wherever the widget bundle was served. If this is missing, the model fetch is blocked, the widget catches the failure with a console.warn and keeps working without face detection — no visible error, but you lose the "no face in the selfie" gate and the liveness score sent to /submit. A silent downgrade of an anti-fraud signal, caused by a CSP that looks correct.
  • style-src must allow inline styles. The widget injects a <style> element inside its shadow root on every render. Under a strict default-src 'self' with no style-src, the widget renders completely unstyled — no button colors, no framing guide, broken layout — and nothing appears in the network tab to explain it.

Widget shows the wrong language

Set the locale attribute explicitly:

<veridia-widget locale="es">

Default is browser locale, fallback to English.

Verifying installation

A working installation should pass this checklist:

CheckHow to verify
Scripts loadDevTools Network tab — both 200
Custom element registereddocument.querySelector('veridia-widget') returns an element
Start button showsVisible "Comenzar" / "Start" button
Camera prompt appearsAfter clicking Start
Capture succeedsveridia:complete event fires with a verificationId

What's next

  • Widget configuration — every attribute and its real default
  • Events — the two events and the full error-code table
  • Examples — complete integrations, including the backend hand-off
  • API Reference — server-side API for fetching verdicts