# Cognau documentation > Anti-deepfake liveness verification. Confirms a live human is present using > randomized challenge-response. Not document KYC, not facial recognition > against a database. > > Summary: https://cognau.com/llms.txt > Generated from https://cognau.com/docs ======================================================================== Source: https://cognau.com/docs/quickstart ======================================================================== # Quickstart Cognau answers one question: **is a live human present right now?** It is not document KYC. There is no ID upload, no name, no address. A short camera check runs a few randomized challenges and returns a pass or fail. The fastest path needs no integration at all. ## 1. Create a verification link From the dashboard, open **Links** and create one. Or from your terminal: ```bash curl -X POST https://machine.cognau.com/api/v1/cockpit/verification-links \ -H "Authorization: Bearer $COGNAU_TEST_KEY" \ -H "Content-Type: application/json" \ -d '{"label": "first test", "maxUses": 1}' ``` ```json { "success": true, "data": { "url": "https://verify.cognau.com/?link=cgn_vl_…", "prefix": "cgn_vl_ab12cd34", "maxUses": 1, "expiresAt": 1760000000000 } } ``` The URL is returned **once**. Only its hash is stored, so it cannot be shown again. Copy it now. ## 2. Open it Open the URL in a browser and complete the check. With a **test** key nothing real happens: no detection runs, no video frame is stored, and the verdict is simulated. It costs nothing. ## 3. Read the result Poll from your backend, or receive a webhook: ```bash curl https://machine.cognau.com/api/v1/cockpit/sessions \ -H "Authorization: Bearer $COGNAU_TEST_KEY" ``` ### Try it right here Paste a **test** key and send the request. It runs against the real API from your browser. ```tryit { "method": "POST", "path": "/cockpit/verification-links", "title": "creates a real link on your test account", "body": "{\n \"label\": \"from the docs\",\n \"maxUses\": 1,\n \"expiresInDays\": 7\n}" } ``` That is the whole loop. When you are ready to put this inside your own product, there are two routes: - **[Embed the widget](/docs/embedding)** in an iframe on your page. - **[Create sessions from your backend](/docs/sessions)** with your secret key, which is the production shape. ## Test and live Every key carries its environment in the string: | Prefix | Behaviour | |---|---| | `cgn_sk_test_…` | Sandbox. No detection, no stored video, simulated verdict, free. | | `cgn_sk_live_…` | Real verification. Billed per check. | Build against `test` until your integration is done. The wire format is identical, so nothing changes when you switch except the key. ======================================================================== Source: https://cognau.com/docs/authentication ======================================================================== # Authentication Cognau uses three credentials. Confusing them is the most common integration mistake, so it is worth two minutes. | Credential | Looks like | Lives | Grants | |---|---|---|---| | **API key** | `cgn_sk_live_…` / `cgn_sk_test_…` | your server, only | full account access | | **Session token** | `cgn_st_…` | the browser | one WebSocket, once | | **Link token** | `cgn_vl_…` | an email or URL | one session, then spent | ## API keys Send as a bearer token: ```bash curl https://machine.cognau.com/api/v1/cockpit/sessions \ -H "Authorization: Bearer cgn_sk_live_…" ``` The key decides both **who** you are and **which environment** you are in. A test key cannot create live sessions and a live key cannot create sandbox ones, so a misconfigured deployment fails loudly instead of quietly billing you. **Never put an API key in a browser.** It grants read access to every session on your account. Only the hash is stored on our side, so a key is shown in full exactly once, at creation; if you lose it, issue another and revoke the old one. Multiple active keys per environment are allowed on purpose, so rotation is create → deploy → revoke, with no window where your integration is down. ## Session tokens Returned by `POST /session/create` and handed to the browser. A session token authorizes exactly one WebSocket connection and nothing else. It is single-use: once a socket opens, the token is spent, which is why a dropped connection ends the session rather than resuming it. ## Link tokens The credential inside a verification URL. Weaker than an API key by construction: it can create the one session it is scoped to, and cannot read verdicts, list sessions, or see anything about your account. Treat the URL itself as the secret. Anyone holding it can run one verification that you are billed for, which is why links default to a single use, always expire, and can be revoked instantly. ======================================================================== Source: https://cognau.com/docs/sessions ======================================================================== # Sessions The production shape. Your backend creates a session with your secret key; the browser receives only a single-use session token. ## 1. Server: create the session ```bash curl -X POST https://machine.cognau.com/api/v1/session/create \ -H "Authorization: Bearer $COGNAU_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "web", "clientReference": "user_4821" }' ``` ```json { "success": true, "data": { "sessionId": "6a7de599a5244ea6a3e65109", "sessionToken": "cgn_st_…", "environment": "live", "livemode": true, "challengeSequence": [{ "type": "headTurn", "index": 0, "params": {} }], "wsEndpoint": "/api/v1/ws/session/6a7de599a5244ea6a3e65109", "expiresAt": 1760000000000 } } ``` ### `clientReference` Your own identifier for whoever is being verified: a user id, an application id, an order number. It is opaque to us, never parsed and never matched across accounts, and it comes back on the webhook. Without it you must store our `sessionId` at create time to know whose result arrived later. ### Other options | Field | Effect | |---|---| | `riskHint` | `low` shortens the sequence to 3 challenges; anything else uses 4 | | `simulate` | sandbox only: `pass` or `fail`, chooses the fake verdict | | `enablePersonId` | opt-in re-recognition; requires you to have collected consent | | `deviceId` | your own device identifier, if you have one | | `trainingConsent` | the user agreed their video may be retained for training | ```tryit { "method": "POST", "path": "/session/create", "title": "creates a sandbox session with a test key", "body": "{\n \"platform\": \"web\",\n \"clientReference\": \"user_4821\",\n \"simulate\": \"pass\"\n}" } ``` ## 2. Browser: run the check Hand the session to the widget. See [Embedding](/docs/embedding). ## 3. Server: trust the webhook The verdict the browser sees is convenient for UX, but it arrives over a channel the end user controls. **Make decisions from the webhook**, which is signed and server-to-server. See [Webhooks](/docs/webhooks). ## Sessions expire A session is short-lived by design, measured in minutes. Create one when the user is about to verify, not when the page is built. If you need something that survives an email, use a [verification link](/docs/verification-links) instead. ## Reading results ```bash curl "https://machine.cognau.com/api/v1/cockpit/sessions?clientReference=user_4821" \ -H "Authorization: Bearer $COGNAU_SECRET_KEY" ``` Filtering by your own reference is the point of setting it: you can answer "did user 4821 ever pass?" without having kept our identifiers. ======================================================================== Source: https://cognau.com/docs/verification-links ======================================================================== # Verification links A link is a hosted verification: send someone a URL, they complete the check, you get the result. No backend, no frontend work. Use it to trial the product, to verify one person over email, or to let a support agent re-verify someone. For production traffic inside your own product, create [sessions](/docs/sessions) from your backend. ## Create ```bash curl -X POST https://machine.cognau.com/api/v1/cockpit/verification-links \ -H "Authorization: Bearer $COGNAU_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "label": "Acme onboarding, J. Doe", "clientReference": "user_4821", "expiresInDays": 7, "maxUses": 1, "redirectUrl": "https://acme.com/verified" }' ``` | Field | Default | Notes | |---|---|---| | `label` | empty | For your eyes only; how you find the result later | | `clientReference` | none | Your id for this person; lands on the webhook | | `expiresInDays` | 7 | Capped at 30 | | `maxUses` | 1 | Single use is the safe default | | `redirectUrl` | none | We append `session_id` and `result` | The response contains the full URL **once**. Only its hash is stored, so it can never be shown again. Copy it into your email at that moment or issue another. ## Why a link and not a session Sessions expire in minutes. A session baked into an email would be dead before the recipient opened it. A link is the durable object, valid for days, and it mints a fresh short-lived session at the moment somebody opens it. ## Treat the URL as a credential Anyone holding it can run one verification billed to you. The defaults exist for that reason: single use, always expiring, revocable instantly. ```bash curl -X DELETE https://machine.cognau.com/api/v1/cockpit/verification-links/$ID \ -H "Authorization: Bearer $COGNAU_SECRET_KEY" ``` A spent link returns `410` with a message you can show the recipient as-is ("This verification link has already been used"). Redemption is rate limited per IP. ## No retries If someone fails, the check is over. The widget offers no "try again", because every attempt is a billable verification and letting the end user restart at will is a way to spend your money and exhaust a multi-use link. Whether they get another attempt is your decision: issue another link. ======================================================================== Source: https://cognau.com/docs/embedding ======================================================================== # Embedding The widget runs in an iframe on your page. Load the script and mount it: ```html
``` ## Three ways to supply a session ```js // 1. A verification link. No backend needed. Cognau.mount('#cognau', { link: 'cgn_vl_…' }); // 2. A session your backend already created. Cognau.mount('#cognau', { session: sessionFromYourApi }); // 3. Your own endpoint, which we POST to when the widget is ready. Cognau.mount('#cognau', { sessionEndpoint: '/api/start-verification' }); ``` Options 2 and 3 never put the session token 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. ## Writing the iframe yourself ```html ``` 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". ```js 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, for sizing the frame | **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](/docs/webhooks). 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: ```bash 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. ======================================================================== Source: https://cognau.com/docs/webhooks ======================================================================== # Webhooks The browser sees a verdict, but it reaches you through the end user's device. Webhooks are signed and server-to-server, so this is where decisions belong. ## Register an endpoint ```bash curl -X POST https://machine.cognau.com/api/v1/cockpit/webhooks \ -H "Authorization: Bearer $COGNAU_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://acme.com/hooks/cognau", "enabledEvents": ["verification.completed"] }' ``` The signing secret is returned **once**. Endpoints are scoped to the environment of the key that created them, so test traffic never reaches your production receiver. ## `verification.completed` ```json { "sessionId": "6a7de599a5244ea6a3e65109", "clientReference": "user_4821", "result": "passed", "confidence": 0.91, "riskFactors": [], "personKey": null, "personSeenBefore": null, "completedAt": "2026-08-13T12:04:20.000Z" } ``` `clientReference` is whatever you set at session or link creation. It is the field that lets you join this event to your own record without having stored our `sessionId`. ## Verify the signature Every delivery carries `x-cognau-signature`. Compute HMAC-SHA256 over the raw body with your endpoint secret and compare in constant time. Reject anything that does not match, and do it **before** parsing the body. ## Delivery Deliveries are retried with backoff and claimed atomically, so exactly one of your instances handles each one even when several are running. Respond `2xx` quickly; do the work afterwards. A persistently failing endpoint is disabled and can be re-enabled from the dashboard, where you can also inspect the event log and redeliver. Handle duplicates. Retries mean the same event can arrive more than once, so key on `sessionId`. ======================================================================== Source: https://cognau.com/docs/errors ======================================================================== # Errors Every error response has the same shape: ```json { "success": false, "error": "human readable message" } ``` | Status | Meaning | Retry? | |---|---|---| | `400` | Malformed request | No, fix the request | | `401` | Missing or invalid API key | No | | `402` | Monthly quota exceeded | Not until the period resets | | `403` | Admin disabled, or embed origin not allowed | No | | `404` | Not found, or a link that was never issued | No | | `410` | Link expired or already used | No, issue a new link | | `429` | Rate limited | Yes, after `Retry-After` | | `500` | Our fault | Yes, with backoff | ## 402 is not 429 A quota is a commercial ceiling, not a speed limit. It clears when the billing period rolls or when the plan changes, so retrying sooner will not help. The body tells you where you stand: ```json { "success": false, "error": "Monthly verification quota exceeded", "details": { "used": 100, "quota": 100, "periodResetsAt": "2026-09-01T00:00:00.000Z" } } ``` Sandbox traffic is never blocked by a quota, so your developers keep working while live traffic is capped. ## Link errors `410` distinguishes "already used" from "expired", and both messages are written to be shown to the recipient as-is. Every other rejection returns the same generic `404`, deliberately: distinguishing "no such link" from "revoked link" would turn the endpoint into an oracle for probing token validity.