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.jslibrary (~1.33 MB, used for selfie quality + face detection) - The Veridia widget bundle (~45 KB minified)
- Two CustomEvents:
veridia:completeandveridia: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:
- Camera permissions: the host app must grant camera access. On iOS, set
WKWebView.allowsInlineMediaPlayback = true. On Android, overrideonPermissionRequestto grantRESOURCE_VIDEO_CAPTURE. - 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:
code | What to check |
|---|---|
invalid_api_key | The key is missing from the element, mistyped, revoked, or belongs to the other environment (qv_pubt_ is test, qv_pub_ is live) |
insufficient_credits | Your tenant balance is 0. Common on a test account that ran out — and easy to mistake for a configuration problem |
camera_denied | The user denied the camera. On iOS Safari this is sticky: re-mounting will not re-prompt |
api_unreachable | Wrong 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.jsshould return200https://widget.xxuxe.online/veridia-widget.min.jsshould return200
"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-srcneedsapi.xxuxe.onlinebecause images are uploaded to the Veridia Worker, not to a presigned R2 URL. Allow-listing*.r2.cloudflarestorage.comdoes nothing — no request is ever made there.connect-srcalso needswidget.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 aconsole.warnand 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-srcmust allow inline styles. The widget injects a<style>element inside its shadow root on every render. Under a strictdefault-src 'self'with nostyle-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:
| Check | How to verify |
|---|---|
| Scripts load | DevTools Network tab — both 200 |
| Custom element registered | document.querySelector('veridia-widget') returns an element |
| Start button shows | Visible "Comenzar" / "Start" button |
| Camera prompt appears | After clicking Start |
| Capture succeeds | veridia: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