sumsub-create-transaction — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sumsub-create-transaction (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 a KytTxnData JSON payload from a compact spec, POSTs it to the Sumsub KYT endpoint, and reports the resulting txnId / score / reviewAnswer.
Two variants, picked automatically by the post script:
| Case | Method + Path |
|---|---|
| Applicant exists | POST https://api.sumsub.com/resources/applicants/{applicantId}/kyt/txns/-/data |
Applicant does not exist (Sumsub creates one from applicant.externalUserId) | POST https://api.sumsub.com/resources/applicants/-/kyt/txns/-/data?levelName=<levelName> |
Body: KytTxnData. Returns the persisted KytTxn with monitoring scores attached.
Both endpoints are marked deprecated, but they remain the canonical "submit transaction" entry points in the official docs. No v2/v3 replacement exists.
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 — transaction monitoring acts on real applicant data and can fire real KYT alerts. If the user offers a prod token, refuse and ask them to generate a sandbox pair at <https://cockpit.sumsub.com/checkus/home?sbx=true> (Connect Sumsub to your AI agent -> Build & configure -> Generate token). 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. |
This is the one routing wrinkle — the path with query string must be signed. The post script handles it: it reads the sidecar route file the builder emits, picks the URI (/resources/applicants/{id}/… or /resources/applicants/-/…?levelName=…), URL-encodes the dynamic segments, and signs the same bytes it sends. If you bypass the script, remember:
If the user has already supplied credentials in conversation, reuse them; otherwise ask once before running. Never echo the secret back.
type: finance (a payment) — for those, the user is really telling you amount + currency + direction + applicant + counterparty.txnId non-empty, applicant.externalUserId non-empty, and per type:finance / travelRule → info.amount, info.currencyCode, info.direction required (the OpenAPI marks all three required on KytTxnInfo).userPlatformEvent → userPlatformEvent.type required.direction, currencyType, applicant.type, nameType, etc.) checked upfront with full allowed-values list on failure.${CLAUDE_SKILL_DIR}/scripts/build_transaction.py (compact spec on stdin → full KytTxnData payload on stdout).${CLAUDE_SKILL_DIR}/scripts/post_transaction.sh — auto-routes to existing-applicant vs non-existing-applicant URL based on whether _applicantId was set in the spec (NOT in the payload — see below).id (the server-assigned identifier, not the txnId you supplied) and clientId from the response body and format: https://cockpit.sumsub.com/checkus/kyt/txns/<id>?clientId=<clientId>&sbx=trueThe user-supplied txnId in the spec (e.g. finance-2026-05-21-0001) is not what goes in the URL — Sumsub assigns a separate identifier on persistence. The sbx=true query param targets the Sandbox workspace — it is the canonical sandbox link param shared across all skills.
txnId (yours), the server id, applicant externalUserId, direction + amount + currency, counterparty (if any), the response's score / reviewAnswer / riskLabels if present, and the dashboard link as a clickable markdown link.# Top-level
txnId: "finance-2026-05-21-0001" # REQUIRED, unique alphanumeric in your system
txnDate: "2026-05-21 14:30:00+0000" # optional; format: yyyy-MM-dd HH:mm:ss+XXXX
zoneId: "UTC+01:00" # optional, time zone string
type: finance # finance | travelRule | kyc | userPlatformEvent | iGamingSession (default: finance)
# Routing — pick ONE
_applicantId: "67abc..." # post to existing applicant (path id, NOT a payload field)
# - OR -
_levelName: "Default" # post to non-existing-applicant URL with ?levelName=...
# (Sumsub creates the applicant from applicant.externalUserId)
# Finance / Travel Rule info (required for those types)
amount: 1500.50
currency: USD # ISO-4217 fiat or crypto ticker (BTC, ETH, …)
currencyType: fiat # fiat | crypto (default: fiat)
direction: out # in | out
amountInDefaultCurrency: 1500.50 # optional, converted to client default
defaultCurrencyCode: USD
paymentDetails: "Invoice #INV-2026-001"
mcc: 5411 # optional, 4-digit Merchant Category Code
# Crypto-only block (used when currencyType=crypto)
crypto:
chain: ETH # ETH | BTC | TRX | … (mandatory for tokens; empty for native)
contract: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
paymentTxnId: "0x1234abcd..." # on-chain hash
fingerprint: "0x1234abcd..." # alternative blockchain identifier
attemptId: "attempt-01" # optional, when retrying
outputIndex: 0 # for UTXO chains
# Applicant (REQUIRED) — the person/entity acting on your platform
applicant:
externalUserId: "user-001" # REQUIRED — your stable user id
type: individual # individual | company (default: individual)
fullName: "John Smith" # required for company; recommended for non-existing-applicant flow
dob: "1990-05-01"
email: "[email protected]"
phone: "+1234567890"
placeOfBirth: "London"
address:
country: USA
town: "New York"
street: "5th Avenue"
formatted: "5th Avenue, New York, USA"
paymentMethod:
type: bankCard # bankCard | bankAccount | crypto | cryptoWallet |
# eWallet | unhostedWallet | card | other
accountId: "4111********1111" # IBAN, last4-hash, wallet address, etc.
issuingCountry: USA
"3dsUsed": true
"2faUsed": false
memo: "optional memo"
idDoc:
number: "A12345678"
country: USA
idDocType: PASSPORT
device:
fingerprint: "abc123"
userAgent: "Mozilla/5.0 ..."
ipInfo: { ip: "1.2.3.4" }
# Counterparty (typical for finance + travelRule; same shape as applicant + a few extras)
counterparty:
externalUserId: "merchant-XYZ"
type: company
fullName: "Acme Inc."
registrationNumber: "12345678"
leiCode: "529900XXXX0000XXXX00"
residenceCountry: QAT
address: { country: QAT }
institution: # → institutionInfo
code: "ACME-CODE"
name: "Acme Bank"
internalId: "<VASP id from directory>"
ceo: # only for company counterparties (Travel Rule)
firstName: "Jane"
lastName: "Roe"
# user-platform-event only (when type=userPlatformEvent)
userPlatformEvent:
type: login # login | failedLogin | signup | passwordReset | twoFaReset | general
twoFaUsed: true
passwordHash: "..."
# Optional escape hatches
sourceKey: "<segregation key>"
props: # custom string-string map
customField: "value"
dailyOutLimit: "10000"| Field | Allowed values |
|---|---|
type (top-level) | finance, travelRule, kyc, auditTrailEvent, userPlatformEvent, scheduledEvent, iGamingSession |
direction | in, out |
currencyType | crypto, fiat |
applicant.type / counterparty.type | individual, company |
applicant.nameType | aliasName, birthName, maidenName, legalName, shortName, tradingName, other |
userPlatformEvent.type | login, failedLogin, signup, passwordReset, twoFaReset, general |
paymentMethod.type is intentionally not enum-checked — the OpenAPI lists KytTxnPaymentMethodType (only smartContract, bankCard, bankAccount) but the docs and live data accept many more (crypto, eWallet, unhostedWallet, etc.). The builder forwards whatever the caller supplies.
On success, report all of:
txnId (yours) and id (server-assigned, used in the dashboard link).externalUserId.direction amount currency, counterparty (if any).score / reviewAnswer / riskLabels if returned.https://cockpit.sumsub.com/checkus/kyt/txns/<id>?clientId=<clientId>&sbx=true. Render as a clickable markdown link. <id> is the server-assigned identifier (not the txnId you sent); both it and clientId are in the POST response body; sbx=true targets the Sandbox workspace.On failure: HTTP status + Sumsub's description/errorName. Most likely 4xx cases:
409 Entity already exists — txnId collision (use a fresh id or the bulk-import method to update).400 — required field missing (typical: info.amount, info.currencyCode, info.direction, or applicant.externalUserId).400 — levelName unknown (when using non-existing-applicant flow).examples/fiat-out.json — outbound EUR card payment to a foreign counterparty (the docs' canonical example).examples/crypto-in.json — inbound ETH deposit with cryptoParams.cryptoChain=ETH, contract address, on-chain paymentTxnId.examples/travel-rule.json — type: travelRule outbound crypto with institution.internalId (VASP id) on the counterparty.examples/login-event.json — type: userPlatformEvent for a successful login with 2FA.examples/new-applicant.json — non-existing-applicant flow: _levelName is set so the POST goes to /-/kyt/txns/-/data with ?levelName=....KytTxnData schema, all sub-objects, all enums, the existing-vs-new-applicant routing, scoring response fields, common gotchas.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.