sumsub/agent-skills

sumsub-integrate-websdk

End-to-end recipe for adding Sumsub KYC to a website or web app via the Sumsub WebSDK.

Ver código-fonte
Documento original do Skill

Renderizado do repositório de origem, preservando títulos, exemplos, código, tabelas, links e imagens.

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`.

The lifecycle in one picture

 ┌─────────────────────────┐
 │ 1. Level exists in the   │  ← one-time, done in dashboard or
 │    workspace              │     via sumsub-create-level
 └─────────────┬───────────┘
               │ levelName
 ┌─────────────▼───────────┐  ┌──────────────────────────────────┐
 │ 2. Server-side token     │◀─┤ Browser calls /api/sumsub/token │
 │    endpoint (HMAC-signed │  └──────────────────────────────────┘
 │    POST /resources/      │
 │    accessTokens)         │
 └─────────────┬───────────┘
               │ {token, userId}
 ┌─────────────▼───────────┐
 │ 3. Browser: snsWebSdk    │  ← user fills doc capture / selfie / form
 │    init → build → launch │     events fire: onApplicantSubmitted, etc.
 └─────────────┬───────────┘
               │ documents submitted
 ┌─────────────▼───────────┐  ┌──────────────────────────────────┐
 │ 4. Sumsub runs checks    │─▶│ Webhook POST → your server      │
 │    (async, ~seconds–min) │  │ (applicantReviewed = the truth) │
 └─────────────┬───────────┘  └──────────────────────────────────┘
               │ verdict
 ┌─────────────▼───────────┐
 │ 5. Your app gates access │  ← server checks reviewAnswer, not
 │    by reading applicant  │     the browser. Browser events are
 │    via GET /applicants…  │     UX only.
 └─────────────────────────┘

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`. 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.
  • IDENTITYPASSPORT, ID_CARD, DRIVERS (mode any).
  • SELFIEvideoRequired: 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

POST https://api.sumsub.com/resources/accessTokens
  ?userId=<your-stable-user-id>
  &levelName=<level-from-stage-1>
  &ttlInSecs=600
  • Body: empty.
  • Auth: App Token + HMAC signature (see `sumsub-api-auth`).
  • 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

bash
SUMSUB_APP_TOKEN='sbx:...'
SUMSUB_SECRET_KEY='...'
USER_ID='u-12345'
LEVEL='basic-kyc-level'

PATH_Q="/resources/accessTokens?userId=${USER_ID}&levelName=${LEVEL}&ttlInSecs=600"
TS=$(date -u +%s)
SIG=$(printf '%s%s%s' "$TS" "POST" "$PATH_Q" \
      | openssl dgst -sha256 -hmac "$SUMSUB_SECRET_KEY" -hex \
      | awk '{print $NF}')

curl -sS -X POST \
  -H "X-App-Token: $SUMSUB_APP_TOKEN" \
  -H "X-App-Access-Ts: $TS" \
  -H "X-App-Access-Sig: $SIG" \
  "https://api.sumsub.com${PATH_Q}"

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

html
<script src="https://static.sumsub.com/idensic/static/sns-websdk-builder.js"></script>

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

html
<div class="kyc-stage" style="position: relative; min-height: 600px;">
  <div id="sumsub-websdk-container"></div>
  <!-- Overlay loader covers the empty-iframe window. Hide on idCheck.onReady. -->
  <div id="kyc-loader" style="position: absolute; inset: 0; display: grid; place-items: center;">
    Loading verification…
  </div>
</div>

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` for a runnable file. Minimal shape:

js
async function getAccessToken() {
  const r = await fetch('/api/sumsub/access-token', { method: 'POST' });
  if (!r.ok) throw new Error('failed to mint access token');
  return (await r.json()).token;
}

const initialToken = await getAccessToken();

const sdk = snsWebSdk
  .init(initialToken, () => getAccessToken())   // refresh callback, returns Promise<string>
  .withConf({
    lang: 'en',
    email: currentUser.email,                   // optional, prefills
    phone: currentUser.phone,                   // optional, prefills
    theme: 'light',                             // 'light' | 'dark'
  })
  .withOptions({
    addViewportTag: false,                      // host page already sets it
    adaptIframeHeight: true,
  })
  .on('idCheck.onReady', () => {
    // SDK iframe content loaded — hide the overlay loader from the container snippet.
    document.getElementById('kyc-loader')?.style.setProperty('display', 'none');
  })
  .on('idCheck.onApplicantSubmitted', () => {
    // user just finished uploading; show "we're reviewing"
  })
  .on('idCheck.onApplicantStatusChanged', (payload) => {
    // status moved; payload.reviewStatus = 'pending' | 'queued' | 'completed' | ...
  })
  .on('idCheck.onError', (err) => {
    console.error('sumsub error', err);
  })
  .onMessage((type, payload) => {
    // catch-all firehose — useful for analytics or debugging
  })
  .build();

sdk.launch('#sumsub-websdk-container');

React recipe

`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.

  1. 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.

EventWhenUse it for
idCheck.onReadySDK iframe content loadedHide your own loader. Required — without this the modal looks empty for 1–3s after launch.
idCheck.onInitializedFirst screen renderedAnalytics: "user saw KYC step"
idCheck.onStepInitiatedDoc-type screen shownTelemetry per doc type
idCheck.onStepCompletedA step finishedProgress bar
idCheck.onApplicantSubmittedDocs submitted, server is processingMove user to a "waiting" view
idCheck.onApplicantStatusChangedStatus movedLive progress hint (still not trusted)
idCheck.onApplicantResubmittedRe-upload after a rejectionRe-arm waiting view
idCheck.onApplicantReviewed (or `onApplicantVerificationCompleted` in 2.0)Final verdict reached client-sideShow a preliminary result, then verify server-side
idCheck.onErrorSDK errorSurface a friendly retry CTA, log code + reason
idCheck.onUploadError / onUploadWarningDoc rejected at uploadInline guidance ("blurred photo", etc.)
idCheck.onLivenessCompleted (2.0 only)Liveness attempt finishedBranch on answer for retry UX
idCheck.onResizeFrame resizedAdjust surrounding layout

Full payload fields per event: `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` 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-algHMAC_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:

js
// Node/Express — bodyParser.raw() so req.body is a Buffer
import crypto from 'node:crypto';

const ALG = { HMAC_SHA1_HEX: 'sha1', HMAC_SHA256_HEX: 'sha256', HMAC_SHA512_HEX: 'sha512' };

function verifySumsubWebhook(req, secret) {
  const alg = ALG[req.header('x-payload-digest-alg') || 'HMAC_SHA256_HEX'];
  const expected = req.header('x-payload-digest');
  const actual = crypto.createHmac(alg, secret).update(req.body).digest('hex');
  return Buffer.from(actual, 'hex').length === Buffer.from(expected, 'hex').length
      && crypto.timingSafeEqual(Buffer.from(actual, 'hex'), Buffer.from(expected, 'hex'));
}

See `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:

bash
# 1. In one shell, start your local receiver (Node, Python, whatever).
node server.js                    # listens on http://localhost:3000

# 2. In another shell, tunnel that port.
ngrok http 3000                   # prints https://<random>.ngrok-free.app -> http://localhost:3000

# 3. Register the webhook against the ngrok URL.
#    Either via the dashboard (Integrations → Webhooks) OR via
#    sumsub-manage-webhooks `create` with target = the ngrok https URL.

# 4. Trigger an event by running a sandbox WebSDK verification end-to-end.
#    Watch the request arrive in your local server logs.

# 5. When you're happy, PATCH the webhook to point at your real server
#    hostname (sumsub-manage-webhooks `update` command).

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

typeWhat it meansAction
applicantCreatedFirst time you minted a token for this externalUserIdLog; nothing required
applicantPendingUser finished uploading; Sumsub is checkingShow "in review"
applicantPrecheckedPrimary data processing done, queued for human/AMLStill "in review"
applicantOnHoldPaused (often AML hit needing analyst)Surface to ops; tell user "extra checks"
applicantReviewedFinal verdict — reviewResult.reviewAnswer is GREEN or REDGate access here. Mark user verified or rejected.
applicantPersonalInfoChangedUser edited info after submissionRe-check before granting access
applicantWorkflowCompletedWhole workflow (multi-level) doneSame as applicantReviewed for single-level flows
applicantActionPending / ReviewedOne-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.

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):

bash
PATH_Q="/resources/applicants/${USER_ID}/one"
TS=$(date -u +%s)
SIG=$(printf '%s%s%s' "$TS" "GET" "$PATH_Q" \
      | openssl dgst -sha256 -hmac "$SUMSUB_SECRET_KEY" -hex \
      | awk '{print $NF}')

curl -sS \
  -H "X-App-Token: $SUMSUB_APP_TOKEN" \
  -H "X-App-Access-Ts: $TS" \
  -H "X-App-Access-Sig: $SIG" \
  "https://api.sumsub.com${PATH_Q}"

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.

  1. The SDK opens to wherever the user left off (re-uploads only the missing

steps).

  1. 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 argument to .init(token, refreshCallback) covers expiry mid-session:

  • SDK calls your refreshCallback when the token nears expiry.
  • It must return Promise<string> resolving to a fresh token (call your

endpoint again).

  • If you return a stale or wrong-userId token, the SDK hangs.

For most flows a 600-second TTL is plenty. Don't pre-fetch and cache — mint on demand.

Sandbox testing

In sandbox mode:

  • Use test documents from the Sumsub docs ("Test documents" page) to

trigger GREEN vs RED outcomes without uploading real PII.

  • AML hits are simulated — names like Greenacre go through; Aikman and

similar are pre-loaded as positive matches.

  • Webhook delivery works the same. For a local receiver, expose it through

ngrok http <port> (or Cloudflare Tunnel / Tailscale Funnel) and register the public URL via the `sumsub-manage-webhooks` skill — it has the full walkthrough and rejects raw localhost targets before they ever reach Sumsub. webhook.site is fine for inspecting payload shapes without a real receiver.

  • Selfies in sandbox bypass real biometrics — any face works.

Sandbox tokens (sbx:) only fire against the sandbox workspace. Production tokens (prd:) only fire against production. There's no fall-through.

Going live checklist

When the user says "we're ready to switch to prod":

  • [ ] Webhook receiver verifies the signature on raw bytes (replay

Stage 5 test against a real delivery).

  • [ ] externalUserId is the stable user id, not the email / display name.
  • [ ] Server is the source of truth for verification state. Browser events

may be ignored entirely.

  • [ ] Token endpoint is auth-gated (only logged-in users can mint a token

for themselves).

  • [ ] applicantReviewed triggers the user-facing state change in your DB,

with idempotency (the same event can arrive twice).

  • [ ] Per-user retry handling: reviewResult.reviewRejectType === 'RETRY'

⇒ let user re-launch the SDK; 'FINAL' ⇒ block.

  • [ ] Production App Token + secret + separate webhook secret are all

in the prod secret store. Sandbox values stay only in dev env.

See also

with payload fields, plus webhook event reference.

integration.

hook + cleanup pattern.

verification with raw-body handling.

reference, shared with every other Sumsub skill.

Stage-1 hand-off.

Stage-5 hand-off: create / list / update / disable sandbox webhooks via the public API, with the localhost-rejection and ngrok walkthrough built in. Production webhook setup is dashboard-only and must be done by a human.

source if anything in this skill drifts.

do mesmo repositório

Mais Skills

Todos os Skills
sumsub
Comunidade

sumsub-analyze-regulation

Analyze a regulation document (PDF or text) and produce a Sumsub configuration plan — mapping regulatory requirements to Sumsub entities (levels, questionnaires, PoA presets, TM rules, workflows, AML resolution rules). TRIGGER when the user provides a regulation PDF, legal act, or compliance requirement document and wants to know what to configure in Sumsub. Acts as the entry point before invoking sumsub-create-level, sumsub-create-questionnaire, sumsub-create-poa-preset, sumsub-create-workflow, sumsub-create-aml-resolution-rules, and other skills. SKIP for direct entity creation requests (no regulatory context) or Sumsub API calls.

instalações
1
GitHub Stars
6
Atualizado
3 de set.
sumsub
Comunidade

sumsub-api-auth

Authenticate to the Sumsub API with an App Token + secret key (HMAC-SHA256 request signing). TRIGGER when the user asks to "call / sign / authenticate Sumsub API requests", debugs 401 Unauthorized / signature errors against api.sumsub.com, or needs a working request example with X-App-Token / X-App-Access-Sig / X-App-Access-Ts headers. SKIP only when a more specific skill in this repo (questionnaire/level/workflow/POA-preset/generic) already covers the user's actual task — those skills sign requests the same way and only need this one for auth deep dives.

instalações
1
GitHub Stars
6
Atualizado
3 de set.
sumsub
Comunidade

sumsub-api-generic

Catch-all fallback for any Sumsub API task that does NOT match a more specific skill (e.g. create-sumsub-level, sumsub-create-questionnaire, sumsub-api-auth). TRIGGER when the user wants to call, inspect, or debug a Sumsub API endpoint not otherwise covered — fetching applicants, listing levels, reviewing AML hits, exporting data, generating SDK tokens, anything against api.sumsub.com. The procedure — locate the right endpoint in the OpenAPI schema, read its request/response shape, build the payload, sign with App Token, and validate. SKIP whenever a narrower Sumsub skill already covers the request.

instalações
1
GitHub Stars
6
Atualizado
3 de set.
sumsub
Comunidade

sumsub-create-aml-resolution-rules

Create, edit, reorder, delete, and publish Sumsub AML Resolution Rules (the AML Resolution Rule Chain) that auto-review AML screening hits. TRIGGER when the user wants to auto-clear false positives, auto-confirm true positives, carry over previous AML reviews, tag AML hits, or set up / inspect / publish the AML rule chain. SKIP for transaction-monitoring (KYT) rules, workflow routing, or AML check settings on a level (separate skills cover those).

instalações
1
GitHub Stars
6
Atualizado
3 de set.