The Verificate widget
For signup pages. It shows the code the customer scans with their phone, and follows the verification as it happens: the link opened, each step done, the decision.
It is 12 KB over the wire, needs no framework, and keeps its styles to itself.
What your server does first
The widget is given two values that your server gets when it creates the session:
| From the session | For the widget | |
|---|---|---|
client_secret |
clientSecret |
Gives progress and nothing else. Meant for the browser |
url |
url |
The link for the phone. Give it to show the code. Leave it out if you send the link by message and want progress only |
Your API key stays on the server. It never goes to the page.
On the page
<div id="verify"></div>
<script src="https://api.verificate.io/sdk/v1/verificate.js"></script>
<script>
const widget = Verificate.mount("#verify", {
clientSecret: "vs_…_secret_…",
url: "https://verificate.me/v#vs_….…",
onFinish(status) {
if (status === "approved") location.assign("/welcome");
},
});
</script>
As a module:
<script type="module">
import { mount } from "https://api.verificate.io/sdk/v1/verificate.mjs";
mount("#verify", { clientSecret, url });
</script>
Settings
clientSecret |
Required |
url |
The session’s link. With it the widget shows the code, and a button that copies the link |
locale |
en, he or es. Left out, the widget speaks the language the customer chose on their phone, and before that the session’s own |
baseUrl |
Where the service lives. Left out, it is where the script was loaded from, and for a copy bundled into your own code https://api.verificate.io. It is never taken from url: the link is for the customer’s phone and lives on verificate.me |
onChange(snapshot) |
Called with every change, and once at the start |
onFinish(status, snapshot) |
Called when the session is approved, rejected, needs_review, expired or canceled |
onProblem(problem) |
"not_found": the secret matches no session. "offline": the service cannot be reached, and the widget keeps trying. null: the trouble has passed |
mount returns the widget:
widget.snapshot |
The latest that is known, or null before the first answer |
widget.destroy() |
Takes the widget off the page and stops following the session |
The element also sends two events, which bubble: verificate:change and
verificate:finish. Their detail is the snapshot.
A session held for review
onFinish("needs_review") means a person will decide, which can take hours.
Let the customer go on, and do not make them wait on the page. The decision
reaches your server by webhook. If the page is still open
when it comes, onFinish is called again with approved or rejected.
What a snapshot holds
{
"type": "snapshot",
"session_id": "vs_7X11LVCKb7GxwD5j6GMZHn",
"status": "in_progress",
"mode": "live",
"locale": "en",
"opened": true,
"steps": [
{ "index": 0, "type": "id_document", "status": "completed", "verification": "pending" },
{ "index": 1, "type": "selfie", "status": "in_progress", "verification": "pending" }
],
"branding": { "name": "Acme", "logo_url": null, "primary_color": "#0b6e5f", "support_url": null, "privacy_url": null },
"expires_at": "2026-09-30T09:40:00.000Z",
"updated_at": "2026-09-29T09:42:31.000Z"
}
No names, no pictures, no reasons. A page cannot learn from the widget why a session was held back or turned down. Your server can, with its key.
Without the widget’s looks
To draw progress yourself:
const following = Verificate.watch({
clientSecret,
baseUrl: "https://api.verificate.io",
onChange(snapshot) { render(snapshot); },
onFinish(status) { … },
});
following.stop();
Looks
The widget takes the typeface of the page around it, and the tenant’s colour, name and logo as they are set at the review desk. Its styles live in a shadow root: the page’s styles do not reach in, and the widget’s do not reach out.
Four properties can be set on the element the widget is mounted on:
#verify {
--verificate-color: #1f4f8f; /* used when the tenant has set no colour */
--verificate-ground: #ffffff; /* background */
--verificate-ink: #101c2a; /* text */
--verificate-radius: 14px; /* corners */
}
The code itself is always black on white. Cameras read that best.
Pages with a content security policy
The widget needs:
script-src https://api.verificate.io
connect-src https://api.verificate.io wss://api.verificate.io
img-src https: (only if the tenant has set a logo)
It needs no 'unsafe-inline', for scripts or for styles, and it works with
require-trusted-types-for 'script'.
How changes arrive
Over a WebSocket. When that cannot be had, or drops, the widget asks for the status every three to ten seconds and tries the socket again, after waits that grow from two seconds to thirty. The customer is told only when the service cannot be reached at all.
The widget stops following once the session is approved, expired or canceled. It keeps following a session that is rejected or held for review, because a reviewer can still change that.