Embedding
The widget opens as a modal over your page. Load the script and open it:
<script src="https://verify.cognau.com/embed.js"></script>
<script>
const v = Cognau.open({
link: 'cgn_vl_…',
onVerdict: r => { v.close(); console.log(r.passed, r.sessionId) }
});
</script>
There is no element to attach to and no container to add. The modal is
position: fixed, so opening it does not reflow, move, or resize anything on
your page, and closing it leaves the DOM exactly as it was. Nothing is written
to your <body>: no class, no inline style, no scroll lock.
It draws no backdrop either. The frame is sized to the modal rather than the viewport, so the rest of your page stays visible, scrollable, and clickable while verification runs. If you want a dimmed background, render your own and put it behind the modal.
Close it yourself with v.close(). It is the same as v.destroy().
Three ways to supply a session
// 1. A verification link. No backend needed.
Cognau.open({ link: 'cgn_vl_…' });
// 2. A session your backend already created.
Cognau.open({ session: sessionFromYourApi });
// 3. Your own endpoint, which we POST to when the widget is ready.
Cognau.open({ sessionEndpoint: '/api/start-verification' });
Options 2 and 3 are the production shape, and they are the same flow seen from two ends:
- your backend calls
POST /session/createwith your secret keycgn_sk_… - it gets back a session containing a single-use
sessionToken - your page passes that session object to
Cognau.open - the widget asks for it over
postMessageand runs
Nothing account-level goes to the browser, and there is no publishable key to
add. If you are looking for a cgn_pk_… to pair with the session, it does not
exist: the session token is already the browser-side credential, scoped to one
verification and spent the moment the socket opens. See
Authentication.
Pass the whole session object, not just the token. A session that also collects a document carries a second credential, and the widget needs both.
The token never travels in a URL. It is handed to the iframe over postMessage
once the widget signals readiness, so the credential never lands in browser
history, a Referer header, or a server log.
Putting it in your layout instead
Pass inline: true and a target, and the widget sits in your page's flow
rather than over it. This is the one mode that takes up space, so it is opt-in:
Cognau.mount('#cognau', { link: 'cgn_vl_…', inline: true });
An inline frame grows and shrinks to fit its content. A modal does not: its
screens are sized against the viewport, and inside a frame the viewport IS the
frame, so a frame that also followed its content would chase itself. Pass
height to choose a different modal size (the default is 640, always capped to
the viewport).
Cognau.mount(target, options) without inline is the same as Cognau.open,
and ignores the target.
Writing the iframe yourself
<iframe src="https://verify.cognau.com/?link=cgn_vl_…&frame=inline"
allow="camera"
style="width:100%;height:640px;border:0"></iframe>
frame=inline tells the widget to size itself from its content, which is what
you want for a frame sitting in your layout. Leave it off and the widget fills
whatever box you give it, as it does inside the modal.
Two things become your responsibility, and both fail silently if missed.
allow="camera" is mandatory. Permissions Policy blocks camera access
inside a cross-origin iframe unless the embedding page delegates it. Without
the attribute the widget reaches the permission screen and stops, which looks
like our bug rather than a missing attribute. Your page must also be HTTPS.
Validate event.origin on every message. Any page can postMessage to
your window, including a forged "passed".
window.addEventListener('message', e => {
if (e.origin !== 'https://verify.cognau.com') return; // REQUIRED
if (e.data?.source !== 'cognau') return;
// …
});
embed.js handles both. That is the only reason it exists.
Events
type |
Payload | Meaning |
|---|---|---|
ready |
mounted, waiting for the user | |
started |
camera granted, session running | |
verdict |
{ passed, sessionId } |
finished (advisory) |
consent_declined |
{ sessionId } |
declined at the consent screen |
error |
{ code } |
could not continue |
resize |
{ height } |
content height. Sizes an inline frame; ignored by a modal unless autoResize: true |
The verdict message is not authoritative. It travels through the end user's browser, so a determined user can post a fake "passed" to your own page. Use it to drive UI; decide from the webhook. The payload deliberately carries no scores, risk factors, or person identifiers.
Restricting who can embed you
By default any site can iframe your widget. To limit it:
curl -X PUT https://machine.cognau.com/api/v1/cockpit/embed-origins \
-H "Authorization: Bearer $COGNAU_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"origins": ["https://app.acme.com", "https://*.acme.com"]}'
Scheme and host only. https://*.acme.com matches any subdomain but not
the bare apex, so add https://acme.com separately if you need it. An empty
list means unrestricted, which is the default.
This governs embedding, not visiting: a link opened directly from an email is not embedded in anything and keeps working whatever the list says.
Be clear about its strength. The origin is reported by the visitor's browser,
so it stops a leaked link being farmed from an unrelated site and makes
misconfiguration loud, but it is not proof against a scripted browser. For a
browser-enforced guarantee, pair it with a CSP frame-ancestors header at your
own edge.
Reading this as an agent? The raw Markdown is at /docs/embedding.md.