Installation
1. Get your keys
Sign in to your Veridia dashboard and open the API keys section. You need two keys, from two different families:
- Publishable —
qv_pubt_...(test) orqv_pub_...(live). Goes in the page. This step only needs this one. - Secret —
qv_sect_...(test) orqv_sec_...(live). Stays on your server. You'll need it in step 3 to read the verdict.
Publishable keys are safe to expose in client-side HTML. They identify your tenant and can start and submit verifications, but GET /v1/verify/{id} refuses them, so they can never read a verdict.
A key can carry an allowed origins list, matched against the bare hostname (yourapp.com, wildcards like *.yourapp.com allowed — not https://yourapp.com). The dashboard does not expose this field yet, so every key is created with an empty list, and an empty list permits every origin. There is nothing to configure here, including for localhost. Treat this as a way to scope where your widget runs later, not as an access control.
2. Embed the widget
Drop two script tags and the custom element on any page:
<!-- 1. Load face-api.js (UMD), then the widget bundle (ESM module).
Order matters: the widget reads window.faceapi at startup. -->
<script src="https://widget.xxuxe.online/face-api.js"></script>
<script type="module" src="https://widget.xxuxe.online/veridia-widget.min.js"></script>
<!-- 2. Drop in the widget -->
<veridia-widget
publishable-key="qv_pubt_YOUR_KEY_HERE"
user-ref="customer-12345"
country="PY"
document-type="dni"
locale="es">
</veridia-widget>
<!-- 3. Listen for events -->
<script>
document.querySelector('veridia-widget')
.addEventListener('veridia:complete', (e) => {
// e.detail is { verificationId, status } — the verdict is NOT here.
console.log('verification:', e.detail.verificationId);
});
</script>
That's the full integration. Open the page on a phone or laptop with a camera, hit "Start", and you have a working biometric capture flow.
veridia:complete gives you verificationId and nothing else — not your user-ref. Neither does GET /v1/verify/{id}. Only the webhook echoes userRef back.
So when the event fires, write verificationId → your user id to your own database right away. If you skip this, a verdict arriving later by polling has no way to tell you whose account it belongs to.
Configuration attributes
Ten attributes, all optional except publishable-key.
| Attribute | Required | Default | Description |
|---|---|---|---|
publishable-key | Yes | — | Your qv_pubt_* or qv_pub_* key from the dashboard |
api-base | No | https://api.xxuxe.online | Override the API endpoint. You almost never need this |
user-ref | No | — | Your own user identifier (max 128 chars). Echoed in webhooks only |
country | No | — | ISO 3166-1 alpha-2 country code (PY, BR, MX) — improves OCR accuracy. Normalized to uppercase for you |
document-type | No | — | One of: dni, passport, drivers_license, national_id, other |
submitted-full-name | No | — | The user's full name, fuzzy-matched against the document (max 255 chars). Produces the name_match score |
require-doc-back | No | depends — see below | Whether to capture the back of the document |
locale | No | browser locale, then en | UI language: en, es, or pt |
accent-color | No | #0f172a | Accent for buttons and active states. Any valid CSS color, not just hex |
active-liveness | No | false | Set "true" to enable the server-verified active liveness challenge |
require-doc-back defaults to true for everything except passports
This is the attribute most likely to surprise you, so read the rule carefully.
If you do not set the attribute at all, the widget computes the default as document-type !== 'passport'. A passport gets two captures (front + selfie); everything else — including the case where you set no document-type at all — gets three (front + back + selfie).
And when you do set it, only two literal values turn it off: "false" and "0". Every other string, including "no", "off", and the empty attribute require-doc-back="", resolves to true.
<!-- 3 captures: front, back, selfie -->
<veridia-widget publishable-key="..." document-type="dni"></veridia-widget>
<!-- 2 captures: front, selfie -->
<veridia-widget publishable-key="..." document-type="dni" require-doc-back="false"></veridia-widget>
<!-- 2 captures: passports have no back -->
<veridia-widget publishable-key="..." document-type="passport"></veridia-widget>
Design your funnel around the real count before you build the screens around it.
active-liveness is off by default
Setting active-liveness="true" opts into the server-verified liveness challenge: the API issues a challenge plan at /v1/verify/init and reveals the steps one at a time, so a pre-recorded or injected video cannot satisfy it.
It is off by default, which means an integration that never mentions the attribute runs without it. If your risk model includes someone feeding frames into the browser rather than holding up a real face, turn it on.
Note that it adds capture steps and therefore time to the user's flow, and it makes the verification issue roughly twenty uploads instead of three — relevant to the per-IP rate limit if many of your users share one NAT.
Configuring programmatically
Instead of attributes, you can assign a config object:
const widget = document.querySelector('veridia-widget');
widget.config = {
publishableKey: 'qv_pubt_YOUR_KEY',
userRef: 'customer-12345',
country: 'PY',
documentType: 'dni',
requireDocBack: false,
locale: 'es',
activeLiveness: true,
};
Setting any observed attribute rebuilds the config from the attributes alone, silently discarding whatever you assigned to .config — including publishableKey. If you configure programmatically and then change, say, locale at runtime to switch languages, the widget loses its key and fails with invalid_api_key. Pick one mechanism per widget instance.
Framework examples
React
import { useEffect, useRef } from 'react';
export function VeridiaVerification({ userRef, onComplete }) {
const widgetRef = useRef(null);
useEffect(() => {
const handler = (e) => onComplete(e.detail);
const node = widgetRef.current;
node?.addEventListener('veridia:complete', handler);
return () => node?.removeEventListener('veridia:complete', handler);
}, [onComplete]);
return (
<veridia-widget
ref={widgetRef}
publishable-key={process.env.NEXT_PUBLIC_VERIDIA_KEY}
user-ref={userRef}
locale="es"
/>
);
}
Make sure to load the script tags once in your _document.tsx or 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>
Vue 3
<template>
<veridia-widget
ref="widget"
:publishable-key="publishableKey"
:user-ref="userRef"
locale="es"
/>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps(['publishableKey', 'userRef']);
const emit = defineEmits(['complete']);
const widget = ref(null);
const handler = (e) => emit('complete', e.detail);
onMounted(() => {
widget.value?.addEventListener('veridia:complete', handler);
});
onUnmounted(() => {
widget.value?.removeEventListener('veridia:complete', handler);
});
</script>
Vue treats <veridia-widget> as a custom element automatically — no extra config needed.
Plain JavaScript (no framework)
<!DOCTYPE html>
<html>
<head>
<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>
<veridia-widget
id="kyc"
publishable-key="qv_pubt_YOUR_KEY"
user-ref="user-001"
document-type="dni"
country="PY"
locale="es">
</veridia-widget>
<script>
const widget = document.getElementById('kyc');
widget.addEventListener('veridia:complete', async (e) => {
const { verificationId } = e.detail;
// Record verificationId -> your user id NOW. It is the only handle
// you will get back from polling, and it does not carry user-ref.
await fetch('/api/kyc-started', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ verificationId, userId: 'user-001' }),
});
});
widget.addEventListener('veridia:error', (e) => {
// e.detail is { code, message, detail? }
console.error('Veridia error:', e.detail.code, e.detail.message);
});
</script>
</body>
</html>
Verify it works
Open the page in a browser over HTTPS (or localhost — browsers grant camera access to both, and to nothing else). You should see:
- A "Start" button under the title "Identity verification" (or its translated equivalent)
- After clicking start, a camera permission prompt
- The capture steps — front of document, back of document unless you turned it off, then selfie
- A "Submitted ✓" confirmation screen
On the document steps the user may either use the camera or pick an existing image from their gallery. On the selfie and liveness steps the gallery option is deliberately absent: allowing a stored file there would defeat the point of the capture. Worth knowing before a compliance review asks.
If something goes wrong
The widget's error screen always shows the same generic message — it does not name the cause. The actual cause is in the veridia:error event, so log it:
widget.addEventListener('veridia:error', (e) => {
console.error('code:', e.detail.code, '| message:', e.detail.message, '| detail:', e.detail);
});
The next step lists every code the widget emits and what each one means. The two most common causes of a failure right at the start are a mistyped or revoked publishable-key (invalid_api_key) and a test tenant with no credits left (insufficient_credits).
Next step
Run a real verification end-to-end and inspect the result.