Skip to main content

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:

  • Publishableqv_pubt_... (test) or qv_pub_... (live). Goes in the page. This step only needs this one.
  • Secretqv_sect_... (test) or qv_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.

Domain whitelist

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.

Store the mapping yourself

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.

AttributeRequiredDefaultDescription
publishable-keyYesYour qv_pubt_* or qv_pub_* key from the dashboard
api-baseNohttps://api.xxuxe.onlineOverride the API endpoint. You almost never need this
user-refNoYour own user identifier (max 128 chars). Echoed in webhooks only
countryNoISO 3166-1 alpha-2 country code (PY, BR, MX) — improves OCR accuracy. Normalized to uppercase for you
document-typeNoOne of: dni, passport, drivers_license, national_id, other
submitted-full-nameNoThe user's full name, fuzzy-matched against the document (max 255 chars). Produces the name_match score
require-doc-backNodepends — see belowWhether to capture the back of the document
localeNobrowser locale, then enUI language: en, es, or pt
accent-colorNo#0f172aAccent for buttons and active states. Any valid CSS color, not just hex
active-livenessNofalseSet "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,
};
Don't mix the two

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:

  1. A "Start" button under the title "Identity verification" (or its translated equivalent)
  2. After clicking start, a camera permission prompt
  3. The capture steps — front of document, back of document unless you turned it off, then selfie
  4. 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.

Step 2: First verification →