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

  1. Your server encrypts a client key of your own choosing.
  2. Your page opens the widget with the scenario identifier and the encrypted key. The widget creates the session for the user.
  3. The user takes the steps of the scenario.
  4. The platform sends the SESSION_COMPLETE webhook to your callback URL.
  5. Your server reads the session status and decides.

Prepare the dashboard

Do these before you write code.

WhatWhereWhy
A scenarioKYC EditorThe scenario defines the steps. Save needs a name and at least one step. Note its identifier.
The secret keyThe general options of the scenarioIt 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 domainAccess, then Allowed domainsThe 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 URLThe callback options of the scenarioEnter the address of your backend and use the test request to check that it is reachable.
A permanent API tokenAccess, then API keysYour 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 readWhat it means
status of successThe session completed successfully.
status of failedThe session failed. The errors of the steps say why, as symbolic codes.
status of suspiciousThe platform flagged the result. Send it to a reviewer in the dashboard.
status of expiredThe user did not finish within the session lifetime.
status of processingNot 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

StageThe sign that it worked
DashboardThe scenario is saved, the domain is listed, the callback test request succeeded.
ServerThe route returns a Base64 string. A different string on each call is normal.
PageThe widget opens over your page, and the camera permission prompt appears.
WidgetThe steps run to the completion screen.
LogThe session appears on KYC Logs with your raw client key.
WebhookYour endpoint received a SESSION_COMPLETE event for that session.
StatusThe status call returns a final status that matches the card in the dashboard.

If it does not work

SymptomWhat to check
The widget does not openThe 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 failedThe 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 startThe 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 arrivesThe 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 unreadableThe body is encrypted. Decrypt it with the secret key of the scenario.
A call returns success, but the body has an errorBranch on the status field of the body. See response format and errors.

Next

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