Integrating Identity Verification with a Web SDK, Step by Step

Developer guides

For: Full-stack developer adding remote identity verification to a web application

A developer's walk through a web SDK integration for remote identity verification: scenario, browser rules, client key, widget, server decision, failure paths, tests and launch checks.

You are adding remote identity verification to a web application, and you want to know the order of work, the reason behind each step, and the places where an integration like this usually breaks. This guide answers one question: what does a developer have to build and check, in what order, to put a verification widget on a page and to decide on a customer from a result that the browser cannot forge? It follows the NeuroVision Web SDK as documented, and it takes the rules of the browser from the browser documentation and the OWASP material on API security.

The Web SDK is a widget that runs in the browser of the user and guides the user through the steps of a scenario. This guide is about that web widget. For the same widget inside a mobile app, read the companion article on integrating identity verification inside a mobile app WebView. For the server side of the flow after the widget closes, read the verification session lifecycle and receiving verification results by polling or webhook.

The shape of the integration

Four places take part, and the most useful habit is to know what may live in each of them.

WhereWhat lives thereWhat must never be there
The dashboardThe scenario, its secret key, the allowed domains, the callback URL, the API tokens.Nothing that belongs to a single user.
Your serverThe secret key of the scenario, the API token, the encryption of the client key, the webhook endpoint, the decision.Nothing that a browser can read.
Your pageThe scenario identifier, the encrypted client key, the launch options, the interface callbacks.The secret key, the API token, and any decision about the customer.
The widgetCamera capture, the guidance screens, the upload of the images to the platform.Nothing you control.

The browser is the one part of this picture that the user controls. That sentence decides most of the design. Anything the page can see, the user can see and change. So the page opens the widget, and the server decides.

Step one: write the scenario for one flow

A scenario is the recipe of a verification: the steps a user goes through, the documents and countries you accept, and the rules that decide the result. You build it in the dashboard editor, and the API works with the identifier of a scenario that already exists. A scenario is not created through the API.

Write one scenario for each flow that differs in steps, documents or rules. A light check at sign-up and a fuller check before a payout are two scenarios, not one scenario with two moods. Start from the template closest to your flow, remove steps you do not need, and save. A scenario needs a name and at least one step to be saved.

Four settings of the scenario matter to the integration, so decide them now and not on the day of launch.

  • The scenario identifier. It is the value your page passes to the widget.
  • The secret key. It encrypts the client key and, if you switch it on, the body of the callback. It belongs on your server only.
  • The session lifetime. The time the user has to complete the session, from minutes up to a year. A session that nobody finishes reports expired, and a short lifetime keeps abandoned sessions from lingering.
  • The retention of session data, in days. Zero means indefinitely, so set it on purpose.

A walk through every tab of the editor is on the page that explains how to configure a KYC scenario.

Step two: prepare the browser environment

Most failures of a first integration are not in the code that calls the widget. They are in the environment of the page. Four rules decide whether the camera can start, and each of them comes from the browser, not from the platform.

Serve the page over HTTPS

Browsers expose the camera only to a secure context. The MDN description of secure contexts says that documents are secure contexts when their resources are delivered over an HTTPS connection or from a loopback address, and that framed documents must also be embedded in a document which is itself a secure context. The documentation of the media capture method adds the consequence: the call is rejected with a NotAllowedError when the browsing context is insecure, which means a page loaded over HTTP rather than HTTPS. The widget documentation says the same thing from the product side. Serve the page over HTTPS, because browsers give camera access only to secure pages.

Add the domain to the allowed domains

The widget runs only on the domains in the allowed domains list of the dashboard, and only on sites that open over HTTPS. A change takes effect within two minutes. The mask *.example.com allows every subdomain of example.com but not example.com itself, so add the bare domain as a separate entry. Add your staging domain too, and keep staging and production as separate entries, so that you can remove one without touching the other. Until at least one domain is added, the Web SDK settings in the scenario are unavailable.

Check the Permissions Policy of your site

Browsers that support the Permissions Policy let a site decide which features a document may use. The MDN page for the camera directive says that where a defined policy blocks the feature, the media capture call returns a promise that rejects with a NotAllowedError, and that the default allowlist for the camera is the origin itself. If your site sends a Permissions-Policy header for other reasons, for example one that switches off many features at once, check that the camera is allowed for the page that hosts the widget. The MDN example for the case is the header value camera=(self).

Keep the widget out of a cross-origin iframe

The media capture documentation says that only the top-level document of a window can request permission to use the camera, unless the top-level document expressly grants it to a frame through the Permissions Policy. Otherwise the user is never asked. The Web SDK documentation gives the practical rule: put the widget on the main page of your site, not inside a cross-origin iframe, or use the inline mode with mountElementId, which does not work inside a cross-origin iframe either.

If your site sets a Content Security Policy

A Content Security Policy tells the browser which resources a document may load. The MDN guide describes it as the way to control which resources, in particular JavaScript resources, a document is allowed to load, with a fetch directive for each kind of resource and default-src as the fallback for those not listed. If your site sets a policy, the script address of the widget has to be allowed by it. The integration documentation tab of your scenario shows the exact script address for your account. The documentation does not list every other address that a widget could need, so do not guess a list. Deploy the policy first in report-only mode, which the MDN guide describes as a mode in which the policy is not enforced and violations are sent to the reporting endpoint, load the widget on your staging domain, and read the reports before you enforce anything.

Step three: create the client key on your server

The client key is an identifier that you define. It links verification sessions to your users, applications or contracts, and the platform does not generate it. With it you can find every session of one user, match results to your own objects such as a user, an order or a loan, and filter sessions through the API.

The rules for choosing it are short. The raw value is at most 36 characters. Latin letters, digits and the separators _ and - are convenient. Do not put personal data into it, because the client key stays on the session after the personal data is erased. Use an opaque identifier of your own, such as the identifier of the user in your database, and keep the mapping on your side.

Your page never sees the raw value as the widget needs it. The widget takes an encrypted form, made with the secret key of the scenario. The algorithm is documented, and the samples implement it. The key is the first 32 characters of the lowercase hexadecimal form of the SHA-256 hash of the secret key. A random 16-byte initialization vector is generated for each call. The raw client key is encrypted with AES-256 in CBC mode with standard padding. The vector and the ciphertext are concatenated and encoded as Base64, and that string is the clientKey for the widget. In Node.js it looks like this.

const crypto = require("crypto");

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");
}

Because the vector is random, the same input gives a different string each time. That is normal, and it is a quick test of your implementation: two calls, two strings, and the dashboard check function decrypts both to the same raw value.

The route that issues the key is part of your attack surface

Your page needs the encrypted key from somewhere, so you add a route to your server, for example /verification/start. Treat that route with the care you give a payment route. Two lines from the OWASP list of API security risks apply. The first risk, broken object level authorization, says that endpoints which handle object identifiers create a wide attack surface and that object level authorization checks should be considered in every function that accesses a data source using an ID from the user. So derive the raw client key from the authenticated user on the server, and never accept it as a parameter from the browser. The fourth risk, unrestricted resource consumption, notes that some resources, among them biometrics validation, are paid for per request. A route that anyone can call in a loop starts verifications on your account. Require a signed-in user, and limit how often one user can start a verification.

Step four: load the script and open the widget

The widget is a script that your page loads and a function that your page calls. Load the script when the page needs it, enable the button when the script has loaded, and show a plain message if the load fails. Call the widget from a click of the user, so that the camera prompt that follows has an obvious cause.

<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();
    window.KYCWidget.setupKYC({
      scenarioId: "<your scenario id>",
      clientKey,
      theme: "light",
      closeCb: () => {},
      successCb: (sessionJson) => {},
      finalizedCb: ({ status }) => {},
    });
  });
</script>

In a React application the script is loaded in an effect and setupKYC is called from a handler, exactly as in the plain page. The options that matter most are these.

OptionWhat it does
scenarioIdThe scenario identifier. Required.
clientKeyYour identifier for the person, encrypted. Required.
clientUserYour identifier of the user, at most 36 characters. It need not be unique, so pass the same value across sessions to find all attempts of one user later.
themelight or dark. The default is the one set in the scenario.
mountElementIdMounts the widget inline in an element, instead of covering the page. Give the element explicit dimensions.
stickySessionWhen true, the widget remembers the client key per scenario in the browser for 15 minutes, and reopening within that time resumes the previous session.

Do not call setupKYC from inside closeCb, because repeated detection can recurse. If you would rather not embed anything, the platform also hosts a verification page for a scenario, and you build the link from the scenario identifier and the encrypted client key, URL-encoded because the Base64 string can contain / and +.

Step five: treat the browser callbacks as interface events

Three callbacks tell your page what happened. closeCb runs when the widget is closed. successCb runs when verification succeeds and receives the session result as a JSON string. finalizedCb runs when the widget is opened for a session that was finalized earlier and receives an object with the status.

All three run in the browser of the user, so a user can tamper with them. Use them to update the screen: close the overlay, show that a check is under way, move to the next page of your flow. Do not use them to approve a customer. The decision is made from the status that your server reads.

A pattern that works well is to let the page ask your own server, not the platform, what the state is. After successCb the page shows a waiting screen and asks an endpoint of your own, for example every few seconds, for the state of the verification in your database. Your server moves that state when the webhook arrives or when it has read the session status. The API token never reaches the page, and the page cannot invent a result, because it only displays what your server stored. One caution about the timer: browsers enforce a minimum timeout delay in inactive tabs, so a user who leaves the tab will see the screen update late. That is a reason to make the state survive a reload and to refresh it when the tab becomes active again.

Step six: decide on the server

Your server learns that the session is complete in one of two ways, and many integrations use both. The SESSION_COMPLETE webhook reaches the callback URL of the scenario. Treat it as a signal, store it durably, answer, and then fetch the result by the session identifier. Or your server calls the session status method, by the session identifier or by the raw client key. The article on receiving results by polling or webhook compares the two.

Whichever you use, read the right field. The status of a session is the session status. It is not the success flag of the call, and a failure can arrive with a successful HTTP code and an error status in the body. The values are idle, processing, success, failed, exception, expired and suspicious. Branch on status, never on message text.

Map the values to states of your own, and keep the platform's status and your decision in separate fields.

What the server readsA reasonable state of your own
idle or processingPending. Wait.
successApproved, after you have read the liveness step. A successful status alone does not establish verified liveness, so read verified and outcome on a liveness step as well.
failedRefused, or a fresh attempt, by your policy. The error codes say why.
suspiciousReview. A person decides in the dashboard.
expiredNo proof. Offer a fresh session.
exceptionA processing outcome. Offer a fresh session or review.

If your scenario runs database checks such as AML screening, their verdicts are not in the session body. Read them after the DATABASE_CHECKS_COMPLETE event, or with the database check results method, and read outcome before passed, because a check that never ran arrives looking like a clean one.

Step seven: handle the paths that are not the happy path

A verification involves a camera, a document, a network and a person who is not a developer. Plan for these situations before you meet them.

SituationWhat happensWhat your code does
The user denies the cameraThe widget shows a screen that explains how to allow it. In the browser the capture call is rejected with NotAllowedError.Nothing in code. Make sure your support text says where the browser setting is.
The computer has no suitable camera, or the scenario does not allow itThe widget offers a QR code and a link to continue on a smartphone.Nothing. Do not block the flow on a desktop.
The user closes the widgetcloseCb runs. The session stays as it was.Do not treat a close as a failure. Offer to continue.
The user reopens within the sticky windowThe previous session resumes, if stickySession is on.Decide whether resuming is wanted for your flow.
The network dropsThe widget shows a screen with a retry button.Nothing.
The session lifetime runs outThe status is expired.Treat it as a normal outcome and start a fresh session.
The widget opens for a finished sessionfinalizedCb runs with the status.Move the user on, and read the status on the server.
The link arrives in the embedded browser of a messengerCamera access can be restricted there.Ask the user to open the page in the system browser.
The phone is turnedThe widget asks to turn it back to portrait.Nothing.

Step eight: test, in this order

Test from the platform outward, so that when something fails you know which side it is on.

  1. The scenario alone. On the logs tab of the dashboard, the KYC SDK button opens the widget for the scenario, so you can go through the check yourself. If this works, the scenario is sound.
  2. The key. The check function in the general options of the scenario encrypts and decrypts a client key. Compare its output with your server code on a test value.
  3. The callback. Enter the callback URL, use the test request, and confirm the address is reachable and the body decrypts if you turned encryption on.
  4. The page on staging. Open the widget from the page, with the staging domain in the allowed domains and, if you have one, the Content Security Policy in report-only mode.
  5. The unhappy paths. Deny the camera. Close the widget midway. Switch the network off. Let a session expire. Open the widget twice for the same user.
  6. The devices. The widget works in current desktop and mobile browsers, so test in the browsers your users use, on a real phone, and in the embedded browser of at least one messenger.
  7. The decision. Confirm that a tampered successCb changes nothing. In the developer tools, call your own approval route without a server-side result, and check that it refuses.

Step nine: launch checklist and what to watch afterwards

Before you switch real traffic on, run through these points.

  • The production domain is in the allowed domains, and the page is served over HTTPS.
  • The client key is encrypted on the server, from the authenticated user, and the secret key never reaches the browser.
  • The permanent API token is kept in a secret store on the server, and the IP allowlist in the security settings is filled in, because an empty allowlist accepts any address.
  • The decision uses the status that your server read, and each status has a state in your system.
  • The webhook endpoint stores the event durably before it answers, answers quickly, and treats the event as a signal.
  • Retention is set on purpose, and you have a way to call the erase method when a person asks, with a record of the answer.
  • You can correlate the raw client key with your user, and you know where to look in the dashboard when support asks about a session.
  • The route that issues the key is authenticated and limited.

After launch, watch the places that tell you early. The session log shows sessions as they arrive. The statistics show results, errors and attempts. A rise in expired sessions usually means a problem in the path to the camera, in the hand-off to a phone, or in the length of the session lifetime, and a rise in particular error codes tells you what users photograph badly.

Where this sits in a verification flow

The NeuroVision Web SDK is a branded widget that runs in current desktop and mobile browsers in 90+ languages. It takes the user through the steps of a scenario: a document, a selfie with face match, and active liveness, with international AML screening as a database check. The dashboard holds the scenario editor, the session log, reports and export, manual decision and roles. Webhooks tell your backend when a session, a questionnaire, a database check or an erasure completes, and the callback can encrypt its body or omit names, document data and images. To see how these steps look in your own scenario, contact the team.

Sources

All sources were fetched on 27 October 2026.

Ready to talk?

Tell us what you are building and we will help you plan the integration.

Talk to us