Quick start
First verification
One user, one widget, one webhook: the whole path from a button on your page to a decision on your server.
This page runs one verification from start to finish, with checks along the way so that you know which part broke if something does. It is the widget path. The first API call page covers the server-side path, and the Web SDK integration guide is the full reference for the same flow.
What happens
- Your server encrypts a client key of your own choosing.
- Your page opens the widget with the scenario identifier and the encrypted key. The widget creates the session for the user.
- The user takes the steps of the scenario.
- The platform sends the
SESSION_COMPLETEwebhook to your callback URL. - Your server reads the session status and decides.
Prepare the dashboard
Do these before you write code.
| What | Where | Why |
|---|---|---|
| A scenario | KYC Editor | The scenario defines the steps. Save needs a name and at least one step. Note its identifier. |
| The secret key | The general options of the scenario | It encrypts the client key. The Check button opens a window where you can encrypt or decrypt a client key. Keep the key on your server only. |
| Your domain | Access, then Allowed domains | The widget runs only on the domains in the list, and only on sites that open over HTTPS. A change takes effect within two minutes. |
| The callback URL | The callback options of the scenario | Enter the address of your backend and use the test request to check that it is reachable. |
| A permanent API token | Access, then API keys | Your server uses it to read the session. See the first API call. |
Until at least one domain is added, the Web SDK settings in the scenario are unavailable. See allowed domains and KYC and AML in the dashboard.
Try the scenario without code
Open the KYC Logs tab, choose your scenario in the list, and click KYC SDK. The widget opens in the dashboard, and you go through the check yourself. The session appears in the log as it runs, with a card for each step. Do this once. It shows you what your user will see, and it proves that the scenario works before your code is in the way.
Create the client key on your server
The client key is an identifier that you define. Do not put personal data in it. The raw value is at most 36 characters. Encrypt it with the secret key of the scenario, and give the encrypted value to your page. The algorithm is on the working with clientKey page. Here it is in Node.js:
const express = require("express");
const crypto = require("crypto");
const app = express();
function encryptClientKey(raw) {
const secret = process.env.SCENARIO_SECRET_KEY; // on the server only
const key = crypto.createHash("sha256").update(secret).digest("hex").substring(0, 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
const encrypted = Buffer.concat([cipher.update(raw, "utf8"), cipher.final()]);
return Buffer.concat([iv, encrypted]).toString("base64");
}
app.post("/verification/start", (req, res) => {
const raw = "first-test-0001"; // your own opaque identifier
res.json({ clientKey: encryptClientKey(raw) }); // store the raw value with your user
});
The route name is an example for a route that you write. Samples for Python, PHP and Go are on the integration pages. Because the initialization vector is random, the same input gives a different string each time. That is normal.
Open the widget
<button id="start" disabled>Loading...</button>
<script>
const button = document.getElementById("start");
const script = document.createElement("script");
script.src = "<WIDGET_SCRIPT_URL>";
script.onload = () => { button.disabled = false; button.textContent = "Verify my identity"; };
script.onerror = () => { button.textContent = "Could not load the widget"; };
document.body.appendChild(script);
button.addEventListener("click", async () => {
const reply = await fetch("/verification/start", { method: "POST" });
const { clientKey } = await reply.json(); // encrypted on your server
window.KYCWidget.setupKYC({
scenarioId: "<your scenario id>",
clientKey,
closeCb: () => console.log("widget closed"),
successCb: (sessionJson) => console.log("widget finished", sessionJson),
});
});
</script>
The integration documentation tab of your scenario shows the exact script address for your account. Serve the page over HTTPS, and put the widget on the main page of your site, not inside a cross-origin iframe, because the browser blocks the camera there. All the launch options are on the installation page.
Finish as the user
Click the button and go through the steps. The widget shows a start screen, asks for camera access, guides the capture of the document, then the selfie and the liveness step, and tells you when the check is complete. See Web SDK introduction for the screens.
Here is what you should see in the dashboard at this point. A new session on the KYC Logs tab, with the raw client key you chose, in the scenario you built. The status is shown on the card of the session. If the session does not appear, the widget probably never opened. Go to the troubleshooting table below.
Receive the webhook
When the session completes, the platform sends a POST request with the SESSION_COMPLETE event to the callback URL. Treat the event as a signal, store it durably, answer quickly, and read the result with the status call afterwards. A minimal receiver:
app.use(express.json());
// STUB: implement against your own database. The sample returns an error for every event until you do.
// Contract: enqueue(job) resolves only after the row is committed, and rejects otherwise.
// A resolved promise must mean durable storage, never an in-memory queue.
const jobs = {
async enqueue(job) { throw new Error("implement jobs.enqueue against your database"); },
};
app.post("/webhooks/verification", async (req, res) => {
const event = req.body; // decrypt first if you turned on body encryption
if (!event || typeof event.sessionId !== "string") {
return res.sendStatus(400); // not an event of this kind
}
try {
await jobs.enqueue({ kind: "session-complete", sessionId: event.sessionId }); // your database table or queue
} catch (err) {
return res.sendStatus(500); // nothing was stored, so do not claim success
}
res.sendStatus(200); // stored: now it is safe to answer
});
// A separate worker takes jobs from the store and reads the session status, see the next section.
Answer only after the job is stored. A handler that answers first and keeps the work in memory loses the event when the process restarts. The documentation does not state how often the platform sends an event again, so do not rely on a repeat. Keep a job that reads the status of sessions that have waited too long, as the article on polling or webhook describes.
If you turned on encryption of the body, decrypt it with the secret key of the scenario. If you chose the option that omits identity fields, the callback leaves out names, document data and images and keeps the session identifier. See receiving webhooks for the events, the other three event bodies and the option that notifies you only when the session is finalized.
Read the result
curl -X POST "<API_BASE_URL>/kyc/session/status" \
-H "Content-Type: application/json" \
-d '{
"token": "<your permanent API token>",
"clientKey": "first-test-0001"
}'
You can use the raw client key or the session identifier from the event. A raw client key resolves a regular session. The answer is the session object: a status, and one entry in results for each step.
| What you read | What it means |
|---|---|
status of success | The session completed successfully. |
status of failed | The session failed. The errors of the steps say why, as symbolic codes. |
status of suspicious | The platform flagged the result. Send it to a reviewer in the dashboard. |
status of expired | The user did not finish within the session lifetime. |
status of processing | Not final yet. Wait for the webhook, or read again. |
On a liveness step, read verified and outcome.class as well, because a successful status alone does not establish verified liveness. If the scenario has AML screening, read its verdicts separately after the DATABASE_CHECKS_COMPLETE event. See session statuses, the session object and database check results.
Decide on the user from this status on your server, never from successCb in the browser.
What you should have seen
| Stage | The sign that it worked |
|---|---|
| Dashboard | The scenario is saved, the domain is listed, the callback test request succeeded. |
| Server | The route returns a Base64 string. A different string on each call is normal. |
| Page | The widget opens over your page, and the camera permission prompt appears. |
| Widget | The steps run to the completion screen. |
| Log | The session appears on KYC Logs with your raw client key. |
| Webhook | Your endpoint received a SESSION_COMPLETE event for that session. |
| Status | The status call returns a final status that matches the card in the dashboard. |
If it does not work
| Symptom | What to check |
|---|---|
| The widget does not open | The domain is in the allowed domains, the page is on HTTPS, and the widget is not inside a cross-origin iframe. |
| The widget reports that decryption failed | The client key was not encrypted, or a different secret key was used, or the string was cut when copied. Pass exactly the Base64 string that your server produced, and check that the secret key belongs to the same scenario. |
| The camera does not start | The browser permission. If the user denies the camera, the widget shows a screen that explains how to allow it. |
| The session appears in the log but no webhook arrives | The callback URL in the scenario, and the test request. The address must be reachable from outside your network. |
| The webhook arrives but the body is unreadable | The body is encrypted. Decrypt it with the secret key of the scenario. |
| A call returns success, but the body has an error | Branch on the status field of the body. See response format and errors. |
Next
- Web SDK integration guide: the same flow with the launch checklist.
- Mobile apps and WebView: run the widget inside an app.
- Webhooks: the four events and their options.
Questions, answered plainly
Do I create the session myself when I use the widget?
No. When your page opens the widget with the scenario identifier and the encrypted client key, the widget creates the session for the user. You find the session later by its identifier, or by your raw client key.
Can I decide from the success callback of the widget?
No. The callbacks run in the browser of the user. Decide from the status that your server reads.
How can I try the scenario before I write any code?
On the logs tab of the dashboard, the KYC SDK button opens the widget for the selected scenario, so that you can go through the check yourself.
Want the full integration?
The Web SDK integration guide ties the pieces together, with the checklist for launch.
Open the integration guide