sumsub-manage-webhooks — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sumsub-manage-webhooks (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Lists, retrieves, creates, updates, and disables/enables ClientWebhook event subscriptions. Reads use /resources/api/clientWebhooks; writes use /resources/api/agent/clientWebhooks.
| Verb | Path | Purpose |
|---|---|---|
GET | /resources/api/clientWebhooks | List webhooks on the tenant. Returns EntityResult<ClientWebhook> ({list: {items: [...] }}). Capped at the oldest 50 server-side (getOldest50). |
GET | /resources/api/clientWebhooks/{id} | Read one webhook by id. Use this to resolve a name from a known id, or to verify what landed after a write. |
POST | /resources/api/agent/clientWebhooks | Create. Body must NOT include id — server assigns it. (The model layer still does an internal upsert, but the request DTO is ClientWebhookCreateRequest without id.) |
PATCH | /resources/api/agent/clientWebhooks | Update an existing webhook (by id in body). DTO is ClientWebhookUpdateRequest. |
Permission required: manageClientSettings.
There is no DELETE and no `/stats` endpoint on the public API — use the Sumsub dashboard UI when you need to delete a webhook or view per-webhook delivery stats.
This skill talks to the public Sumsub API and signs each request per the authentication reference. The full how-it-works writeup lives in the sumsub-api-auth skill — read it if you hit 401 Invalid signature.
⚠️ Sandbox tokens only. Do not accept or use a production App Token here. If the user offers one, refuse and ask them to generate a sandbox pair at <https://cockpit.sumsub.com/checkus/devSpace/appTokens> (toggle the workspace to Sandbox first, then Create). Token + secret are shown once — copy both before closing the dialog. The helper script enforces this — it rejects tokens that don't start withsbx:unlessSUMSUB_ALLOW_PROD=1is set.
| Var | Example |
|---|---|
SUMSUB_APP_TOKEN | sbx:... — sandbox App Token from the dashboard. |
SUMSUB_SECRET_KEY | The paired secret shown once at token creation. |
SUMSUB_BASE | Optional. Defaults to https://api.sumsub.com. |
If the user has already supplied credentials in conversation, reuse them; otherwise ask once before running. Never echo the secret back.
Because this skill only accepts sandbox App Tokens, every webhook it creates, updates, or toggles lives in the sandbox workspace. Sandbox and production are separate tenants on Sumsub's side — there is no "promote to prod" path, and re-running this skill with a production token is not the right way to set up a real webhook.
When the user is ready to wire up a production webhook:
it without SUMSUB_ALLOW_PROD=1, and you should not suggest that override).
event subscription, custom headers — should be configured by a human directly in the Sumsub dashboard (Integrations → Webhooks, with the workspace toggle on Production). Setting up a production webhook is a security-sensitive operation (the signing secret authenticates real PII deliveries) and the audit trail should attribute it to a person.
the final spec the user wants (event list, headers, signature algorithm), then hand that spec off as plain documentation so a human can recreate it in production.
manage_webhooks.sh is the orchestrator:
manage_webhooks.sh list # GET all webhooks (table summary; capped at 50)
manage_webhooks.sh list --json # raw JSON of all webhooks
manage_webhooks.sh get <webhookId> # one webhook (filtered from the list)
manage_webhooks.sh create <spec.json> # POST without id (compact spec → ClientWebhook)
manage_webhooks.sh update <spec.json> # POST with id (spec MUST contain id)
manage_webhooks.sh disable <webhookId> # GET → flip disabled=true → POST
manage_webhooks.sh enable <webhookId> # GET → flip disabled=false → POSTcreate and update both call build_webhook_payload.py to expand the compact spec.
Sumsub delivers webhooks from its own infrastructure, so the target URL has to resolve and accept connections from the public internet. Common gotcha: users paste http://localhost:3000/webhook (or 127.0.0.1, 0.0.0.0, ::1) while developing locally. Sumsub accepts the URL at creation time but every delivery will fail — and targets like these are rejected by the skill's payload builder up front.
If the user supplies a localhost-ish URL, don't submit it. Instead, walk them through exposing the local server through a public tunnel before creating the webhook:
brew install ngrok/ngrok/ngrok. Other platforms: download from the link. First-time users need a free ngrok account to grab an auth token, then ngrok config add-authtoken <TOKEN> once.ngrok http <port> in a separate terminal and keep it open.Forwarding https://<random>.ngrok-free.app -> http://localhost:<port> line. The https://...ngrok-free.app part is the public URL./webhook, /sumsub) and use the full URL as target. Then re-run the create subcommand.Heads-up to mention: on the free ngrok plan the public URL changes every time ngrok restarts — the webhook will need to be re-updated (POST with the existing id and the new target) each session. A reserved domain (paid) or --domain=<your-subdomain> keeps it stable. Alternatives if the user prefers: Cloudflare Tunnel (cloudflared tunnel), Tailscale Funnel, localtunnel — same idea, same procedure.
create / update# Identity (omit on create; required on update)
id: 698bfc... # id from a previous list / create response
# Display + addressing
name: "Production webhook" # required (no min length but the dashboard expects something)
description: "Sends KYC events to our backend"
target: "https://example.com/sumsub/webhook" # required — destination URL (or slack / email / telegram address depending on targetType)
targetType: http # http | email | slack | telegram (default: http)
# Subscription
types: # required — event-type strings (see "Event types" below)
- applicantReviewed
- applicantPending
- applicantOnHold
- applicantCreated
applicantType: individual # individual | company (omit to subscribe to both)
sourceKeys: [] # optional — restrict to specific source keys
# Auth + delivery
secretKey: "..." # HMAC secret used to sign payloads
signatureAlgorithm: HMAC_SHA256_HEX # HMAC_SHA1_HEX | HMAC_SHA256_HEX | HMAC_SHA512_HEX (default: SHA256)
headers: # optional extra HTTP headers added to each delivery
- { key: "X-Source", value: "sumsub" }
- { key: "Authorization", value: "Bearer ${MY_TOKEN}" } # caller substitutes before sending
# Lifecycle flags
disabled: false # default false; set true to pause without deleting
notResendFailedWebhooks: false # default false; true = no automatic retries on delivery failureThe builder validates enums (targetType, signatureAlgorithm, applicantType), rejects empty types, and wraps headers so that the key/value shape matches ClientWebhookHeader. Unknown keys pass through (escape hatch).
types[])The OpenAPI keeps types as a free-form string[]. The names below cover the commonly-emitted Sumsub events. Unknown event types are silently accepted server-side and the webhook simply never fires — so typos are not caught by the API.
| Group | Event type | When it fires |
|---|---|---|
| Applicant lifecycle | applicantCreated | New applicant created |
applicantPrechecked | Pre-screen complete | |
applicantPending | Submitted for review | |
applicantReviewed | Final review answer (GREEN / RED) reached | |
applicantOnHold | Review held / paused | |
applicantActivated | Applicant activated | |
applicantDeactivated | Applicant deactivated | |
applicantReset | Verification reset (retry) | |
applicantLevelChanged | Level reassigned | |
applicantTagsChanged | Tags added/removed | |
applicantPersonalInfoChanged | Personal info edited | |
applicantDeleted | Applicant deleted | |
applicantPersonalDataDeleted | GDPR personal-data erasure executed | |
| Action workflow | applicantActionPending / applicantActionReviewed / applicantActionOnHold | Action-flow events |
| Workflow | applicantWorkflowCompleted | Workflow run finished (not applicantWorkflowRunCompleted) |
| Video ident | videoIdentStatusChanged | Live status update |
videoIdentCompositionCompleted | Recording assembly finished | |
| KYT (applicant-scoped) | applicantKytTxnApproved / applicantKytTxnRejected / applicantKytTxnReviewed / applicantKytTxnDeleted / applicantKytTxnDataChanged / applicantKytTxnAwaitingUser / applicantKytOnHold | Per-applicant transaction-monitoring events |
| KYT (case-scoped) | kytCaseCreated / kytCaseStatusChanged / kytCaseReviewed | KYT case-management events (note: it's kytCaseStatusChanged, not kytCaseUpdated) |
| AML case | amlCaseApproved / amlCaseRejected / amlCaseOnHold | AML-case disposition events |
| Travel Rule | travelRuleAction | Travel-rule lifecycle events |
| KYB | kybCompanyActivity | KYB ongoing-monitoring events |
The skill forwards whatever the caller writes — no client-side validation, since Sumsub may add events faster than this list updates.
id, name, target, disabled, types[], applicantType, signatureAlgorithm, createdAt.secretKey redacted in the output as a defensive measure).ClientWebhook (with server-assigned id on create) and a one-line summary.disabled value.examples/basic-http.json — minimal webhook: HTTPS endpoint, four applicant-lifecycle events, SHA-256 signing.examples/with-headers-and-restrictions.json — broad event set scoped to a specific sourceKey, with custom HTTP headers.examples/legacy-sha1.json — SHA1-signed webhook, for receivers that already verify SHA1.examples/update-existing.json — same as basic but with id set, demonstrating the update path.references/webhook-schema.md — full ClientWebhook schema, all enums, gotchas (secret not write-only, signing semantics).~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.