Drop-in browser loader for the FaceVault hosted verification widget. Add a button to your page, and your users complete KYC (ID + selfie + liveness) in a modal — no SDK, no build step, no camera code on your side.
<script src="https://app.facevault.id/embed.js" async></script>
<button data-fv-token="<token-from-your-server>">Verify with FaceVault</button>That's the whole front-end. The verification UI runs inside an iframe served by
FaceVault; embed.js just opens it, enforces the postMessage origin, and forwards
events to your page.
┌────────────┐ 1. POST /widget_sessions ┌──────────────────┐
│ Your │ ──────(api_key, site_id)──────▶ │ api.facevault.id │
│ backend │ ◀─────── widget_token ───────── │ │
└────────────┘ └──────────────────┘
│ 2. render <button data-fv-token=…> + embed.js
▼
┌────────────┐ 3. click → FV.open() ┌──────────────────┐
│ Your page │ ──── opens iframe (modal) ─────▶ │ app.facevault.id │
│ (embed.js) │ ◀──── fv:complete (decision) ─── │ (hosted webapp) │
└────────────┘ └──────────────────┘
- Your backend mints a single-use
widget_tokenby callingPOST /api/v1/widget_sessionswith your API key. The key never touches the browser. - Your page renders a
[data-fv-token]button and loadsembed.js. embed.jsopens the hosted widget in a modal on click and emits events (ready,progress,complete,error,cancel) back to your page.
The token is HMAC-signed server-side, single-use, and expires in 5 minutes.
POST https://api.facevault.id/api/v1/widget_sessions
X-FaceVault-Api-Key: fv_live_your_api_key
Content-Type: application/json
{ "site_id": "fvs_pk_…", "external_user_id": "user-123" }201 Created
{
"session_id": "…",
"widget_token": "eyJ…",
"expires_at": "2026-05-27T14:05:00Z",
"embed_url": "https://app.facevault.id/?embed=1&site_id=fvs_pk_…#token=eyJ…"
}Required API-key scope: sessions:create. Get your site_id (fvs_pk_…) from the
dashboard. Working server examples for FastAPI and
Express are in examples/.
<script src="https://app.facevault.id/embed.js" async></script>
<button data-fv-token="eyJ…">Verify with FaceVault</button>Any element with [data-fv-token] is auto-bound on load (and on DOM mutation, so
dynamically-added buttons work too).
<script>
// Register after load so embed.js (async) has defined window.FV.
window.addEventListener('load', function () {
FV.on('complete', function (e) {
// e.decision: "passed" | "review" | "failed"
console.log('verification', e.session_id, e.decision);
});
FV.on('error', function (e) { console.warn('fv error', e.code); });
FV.on('cancel', function () { console.log('user closed the modal'); });
});
</script>The browser decision is for UX — show a "thanks, we got it" screen, hide
a form, etc. Don't grant access based on this event; do that in step 4.
When verification reaches a final state, FaceVault POSTs a webhook to the
URL you configured in the dashboard. Verify the HMAC against the raw
request body, then trust the trust_decision it carries.
# FastAPI — see examples/fastapi/app.py for the full route.
from facevault import parse_event, verify_signature
@app.post("/webhook")
async def webhook(request: Request):
body = await request.body()
sig = request.headers.get("X-FaceVault-Signature", "")
if not verify_signature(body, sig, WEBHOOK_SECRET):
raise HTTPException(400, "bad signature")
event = parse_event(body)
# Gate access in your own DB based on event.trust_decision
...// Express — see examples/express/server.js for the full route.
const { verifySignature, parseEvent } = require("facevault");
app.post("/webhook", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.headers["x-facevault-signature"] || "";
if (!verifySignature(req.body, sig, WEBHOOK_SECRET)) {
return res.status(400).send("bad signature");
}
const event = parseEvent(req.body);
// Gate access in your own DB based on event.trustDecision
...
});Install the SDK once: pip install facevault or npm install facevault.
| Call | Description |
|---|---|
FV.open(token, opts?) |
Open the widget for a widget_token. opts.siteId is optional (derived from the token otherwise). |
FV.on(event, handler) |
Subscribe to an event. |
FV.close() |
Close the modal. |
| Event | Payload |
|---|---|
ready |
{ session_id } |
progress |
{ screen } |
complete |
{ decision, session_id, error } |
error |
{ code } |
cancel |
{} — synthesized when the user closes the modal before completing |
embed.js opens the webapp on the same origin it was loaded from. To override explicitly — e.g. for a self-hosted FaceVault deployment, or a custom CDN serving the loader — pass the target origin via data-fv-origin:
<script src="https://app.facevault.id/embed.js" data-fv-origin="https://app.your-domain.com" async></script>Modern evergreen browsers. Uses MutationObserver, postMessage, and iframe
allow="camera; microphone"; degrades gracefully on very old browsers (buttons
still work; auto-rebind is skipped).
Issues and PRs welcome — see CONTRIBUTING.md for the dev
setup and ground rules (notably: keep the public FV API stable and embed.js
dependency-free). By participating you agree to the
Code of Conduct.
Found a vulnerability? Don't open a public issue — see
SECURITY.md and email security@facevault.id.
See CHANGELOG.md.
MIT © Kaditham Holdings Pte Ltd