| name | sumsub-create-transaction |
| description | Submit a transaction to Sumsub Transaction Monitoring (KYT) via the ApplicantResource API. TRIGGER when the user asks to "submit / create / send / record a transaction", "test a KYT rule with a transaction", post a fiat or crypto payment for monitoring, attach Travel Rule data to a transfer, log a user-platform event (signup / login / password-reset / 2FA-reset) for monitoring, or submit a transaction for an applicant that does not exist yet (the request creates them). SKIP for editing arbitrary fields on an existing transaction, for bulk import (`/kyt/misc/txns/import`), for fetching / approving / rejecting transactions, for KYT rule management, or for Travel Rule data-exchange flows that are not transaction creation. |
| allowed-tools | Read, Write, Bash |
Sumsub — Create Transaction (KYT)
Builds a KytTxnData JSON payload from a compact spec, POSTs it to the Sumsub KYT endpoint, and reports the resulting txnId / score / reviewAnswer.
Endpoint
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.
Auth — App Token + secret (sandbox only)
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/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. |
Signing the resolved path
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:
- Sign the path you put on the wire — encoded form, query string included.
- Body bytes signed must equal the bytes sent (no whitespace re-flow).
If the user has already supplied credentials in conversation, reuse them;
otherwise ask once before running. Never echo the secret back.
Procedure
-
Map the user's intent to the compact spec below. The vast majority of transactions are type: finance (a payment) — for those, the user is really telling you amount + currency + direction + applicant + counterparty.
-
Validate: 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.
- Enums (
direction, currencyType, applicant.type, nameType, etc.) checked upfront with full allowed-values list on failure.
-
Generate the full payload with ${CLAUDE_SKILL_DIR}/scripts/build_transaction.py (compact spec on stdin → full KytTxnData payload on stdout).
-
POST via ${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).
-
Build the dashboard link. Read 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>&xSNSEnv=sbx
The 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 xSNSEnv=sbx query param targets the Sandbox workspace — it is the canonical sandbox link param shared across all skills.
-
Report: 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.
Compact spec format (JSON or YAML on stdin)
txnId: "finance-2026-05-21-0001"
txnDate: "2026-05-21 14:30:00+0000"
zoneId: "UTC+01:00"
type: finance
_applicantId: "67abc..."
_levelName: "Default"
amount: 1500.50
currency: USD
currencyType: fiat
direction: out
amountInDefaultCurrency: 1500.50
defaultCurrencyCode: USD
paymentDetails: "Invoice #INV-2026-001"
mcc: 5411
crypto:
chain: ETH
contract: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
paymentTxnId: "0x1234abcd..."
fingerprint: "0x1234abcd..."
attemptId: "attempt-01"
outputIndex: 0
applicant:
externalUserId: "user-001"
type: individual
fullName: "John Smith"
dob: "1990-05-01"
email: "john@example.com"
phone: "+1234567890"
placeOfBirth: "London"
address:
country: USA
town: "New York"
street: "5th Avenue"
formatted: "5th Avenue, New York, USA"
paymentMethod:
type: bankCard
accountId: "4111********1111"
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:
externalUserId: "merchant-XYZ"
type: company
fullName: "Acme Inc."
registrationNumber: "12345678"
leiCode: "529900XXXX0000XXXX00"
residenceCountry: QAT
address: { country: QAT }
institution:
code: "ACME-CODE"
name: "Acme Bank"
internalId: "<VASP id from directory>"
ceo:
firstName: "Jane"
lastName: "Roe"
userPlatformEvent:
type: login
twoFaUsed: true
passwordHash: "..."
sourceKey: "<segregation key>"
props:
customField: "value"
dailyOutLimit: "10000"
Enums (validated upfront)
| 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.
Outputs
On success, report all of:
txnId (yours) and id (server-assigned, used in the dashboard link).
- Applicant
externalUserId.
direction amount currency, counterparty (if any).
- The response's
score / reviewAnswer / riskLabels if returned.
- Dashboard link:
https://cockpit.sumsub.com/checkus/kyt/txns/<id>?clientId=<clientId>&xSNSEnv=sbx. 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; xSNSEnv=sbx 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).
Worked examples
See also