Web SDK Integration Guide
Overview
The Width Web SDK provides a drop-in identity
verification flow for web apps. It supports KYC and KYB workflows
including document capture, liveness detection, and review. The SDK is
self-hosted: you import one package, configure it with a fluent chain,
and call launch() — the SDK presents its own modal or
new tab and reports the outcome through events. No DOM container is
required. It ships as an ESM build with full TypeScript types and no
peer dependencies.
Requirements
| Requirement | Minimum |
|---|---|
| Module format | ESM (import) |
| TypeScript | Types included (optional) |
| Peer dependencies | None |
| Backend |
Must issue a session token via sdk.credentials
|
Step 1: Get a session token (backend)
The Web SDK starts from a one-time session token (the
sdkToken). Your server requests it from Width's
sdk.credentials endpoint, then hands it to the browser. The
public apiKey and domainId are configured in
the next steps; the secret signing happens only on your server.
// Server-side request to Width (signed with your secret)
POST {WIDTH_GATEWAY_URL}/gateway?api=sdk.credentials&apiVersion=1.0.0&apiKey={your_apiKey}&sign=...
Content-Type: application/json
{}// Response — read sdkToken and the domains[] list
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"data": {
"sdkToken": "eyJhbGciOi...",
"domains": [
{ "code": "DEFAULT", "name": "Default", "id": "dom_xxxxxxxx" }
]
}
}
}
Expose a small backend route (for example
/your-backend/sdk-token) that returns the
sdkToken and the domainId to your frontend.
Pass the chosen domainId from domains[] into
the SDK config in Step 3.
sdkToken is single-use. Reissue a new token
whenever a user starts a new verification — do not cache or
reuse it.
sdk.credentials call uses the gateway base URL and HMAC signing documented in the OpenAPI reference.Step 2: Install the SDK
Install from npm. After install, import by the package name
@width-com/web-sdk — ESM build, TypeScript types
included, no peer dependencies. The tag you install also
selects the environment the SDK connects to: in this documentation baseline, the default (latest) release targets
production and the @beta tag targets the
platform (testing) environment. Install the tag that
matches the environment your apiKey belongs to, and
confirm which address your installed build opens before you ship (see
the note below).
# Production (正式环境) npm install @width-com/web-sdk # or pnpm add @width-com/web-sdk # or yarn add @width-com/web-sdk # Testing / beta (测试环境) npm install @width-com/web-sdk@beta # or pnpm add @width-com/web-sdk@beta # or yarn add @width-com/web-sdk@beta
Invalid apiKey
The verification page address is compiled into each published
version, so the environment is fixed at install time and
withConf carries no field that changes it. in this documentation baseline, the default (latest) release opens
https://identity.width.com/sdk-page (production) and
@beta opens
https://identity-platform.width.com/sdk-page (platform)
— that pairing reflects the builds the two tags pointed at in the guide,
not a fixed rule, so check which host your installed build opens
before you ship. If the tag does not match the environment your
apiKey belongs to, that page renders
Invalid apiKey and the flow stops there — backend
OpenAPI calls made with the same key are unaffected, so the key itself
is not the problem. Choose the tag by environment, not by version
number. One key covers both sides: withConf declares a
single key field, apiKey, and it is the same key you sign
sdk.credentials with — the TypeScript declarations
shipped inside the package list every field it accepts. For reference,
@beta is recorded here as 1.0.0-beta.3
(published 2026-08-05) and the default to 1.0.1; both
carry eventType and language.
Step 3: Initialize (Quick Start)
Build the verification with a fluent chain: init the
session token, withConf the configuration, subscribe to
events with on / onMessage, then
build().launch(). launch() presents the
self-hosted modal or new tab — you do not pass a DOM container.
import { widthWebSdk } from '@width-com/web-sdk';
// Get the session token from your backend (sdkToken from sdk.credentials)
const { sdkToken, domainId } =
await fetch('/your-backend/sdk-token').then(r => r.json());
widthWebSdk
.init(sdkToken)
.withConf({
apiKey: 'ak_your_public_api_key', // required
domainId, // required
email: 'enduser@example.com', // required
customerId: 'cust_internal_id', // required
mode: 'modal', // 'modal' (default) | 'newtab'
// eventType: 'KYC_EVENT', // optional — 'KYC_EVENT' | 'KYB_EVENT'; omit for default routing
})
.on('idCheck.onApplicantVerificationCompleted', (payload) => {
// payload.reviewResult.reviewAnswer: 'GREEN' (approved) | 'RED' (not approved)
})
.on('idCheck.onApplicantSubmitted', (payload) => {
// Submitted; async review in progress
})
.on('idCheck.onError', (payload) => {
// payload.code / payload.error
})
.onMessage((type, payload) => {
if (type === 'width.onCancelled') {
// User closed the flow
}
})
.build()
.launch(); // self-hosted modal / new tab — no DOM container neededeventType and language require
1.0.1 or later
Both fields are available from 1.0.1 (released
2026-08-20), the documented default (latest) release, and
from 1.0.0-beta.3, which the @beta tag
is recorded here as — so both environments have them, and you
still pick the tag by environment (see Step 2). Stable releases up to
1.0.0 do not contain them: a TypeScript project fails to
compile, and a plain JavaScript project ignores them without any
error. eventType accepts KYC_EVENT or
KYB_EVENT, language accepts
en or zh; both are optional, and omitting
them keeps the default behaviour.
Configuration (withConf)
| Field | Type | Required | Description |
|---|---|---|---|
apiKey |
string | Yes |
Public Width API Key (ak_...)
|
domainId |
string | Yes |
Business domain GID, from domains[] in
sdk.credentials
|
email |
string | Yes | End-user email |
customerId |
string | Yes | Your internal user ID |
mode |
'modal' | 'newtab' |
No |
Presentation, default 'modal'
|
language |
'en' | 'zh' |
No |
Interface language. Omit it and the interface is English; the
browser language is not detected automatically. Available from
1.0.1.
|
eventType |
'KYC_EVENT' | 'KYB_EVENT' |
No |
Which flow to run — individual verification
(KYC_EVENT) or corporate verification
(KYB_EVENT). Omit the field to keep the default
routing (see
Workflows & the Decision Model). Available from 1.0.1.
|
zIndex |
number | No | No default is specified in this documentation baseline.Modal stacking order |
Step 4: Handle the result
launch() returns the full verification-page URL for this session on success, and an empty string on failure. Errors are delivered through idCheck.onError.
Branch on the events you subscribed to. The final verdict is carried by
reviewResult.reviewAnswer. It takes exactly two values:
GREEN when the outcome is approved, and RED
for every other outcome.
.on('idCheck.onApplicantVerificationCompleted', (payload) => {
const { reviewResult, executionId } = payload;
if (reviewResult.reviewAnswer === 'GREEN') {
grantAccess(executionId);
} else {
handleNotApproved(executionId);
}
})Each event maps to a branch of your handling logic:
| Event | Handle |
|---|---|
idCheck.onApplicantVerificationCompleted |
Final verdict available — read
reviewAnswer
|
idCheck.onApplicantSubmitted |
Submitted; enters async review |
idCheck.onError |
Report the failure |
width.onCancelled (via onMessage) |
User closed the flow |
executionId is the handle for matching against your
backend / OpenAPI. Persist it on
idCheck.onApplicantSubmitted (async review) so your
server can poll the final status later.
Verification result reference
The final verdict is carried by
reviewResult.reviewAnswer:
reviewAnswer |
Meaning |
|---|---|
GREEN |
Approved |
RED |
Not approved. The SDK maps every outcome other than an approval
to RED — a rejection lands here, and so does
any other non-approved result. Before treating RED as
a rejection, read the case status (caseStatus from
workflow.execution.status, or the case in the Console).
|
1.0.1 (and 1.0.0-beta.3), the Web SDK
reduces the decision to these two values; there is no third value.
Branch on GREEN for approval, and treat RED
as “not approved” rather than “rejected”
unless the case status confirms a rejection.
Methods
| Call | Returns | Purpose |
|---|---|---|
widthWebSdk.init(accessToken) |
builder | Start the chain with the session token |
.withConf(conf) |
builder | Provide configuration (see Configuration) |
.on(eventName, callback) |
builder | Subscribe to a single event |
.onMessage((type, payload) => {}) |
builder | Receive all events with one handler |
.build() |
instance | Finish building |
instance.launch(container?) |
string |
Start verification; returns the full verification-page URL for
this session — of the form
https://identity.width.com/sdk-page?sdkMode=1&sdkSession=<session id>&apiKey=<your apiKey>&theme=light, with the host taken from the build you installed (see Step
2) — and '' when the launch fails. The URL
addresses this one verification session; the session token behind
it is single-use (see Step 1). container is
ignored
|
instance.close() |
void |
Programmatically close the verification window |
Events
Subscribe to a single event with
.on(eventName, callback), or receive all with
.onMessage((type, payload) => {}).
| Event | When | Payload |
|---|---|---|
idCheck.onApplicantVerificationCompleted |
Flow complete, has a verdict |
{ reviewStatus: 'completed', reviewResult: { reviewAnswer:
'GREEN' | 'RED' }, executionId }
|
idCheck.onApplicantStatusChanged |
Fires together with Completed | { reviewStatus: 'completed', reviewResult: { reviewAnswer: 'GREEN' | 'RED' }, executionId } |
idCheck.onApplicantSubmitted |
Submitted, async review | { executionId } |
idCheck.onError |
Failure | { code, error, executionId? } |
width.onCancelled |
User closed (via onMessage only)
|
{} |
idCheck.onError — the SDK does not throw to your
code.
Error Codes
On idCheck.onError, payload.code is one of:
| Code | Meaning | Retryable |
|---|---|---|
INVALID_INPUT |
Launch parameter missing or invalid | No |
SDK_ERROR |
Network, timeout, or internal error | Yes |
WORKFLOW_FAILED |
Flow ended in a non-success state | No |
Browser Support
The Web SDK relies on crypto.randomUUID, ES2020 syntax,
and a modern fetch / postMessage. The
versions below are the earliest that carry all three; anything older is
not supported.
| Browser | Minimum version |
|---|---|
| Chrome / Edge (desktop) | 92+ |
| Firefox | 95+ |
| Safari (macOS and iOS) | 15.4+ |
| Chrome (Android) | 92+ |
Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
onError { INVALID_INPUT, "apiKey is required." }
|
apiKey missing or empty
|
Pass the public API Key |
onError { INVALID_INPUT, "accessToken is required." }
|
Backend didn't return sdkToken
|
Check the sdk.credentials integration
|
onError { INVALID_INPUT, "domainId is required." }
|
domainId not passed in withConf
|
Pass the id of the domain you picked from
domains[] in the sdk.credentials
response
|
onError { INVALID_INPUT, "email is required." }
|
Missing email
|
Pass the end-user email |
onError { INVALID_INPUT, "customerId is required." }
|
Missing customerId
|
Pass the internal user ID |
onError { INVALID_INPUT, "email format is invalid." }
|
Invalid email format | Validate in your form first |
onError { INVALID_INPUT, "apiKey does not match SDK credentials."
}
|
apiKey and accessToken belong to
different apps
|
Reissue the token with the same apiKey
|
onError { SDK_ERROR } |
Network / connection issue |
Rebuild the chain and launch() again
|
| Modal won't appear / iframe blocked |
Domain not allowlisted, or CSP frame-src missing the
origin
|
Add the origin under API & Integration in
the Console (see the note below the table), then allow it in your
CSP frame-src
|
| Camera permission denied |
Host Permissions-Policy is too strict
|
Update your policy headers (see note below) |
| No events fire | Session still ongoing, or callbacks not registered |
Wait for completion; confirm your
.on() callbacks
|
https:// or
http://, carry no path, and use no wildcards, so list
each host on its own line (for example
https://app.example.com and
https://checkout.example.com). If the button that
generates a key is not on that page, the role on your account is
missing the manage API keys permission — those
controls render by permission, so they are hidden rather than greyed
out; ask a tenant administrator to grant it.
modal mode the SDK inserts an iframe that loads the
verification page into your own page, so if your page sets a
Content-Security-Policy you must allow that origin under
frame-src, and grant camera (plus the microphone /
motion sensors used by liveness) to the same origin via
Permissions-Policy. Which origin to write depends on the
tag you installed (see Step 2): the default
(latest, baseline 1.0.1) build loads
https://identity.width.com, and the
@beta build loads
https://identity-platform.width.com. Send the block that
matches the build you ship — if the origin is absent from
frame-src, the browser blocks the iframe and the
verification window stays blank.
# Default (latest) build / 默认(latest)版
Content-Security-Policy: frame-src https://identity.width.com;
Permissions-Policy: camera=(self "https://identity.width.com"),
microphone=(self "https://identity.width.com"),
accelerometer=(self "https://identity.width.com"),
gyroscope=(self "https://identity.width.com")# @beta build / @beta 版
Content-Security-Policy: frame-src https://identity-platform.width.com;
Permissions-Policy: camera=(self "https://identity-platform.width.com"),
microphone=(self "https://identity-platform.width.com"),
accelerometer=(self "https://identity-platform.width.com"),
gyroscope=(self "https://identity-platform.width.com")