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/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. |
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>&sbx=true
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 sbx=true 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)
# 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: "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 # 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"
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>&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).
Worked examples
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=....
See also
1---2name: sumsub-create-transaction3description: 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.4---56# Sumsub — Create Transaction (KYT)78Builds a `KytTxnData` JSON payload from a compact spec, POSTs it to the Sumsub KYT endpoint, and reports the resulting `txnId` / `score` / `reviewAnswer`.910## Endpoint1112Two variants, picked automatically by the post script:1314| Case | Method + Path |15|---|---|16| Applicant exists | `POST https://api.sumsub.com/resources/applicants/{applicantId}/kyt/txns/-/data` |17| Applicant does **not** exist (Sumsub creates one from `applicant.externalUserId`) | `POST https://api.sumsub.com/resources/applicants/-/kyt/txns/-/data?levelName=<levelName>` |1819Body: [`KytTxnData`](references/transaction-schema.md). Returns the persisted `KytTxn` with monitoring scores attached.2021Both endpoints are marked deprecated, but they remain the canonical "submit transaction" entry points in the [official docs](https://docs.sumsub.com/reference/submit-transaction-for-existing-applicant.md). No v2/v3 replacement exists.2223## Auth — App Token + secret (sandbox only)2425This skill talks to the public Sumsub API and signs each request per26[the authentication reference](https://docs.sumsub.com/reference/authentication).27The full how-it-works writeup lives in the [`sumsub-api-auth`](../sumsub-api-auth/SKILL.md)28skill — read it if you hit `401 Invalid signature`.2930> **⚠️ Sandbox tokens only.** Do **not** accept or use a production App Token31> here — transaction monitoring acts on real applicant data and can fire32> real KYT alerts. If the user offers a prod token, refuse and ask them to33> generate a sandbox pair at <https://cockpit.sumsub.com/checkus/home?sbx=true>34> (**Connect Sumsub to your AI agent** -> **Build & configure** ->35> **Generate token**). Token + secret are shown once — copy both before36> closing the dialog. The helper37> script enforces this — it rejects tokens that don't start with `sbx:`.3839| Var | Example |40|---|---|41| `SUMSUB_APP_TOKEN` | `sbx:...` — sandbox App Token from the dashboard. |42| `SUMSUB_SECRET_KEY` | The paired secret shown once at token creation. |43| `SUMSUB_BASE` | Optional. Defaults to `https://api.sumsub.com`. |4445### Signing the resolved path4647This is the one routing wrinkle — the path **with query string** must be48signed. The post script handles it: it reads the sidecar route file the49builder emits, picks the URI (`/resources/applicants/{id}/…` or50`/resources/applicants/-/…?levelName=…`), URL-encodes the dynamic segments,51and signs the same bytes it sends. If you bypass the script, remember:5253- Sign the path you put on the wire — encoded form, query string included.54- Body bytes signed must equal the bytes sent (no whitespace re-flow).5556If the user has already supplied credentials in conversation, reuse them;57otherwise ask once before running. Never echo the secret back.5859## Procedure60611. **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*.622. **Validate**: `txnId` non-empty, `applicant.externalUserId` non-empty, and per `type`:63 - `finance` / `travelRule` → `info.amount`, `info.currencyCode`, `info.direction` required (the OpenAPI marks all three required on `KytTxnInfo`).64 - `userPlatformEvent` → `userPlatformEvent.type` required.65 - Enums (`direction`, `currencyType`, `applicant.type`, `nameType`, etc.) checked upfront with full allowed-values list on failure.663. **Generate** the full payload with `${CLAUDE_SKILL_DIR}/scripts/build_transaction.py` (compact spec on stdin → full `KytTxnData` payload on stdout).674. **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).685. **Build the dashboard link.** Read `id` (the **server-assigned identifier**, not the `txnId` you supplied) and `clientId` from the response body and format:6970 ```71 https://cockpit.sumsub.com/checkus/kyt/txns/<id>?clientId=<clientId>&sbx=true72 ```7374 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 `sbx=true` query param targets the **Sandbox** workspace — it is the canonical sandbox link param shared across all skills.756. **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**.7677## Compact spec format (JSON or YAML on stdin)7879```yaml80# Top-level81txnId: "finance-2026-05-21-0001" # REQUIRED, unique alphanumeric in your system82txnDate: "2026-05-21 14:30:00+0000" # optional; format: yyyy-MM-dd HH:mm:ss+XXXX83zoneId: "UTC+01:00" # optional, time zone string84type: finance # finance | travelRule | kyc | userPlatformEvent | iGamingSession (default: finance)8586# Routing — pick ONE87_applicantId: "67abc..." # post to existing applicant (path id, NOT a payload field)88# - OR -89_levelName: "Default" # post to non-existing-applicant URL with ?levelName=...90 # (Sumsub creates the applicant from applicant.externalUserId)9192# Finance / Travel Rule info (required for those types)93amount: 1500.5094currency: USD # ISO-4217 fiat or crypto ticker (BTC, ETH, …)95currencyType: fiat # fiat | crypto (default: fiat)96direction: out # in | out97amountInDefaultCurrency: 1500.50 # optional, converted to client default98defaultCurrencyCode: USD99paymentDetails: "Invoice #INV-2026-001"100mcc: 5411 # optional, 4-digit Merchant Category Code101102# Crypto-only block (used when currencyType=crypto)103crypto:104 chain: ETH # ETH | BTC | TRX | … (mandatory for tokens; empty for native)105 contract: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"106 paymentTxnId: "0x1234abcd..." # on-chain hash107 fingerprint: "0x1234abcd..." # alternative blockchain identifier108 attemptId: "attempt-01" # optional, when retrying109 outputIndex: 0 # for UTXO chains110111# Applicant (REQUIRED) — the person/entity acting on your platform112applicant:113 externalUserId: "user-001" # REQUIRED — your stable user id114 type: individual # individual | company (default: individual)115 fullName: "John Smith" # required for company; recommended for non-existing-applicant flow116 dob: "1990-05-01"117 email: "john@example.com"118 phone: "+1234567890"119 placeOfBirth: "London"120 address:121 country: USA122 town: "New York"123 street: "5th Avenue"124 formatted: "5th Avenue, New York, USA"125 paymentMethod:126 type: bankCard # bankCard | bankAccount | crypto | cryptoWallet |127 # eWallet | unhostedWallet | card | other128 accountId: "4111********1111" # IBAN, last4-hash, wallet address, etc.129 issuingCountry: USA130 "3dsUsed": true131 "2faUsed": false132 memo: "optional memo"133 idDoc:134 number: "A12345678"135 country: USA136 idDocType: PASSPORT137 device:138 fingerprint: "abc123"139 userAgent: "Mozilla/5.0 ..."140 ipInfo: { ip: "1.2.3.4" }141142# Counterparty (typical for finance + travelRule; same shape as applicant + a few extras)143counterparty:144 externalUserId: "merchant-XYZ"145 type: company146 fullName: "Acme Inc."147 registrationNumber: "12345678"148 leiCode: "529900XXXX0000XXXX00"149 residenceCountry: QAT150 address: { country: QAT }151 institution: # → institutionInfo152 code: "ACME-CODE"153 name: "Acme Bank"154 internalId: "<VASP id from directory>"155 ceo: # only for company counterparties (Travel Rule)156 firstName: "Jane"157 lastName: "Roe"158159# user-platform-event only (when type=userPlatformEvent)160userPlatformEvent:161 type: login # login | failedLogin | signup | passwordReset | twoFaReset | general162 twoFaUsed: true163 passwordHash: "..."164165# Optional escape hatches166sourceKey: "<segregation key>"167props: # custom string-string map168 customField: "value"169 dailyOutLimit: "10000"170```171172### Enums (validated upfront)173174| Field | Allowed values |175|---|---|176| `type` (top-level) | `finance`, `travelRule`, `kyc`, `auditTrailEvent`, `userPlatformEvent`, `scheduledEvent`, `iGamingSession` |177| `direction` | `in`, `out` |178| `currencyType` | `crypto`, `fiat` |179| `applicant.type` / `counterparty.type` | `individual`, `company` |180| `applicant.nameType` | `aliasName`, `birthName`, `maidenName`, `legalName`, `shortName`, `tradingName`, `other` |181| `userPlatformEvent.type` | `login`, `failedLogin`, `signup`, `passwordReset`, `twoFaReset`, `general` |182183`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.184185## Outputs186187On success, report all of:188- `txnId` (yours) **and** `id` (server-assigned, used in the dashboard link).189- Applicant `externalUserId`.190- `direction amount currency`, counterparty (if any).191- The response's `score` / `reviewAnswer` / `riskLabels` if returned.192- **Dashboard link**: `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.193194On failure: HTTP status + Sumsub's `description`/`errorName`. Most likely 4xx cases:195- `409 Entity already exists` — `txnId` collision (use a fresh id or the bulk-import method to update).196- `400` — required field missing (typical: `info.amount`, `info.currencyCode`, `info.direction`, or `applicant.externalUserId`).197- `400` — `levelName` unknown (when using non-existing-applicant flow).198199## Worked examples200201- [`examples/fiat-out.json`](examples/fiat-out.json) — outbound EUR card payment to a foreign counterparty (the docs' canonical example).202- [`examples/crypto-in.json`](examples/crypto-in.json) — inbound ETH deposit with `cryptoParams.cryptoChain=ETH`, contract address, on-chain `paymentTxnId`.203- [`examples/travel-rule.json`](examples/travel-rule.json) — `type: travelRule` outbound crypto with `institution.internalId` (VASP id) on the counterparty.204- [`examples/login-event.json`](examples/login-event.json) — `type: userPlatformEvent` for a successful login with 2FA.205- [`examples/new-applicant.json`](examples/new-applicant.json) — non-existing-applicant flow: `_levelName` is set so the POST goes to `/-/kyt/txns/-/data` with `?levelName=...`.206207## See also208209- [references/transaction-schema.md](references/transaction-schema.md) — full `KytTxnData` schema, all sub-objects, all enums, the existing-vs-new-applicant routing, scoring response fields, common gotchas.210- [Sumsub docs — Submit transaction](https://docs.sumsub.com/reference/submit-transaction-for-existing-applicant.md)211- [Sumsub docs — Submit transaction for non-existing applicant](https://docs.sumsub.com/reference/submit-transaction-for-non-existing-applicant.md)212- [Sumsub docs — Submit transactions and review results](https://docs.sumsub.com/docs/submit-transactions-and-review-results.md)