sumsub-create-level — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sumsub-create-level (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.
Builds an ApplicantLevel JSON payload from a compact spec, POSTs (or PATCHes) it to the Sumsub API, and reports the resulting name / id.
| Method | Path | When |
|---|---|---|
POST | /resources/applicants/-/levels | Create a new level. Body must NOT include id or key — server assigns them. |
PATCH | /resources/applicants/-/levels | Update an existing level (by id in body). |
GET | /resources/applicants/-/levels/{id} | Read one level. Use this to verify what landed (tenant gates may silently drop fields) or to resolve name from a known id. |
GET | /resources/applicants/-/levels | List all levels (use to reuse existing levels before creating duplicates). |
Body: ApplicantLevel. Returns the persisted level with id, createdAt, audit trails.
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 — creating a level is a workspace-visible write. If the user offers a prod token, 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 with sbx:.| 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.
sumsub-check-permissions skill and parse the JSON result. Store the allowed array; use it for all feature-gate checks in step 2 and the entitlements section below. Do this before anything else.id and skip the POST — the server creates a duplicate on every POST with no name deduplication.type is a real IdDocSetType, every QUESTIONNAIRE step has a questionnaireDefId that points to an existing questionnaire, every PROOF_OF_RESIDENCE* step has a poaPresetId / poaStepSettingsId (the server rejects the level if it's missing — run sumsub-create-poa-preset first if no preset exists), every COMPANY step has at least one named sub-step. Use the allowed array from step 0 to reject any entitlement-gated feature not present in it.${CLAUDE_SKILL_DIR}/scripts/build_level.py with the spec on stdin → full payload on stdout.${CLAUDE_SKILL_DIR}/scripts/post_level.sh. Returns response body + HTTP status.${CLAUDE_SKILL_DIR}/scripts/get_level.sh so the user sees the diff, then PATCH via ${CLAUDE_SKILL_DIR}/scripts/patch_level.sh. The spec passed to `build_level.py` must include `id: <level-id>` — the builder preserves it into the payload, and PATCH refuses bodies without it. PATCH replaces `requiredIdDocs.docSets` as a single array — every docSet you send must carry the full intended state, since fields omitted from a docSet are wiped on the server. Copy preserved values from the GET response into your PATCH spec.${CLAUDE_SKILL_DIR}/scripts/get_level.sh and compare to what was sent — several fields land differently from what was sent (see gotchas in references/level-schema.md). Report any discrepancy to the user.id, applicantType, and clientId from the response body and format: https://cockpit.sumsub.com/checkus/sdkIntegrations/levels/<applicantType>Level/<id>?clientId=<clientId>&xSNSEnv=sbxThe <applicantType>Level segment is literally individualLevel for applicantType: individual (confirmed) and companyLevel for applicantType: company (assumed by analogy — surface as the best guess and flag if it 404s). The xSNSEnv=sbx query param targets the Sandbox workspace — it is the canonical sandbox link param shared across all skills.
name, applicantType, ordered list of docSets created.Level ID (for SDK access tokens / future PATCH): <id>.Surface 4xx errors verbatim — they usually point to a missing questionnaireDefId or an unknown enum value.
Many level settings are gated behind tenant entitlements (allowedChecks). Before enabling a feature, check that the required BackgroundCheckTarget is present in the allowed array returned by sumsub-check-permissions (fetched in step 0).
DocSet type → required permission (OR — any one suffices):
| DocSet type | Required (any one of) | |||
|---|---|---|---|---|
QUESTIONNAIRE / QUESTIONNAIRE2-4 | QUESTIONNAIRE | |||
COMPANY / COMPANY_DATA | COMPANY \ | KYB_FULL \ | KYB_AUTO_AML_AND_REGISTRY \ | KYB_AUTO_AML_ONLY |
SOLANA_ATTESTATION / LINEA_ATTESTATION | PAYMENT_METHOD_CRYPTO | |||
PAYMENT_METHODS | PAYMENT_SOURCE \ | PAYMENT_METHOD \ | PAYMENT_METHOD_CRYPTO \ | KYT_UNHOSTED_WALLET_VERIFICATION |
INVESTABILITY | PROOF_OF_FUNDS | |||
PROOF_OF_RESIDENCE / PROOF_OF_RESIDENCE2 | POA \ | ADVANCED_POA_TYPE_DETECTION (often missing from allowedChecks even when the API actually allows it — proceed with a one-line warning; see below) | ||
E_KYC | E_KYC_TARGET | |||
E_SIGN | E_SIGN_TARGET | |||
TR_RECIPIENT_INFORMATION | TRAVEL_RULE | |||
DEVICE_CHECK | DEVICE_INTELLIGENCE |
Types not in this table (IDENTITY, SELFIE, APPLICANT_DATA, EMAIL_VERIFICATION, PHONE_VERIFICATION, etc.) are available to all tenants — no entitlement required.
If a requested feature requires an entitlement the tenant doesn't have — stop immediately. Do not build or POST the level. Tell the user which entitlement is missing, that the feature is unavailable on their account, and that they need to contact their CSM or Sumsub support to get it enabled. Resume only after the user confirms the entitlement has been added or explicitly decides to drop the feature.
Exception — POA. The POA / ADVANCED_POA_TYPE_DETECTION keys are often absent from allowedChecks even on tenants where the API actually accepts PROOF_OF_RESIDENCE levels (the entitlement seems to be baseline or covered by other keys; the documented mapping is stale on some tenants). When only POA is missing, proceed with a one-line warning to the user so they have context if support is later needed. Do NOT pause for explicit confirmation — Sumsub itself will reject the write if the tenant truly lacks the right, and that 4xx will be more informative than a pre-emptive halt. Surface any entitlement-related error verbatim if it comes back.
Creating a level is a write to a shared workspace. Always:
id.This applies to every message you send the user about this level — not just the final report:
name / title (e.g. "POA preset «POA — 60 days»", "questionnaire «Applicant basics»"). Do not paste the raw id ("6a16bfd4ded0fe13aa48165d") into prose — the user can't read it and it does not let them judge whether you picked the right entity.Level ID (for SDK access tokens / future PATCH): <id> on its own line — that line is the one place a raw id is correct, because the user needs to copy it for the next API call.If you don't yet know an entity's name (e.g. user supplied only an id from outside this session), GET the entity first and surface its name; do not fall back to the id.
Accepts JSON or YAML on stdin. The builder fills in sensible defaults for each doc-set type (see references/level-schema.md).
{
"name": "Basic KYC",
"applicantType": "individual",
"type": "standalone",
"docSets": [
{"type": "IDENTITY", "docTypes": ["PASSPORT","ID_CARD","DRIVERS"]},
{"type": "SELFIE", "videoRequired": "passiveLiveness"},
{"type": "PROOF_OF_RESIDENCE", "docTypes": ["UTILITY_BILL"]},
{"type": "QUESTIONNAIRE", "questionnaireDefId": "source-of-funds"}
]
}docSets[].type valuesAPPLICANT_DATA, EMAIL_VERIFICATION, PHONE_VERIFICATION, IDENTITY, IDENTITY2/3/4, SELFIE, SELFIE2, PROOF_OF_RESIDENCE, PROOF_OF_RESIDENCE2, PROOF_OF_PAYMENT, PAYMENT_METHODS, INVESTABILITY, COMPANY, COMPANY_DATA, COMPANY_DOCUMENTS, COMPANY_BENEFICIARIES, ACCREDITED_INVESTOR, E_SIGN, QUESTIONNAIRE/2/3/4, E_KYC, OTHER_DOCS, TR_RECIPIENT_INFORMATION, DEVICE_CHECK.
type | Shortcut keys | Builder expands to |
|---|---|---|
IDENTITY* | docTypes; videoRequired (disabled / docapture); captureMode & uploaderMode (sent only when docapture); nfcVerificationSettings: {mode} | flat fields on the docSet — only what you set. See Dashboard ↔ API mapping below. |
SELFIE* | videoRequired (default passiveLiveness; full set: disabled / enabled / photoRequired / passiveLiveness / staticLiveness), docTypes (default ["SELFIE"]) | bare docSet with videoRequired |
PROOF_OF_RESIDENCE* | docTypes (default ["UTILITY_BILL"]); `poaPresetId` (or poaStepSettingsId) to attach a POA preset by id | docSet + poaStepSettingsId |
QUESTIONNAIRE* | `questionnaireDefId` (or questionnaireId alias) — required | bare docSet |
APPLICANT_DATA | fields — array of strings or {name, required, prefill, immutableIfPresent} | fields[] with defaults |
PAYMENT_METHODS | paymentMethods (pass-through) | as-is |
EMAIL_VERIFICATION / PHONE_VERIFICATION | — | bare docSet |
COMPANY | steps — array of {name, minDocsCnt?, idDocTypes?, idDocSubTypes?, fields?, applicantLevelName?} | full KYB step structure |
E_SIGN | esignSettings (pass-through) | as-is |
Unknown keys in a docSet are passed through to the API verbatim, so escape hatches are easy when you need an obscure field.
Verbatim labels from the Sumsub dashboard sidebar, paired with the API value to write. Match the user's description to a label, then use the value.
| Dashboard control | Label (user-visible) | API value | Spec key |
|---|---|---|---|
| Capture method | File upload | disabled | videoRequired |
| Capture method | Live capture | docapture | videoRequired |
| Capture mode¹ | Both manual and auto capture work at the same time | manualAndAuto | captureMode |
| Capture mode¹ | Only manual capture is active | manualOnly | captureMode |
| Capture mode¹ | Seamless live capture | seamless | captureMode |
| Fallback to file upload¹ | Always available | always | uploaderMode |
| Fallback to file upload¹ | Available only if camera capture failed | fallback | uploaderMode |
| Fallback to file upload¹ | Not available | never | uploaderMode |
| NFC verification² | Disabled | disabled | nfcVerificationSettings.mode |
| NFC verification² | Optional | optional | nfcVerificationSettings.mode |
| NFC verification² | Required | required | nfcVerificationSettings.mode |
¹ Only meaningful with videoRequired: docapture. Silently dropped from the payload otherwise (matches the dashboard's own behavior — it deletes both keys when the radio is toggled to File upload). ² required auto-rejects Web SDK applicants; Mobile SDK only.
Defaults emitted by the builder (necessary for the dashboard to render — the controls bind to actual stored values, not implicit UI defaults; empty fields render as "Select" placeholder, not as the default option):
videoRequired: docapture → captureMode: manualAndAuto, uploaderMode: always (unless caller overrides).nfcVerificationSettings: {mode: disabled} unless caller overrides.These match what the dashboard's own Vue watcher writes when the user first toggles Live capture. Caveat on PATCH: requiredIdDocs.docSets is replaced wholesale (only top-level Level fields merge) — every docSet in the PATCH spec must carry the full intended state, since omitted sub-fields are wiped. GET the level first and copy values you want to preserve.
This skill's QUESTIONNAIRE and PROOF_OF_RESIDENCE doc-sets accept ids produced by sibling skills, so a 3-step build-everything-from-scratch flow is natural:
| First run … | Pass the returned id as … |
|---|---|
sumsub-create-questionnaire | docSets[].questionnaireDefId on a QUESTIONNAIRE doc-set |
sumsub-create-poa-preset | docSets[].poaPresetId on a PROOF_OF_RESIDENCE doc-set |
See examples/with-presets.json for an end-to-end level that references both.
On success, lead with the human-readable info:
name, applicantType, ordered list of docSets[].idDocSetType.https://cockpit.sumsub.com/checkus/sdkIntegrations/levels/<applicantType>Level/<id>?clientId=<clientId>&xSNSEnv=sbx. Render as a clickable markdown link. The <applicantType>Level segment is individualLevel for individuals (confirmed) or companyLevel for companies (assumed pattern). All three fields (id, applicantType, clientId) come from the POST response body; xSNSEnv=sbx targets the Sandbox workspace.Level ID (for SDK access tokens / future PATCH): <id>.On failure: HTTP status + the description/errorName from Sumsub's error envelope.
examples/identity-selfie.json — the most common pattern.examples/identity-live-capture.json — IDENTITY-only with Live capture + all docapture sub-options + NFC.examples/questionnaire-only.json — pure data-collection level (like the SOF level in this repo).examples/full-kyc.json — APPLICANT_DATA + IDENTITY + SELFIE + PROOF_OF_RESIDENCE + QUESTIONNAIRE.examples/kyb-company.json — KYB level with company + UBOs + representatives steps.examples/with-presets.json — references both a questionnaire (questionnaireDefId) and a POA preset (poaPresetId) returned by sibling skills.ApplicantLevel schema, all enum values, gotchas.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.