sumsub-integrate-websdk
End-to-end recipe for adding Sumsub KYC to a website or web app via the Sumsub WebSDK. TRIGGER when the user asks to "integrate / embed / add Sumsub", "show the KYC widget", "add WebSDK", "verify users with Sumsub on the frontend", supplies an existing levelName they want to plug into a page, or ask
By sumsub · 850 installs
npx skills add sumsub/agent-skills --skill sumsub-integrate-websdk
Source repository · Upstream listing
Sumsub — WebSDK integration
Embed Sumsub KYC into a web project end to end, from level creation to the
"applicantReviewed" webhook that gates user access.
⚠️ Sandbox tokens only
Do not accept or use a production App Token / secret during integration
work with this skill. The token generates real SDK sessions tied to real
applicants. Insist on a sandbox pair from
<https://cockpit.sumsub.com/checkus/home?sbx=true — Connect Sumsub to
your AI agent Build & configure Generate token . Token + secret
are revealed once at creation; copy both before closing the dialog. Helper scripts in sibling
skills enforce this with an sbx: prefix check; the curl recipes below
assume the same.
Deeper auth mechanics: [ sumsub api auth ](../sumsub api auth/SKILL.md).
The lifecycle in one picture
The split between browser events (UX) and webhooks + server reads
(authoritative truth) is the most missed part of a WebSDK integration. Don't
trust onApplicantStatusChanged for entitlement decisions.
Stage 1 — Have a level
Every SDK launch references a levelName that exists in the workspace.
If the user has one (e.g. basic kyc level , the Sumsub default), capture it
and move to Stage 2.
If the user doesn't yet have a level , brainstorm with them and hand off to
[ sumsub create level ](../sumsub create level/SKILL.md). Don't silently pick
defaults — the level encodes who can verify (country / applicant type) and
what they must provide (ID, selfie, PoA, questionnaire). A reasonable
starter flow when the user is genuinely unsure:
APPLICANT DATA — name, DOB, country, addresses.
IDENTITY — PASSPORT , ID CARD , DRIVERS (mode any ).
SELFIE — videoRequired: passiveLiveness .
Add PROOF OF RESIDENCE only if regulatory; add QUESTIONNAIRE only if
they need structured data (source of funds, occupation). For each addition,
ask "what decision does this gate?" before agreeing to include it.
Stage 2 — Server side access token endpoint
The SDK needs an access token , generated by your backend with the App
Token + secret. The token is short lived ( ttlInSecs , default 1800) and scoped to one (userId, levelName) pair.
Endpoint shape
Body: empty .
Auth: App Token + HMAC signature (see [ sumsub api auth ](../sumsub api auth/SKILL.md)).
Response: { "token": " act sbx <... ", "userId": "..." } .
userId choice (load bearing)
This is the externalUserId Sumsub stores against the applicant. Make it:
Stable per real user (don't regenerate on each page load — the SDK looks
up returning applicants by this id).
Opaque to the user (a UUID or DB row id; not their email).
Tied to your auth system (so a webhook callback can resolve it back to a
user record).
Wrong userId choice → duplicate applicants, "stuck in submitted" support
tickets, and the inability to resume an interrupted verification.
Curl recipe
URL encode userId if it might contain / , ? , or & . The signing string
must match the URI on the wire exactly — sign the encoded form.
Wiring it into the user's backend
Frame the endpoint as:
Path : any (e.g. POST /api/sumsub/access token ).
Inputs : the authenticated user's id, the levelName (often hardcoded
per page).
Auth : user must be logged in to your app — anyone hitting this route
can spin up a verification session for that userId .
Output : forward Sumsub's response body verbatim, or just the token
field. Don't cache it server side; the browser asks per launch.
Show the snippet for the user's actual stack (Express, FastAPI, Go, etc.)
but the contract is the same in all of them: sign, call Sumsub, return token.
Stage 3 — Frontend SDK init
Load the builder
This exposes the global snsWebSdk . For bundler based projects, an npm
package exists but the CDN script is what Sumsub officially documents and
what every framework wrapper ends up calling.
Container
Give the stage a defined min height (e.g. 600px ) so the iframe doesn't
collapse before the SDK adapts its height.
Don't skip the overlay loader. Between .launch() returning and the SDK
iframe loading content from api.sumsub.com/websdk/websdk.html there is a
1–3s window where the container holds an empty iframe and looks broken —
especially inside a modal that the user just opened. Mount a loader that
covers the container, then hide it in the idCheck.onReady handler (Stage 4).
Treating onReady as informational and leaving the handler empty is the
single most common "the widget is blank" report.
Canonical vanilla launch
See [ examples/vanilla.html ](examples/vanilla.html) for a runnable file.
Minimal shape:
React recipe
[ examples/react component.tsx ](examples/react component.tsx) — wraps the
same builder in a useEffect with cleanup. Two gotchas it handles:
1. The CDN script must be present before snsWebSdk is read. Either inject
it once in the document head, or dynamically load it and await the
<script 's load event.
2. On React 18 strict mode in dev, components mount twice — the cleanup
function must remove the iframe / clear the container, otherwise you
get two stacked widgets.
Other frameworks
The builder API is framework agnostic. For Vue/Svelte/Angular, mirror the
React pattern: lifecycle hook on mount → fetch token → build → launch into a
ref'd element; on unmount → empty the container.
Stage 4 — Client lifecycle events
Wire these handlers on the SDK instance. Treat them as UX signals , not
authoritative state.
Event When Use it for
idCheck.onReady SDK iframe content loaded Hide your own loader. Required — without this the modal looks empty for 1–3s after launch.
idCheck.onInitialized First screen rendered Analytics: "user saw KYC step"
idCheck.onStepInitiated Doc type screen shown Telemetry per doc type
idCheck.onStepCompleted A step finished Progress bar
idCheck.onApplicantSubmitted Docs submitted, server is processing Move user to a "waiting" view
idCheck.onApplicantStatusChanged Status moved Live progress hint (still not trusted)
idCheck.onApplicantResubmitted Re upload after a rejection Re arm waiting view
idCheck.onApplicantReviewed (or onApplicantVerificationCompleted in 2.0) Final verdict reached client side Show a preliminary result, then verify server side
idCheck.onError SDK error Surface a friendly retry CTA, log code + reason
idCheck.onUploadError / onUploadWarning Doc rejected at upload Inline guidance ("blurred photo", etc.)
idCheck.onLivenessCompleted (2.0 only) Liveness attempt finished Branch on answer for retry UX
idCheck.onResize Frame resized Adjust surrounding layout
Full payload fields per event: [ references/lifecycle.md ](references/lifecycle.md).
Required handlers for a baseline integration
If you wire nothing else, wire these three. Skipping any of them produces a
known bad UX:
idCheck.onReady → hide the overlay loader from Stage 3. Without this the
modal looks blank for 1–3s after .launch() .
idCheck.onApplicantSubmitted → move the user to a "we're reviewing"
state. Without this the user re uploads or contacts support.
idCheck.onError → render a retryable error to the UI. Without this
failures only land in console.error and the user sees a stuck loader.
Why you can't trust onApplicantReviewed alone
The browser event fires from inside the iframe. A bad actor can spoof it
trivially. The only authoritative signal is server side : either a
webhook delivery (Stage 5) or an authenticated GET against
/resources/applicants/{userId}/one .
Stage 5 — Server side source of truth
Webhook receiver
Sumsub POSTs JSON to your URL on every event. Two paths for registering it:
Sandbox (while building this integration): use the
[ sumsub manage webhooks ](../sumsub manage webhooks/SKILL.md) skill. It
builds the clientWebhooks payload from a compact spec, POSTs to
/resources/api/agent/clientWebhooks with App Token auth, refuses non sbx: tokens,
rejects localhost / 127.0.0.1 targets up front, and walks the user
through exposing their local receiver via ngrok http <port so Sumsub can
actually reach it. Hand off the target , types[] , and signatureAlgorithm
the user wants and let that skill do the POST.
Production: do not create the prod webhook from any skill, including
this one. Production webhook setup must be done by a human directly in the
Sumsub dashboard (Integrations → Webhooks, workspace toggle on
Production ). The signing secret authenticates real PII deliveries; the
audit trail should attribute setup to a person. Prototype the spec against
sandbox here, then hand the final settings (target, event list, signature
algorithm, custom headers) to whoever has prod access to recreate manually.
Headers you care about:
x payload digest — the signature, hex encoded.
x payload digest alg — HMAC SHA256 HEX (default), HMAC SHA512 HEX ,
or the legacy HMAC SHA1 HEX .
The signing secret is not your App Token secret . It's a separate
webhook secret generated (or supplied) at webhook creation time in the
dashboard. Store it in env ( SUMSUB WEBHOOK SECRET ) alongside the App Token
pair.
Verification recipe — note the raw bytes requirement; do NOT JSON parse
before computing the digest, because re serialising changes whitespace and
key order:
See [ examples/webhook verify.js ](examples/webhook verify.js) for a
complete handler.
Local testing with ngrok
Sumsub needs a publicly reachable URL — your laptop's localhost won't do.
The fastest end to end loop for local dev:
webhook.site works too if you just want to see the raw payload without
running a local receiver — but it can't echo back a 200 to verify the
delivery flow.
Webhook events that matter for a KYC flow
type What it means Action
applicantCreated First time you minted a token for this externalUserId Log; nothing required
applicantPending User finished uploading; Sumsub is checking Show "in review"
applicantPrechecked Primary data processing done, queued for human/AML Still "in review"
applicantOnHold Paused (often AML hit needing analyst) Surface to ops; tell user "extra checks"
applicantReviewed Final verdict — reviewResult.reviewAnswer is GREEN or RED Gate access here. Mark user verified or rejected.
applicantPersonalInfoChanged User edited info after submission Re check before granting access
applicantWorkflowCompleted Whole workflow (multi level) done Same as applicantReviewed for single level flows
applicantActionPending / Reviewed One off action (separate from the level flow) Per action handling
reviewResult.reviewAnswer :
GREEN — approved.
RED — rejected. rejectLabels says why; reviewRejectType is FINAL
(can't retry) or RETRY (user may resubmit).
Full list: [Sumsub webhook docs](https://docs.sumsub.com/docs/user verification webhooks).
Server side status check (fallback / on demand)
For pages that need to check status synchronously (e.g. user logs back in
between webhook arriving and your DB updating):
Reads reviewStatus and reviewResult.reviewAnswer . Cheap; use as a
fallback, not as a polling loop — webhooks are the primary signal.
Resumption / returning users
The SDK looks up applicants by externalUserId . If a user starts
verification, abandons, and returns 3 days later:
1. Your endpoint mints a new access token for the same userId + same
levelName .
2. The SDK opens to wherever the user left off (re uploads only the missing
steps).
3. No duplicate applicant is created.
Don't generate a new userId for returning users — that's the 1 cause of
"the user is stuck and customer support sees two applicants".
Token refresh
The first argume