Transactions & monitoring
Fiat transaction screening, Web3 transaction screening and continuous customer monitoring.
Transaction Screening
Submit transaction data for AML risk assessment. Use
eventType: "TRANSACTION_EVENT".
TRANSACTION_EVENT is for
fiat transaction screening (bank transfers, payments
and other off-chain transactions). To screen a blockchain wallet
address or an on-chain transaction, use
WEB3_TRANSACTION_EVENT instead — see
Web3 Transaction Screening.
Endpoint:
workflow.apply.submit
POST
Request
{
"eventType": "TRANSACTION_EVENT",
"source": "API",
"data": {
"clientTransactionId": "TXN-20260416-001",
"transactionType": "WITHDRAW",
"amount": 199999.98,
"transactionCurrencyId": "SGD",
"altCurrencyId": "SGD",
"amountInAltCurrency": 199999.99,
"customerId": "C_abc123",
"customerType": 1,
"cpAId": "SENDER_001",
"cpABank": "Standard Chartered",
"cpACountry": "SINGAPORE",
"cpBId": null,
"cpBBank": null,
"cpBCountry": null,
"transactionTimestamp": "2026-04-16T10:30:00Z"
}
}
Non-blocking Response (Async)
Returns immediately with executionId. Poll
workflow.execution.status for the final result — read
caseStatus / riskScore as described in
Workflow Lifecycle API → Status & Case Decision.
{
"apiCode": 200,
"data": {
"executionId": "exec-7a3f1b2c4d5e6f78",
"requestId": "req-1776243366618",
"status": "RUNNING"
}
}
Blocking Response (Sync)
Waits for workflow completion and returns the decision inline:
{
"apiCode": 200,
"data": {
"executionId": "exec-7a3f1b2c4d5e6f78",
"status": "COMPLETED",
"durationMs": 12,
"output": {
"action": "approve",
"code": 100000
}
}
}
| Field | Description |
|---|---|
status |
COMPLETED = decision made, TIMEOUT =
exceeded time limit, FAILED = error
|
durationMs |
Processing time in milliseconds |
output.action |
approve / reject / review
|
output.code |
Decision code (defaults: approve=100000, reject=100010; configurable) |
Web3 Transaction Screening
Submit an on-chain (Web3) transaction event and run blockchain KYT (Know
Your Transaction) screening keyed on a
wallet address (walletAddress) or a transaction hash
(txHash)
— provide at least one, or both. The response returns the
screening result and a decision. Use
eventType: "WEB3_TRANSACTION_EVENT".
Endpoint:
workflow.apply.submit
POST
api / apiVersion /
apiKey / sign) plus the
x-domain header, and the same HMAC-SHA256 request-body
signing as every other OpenAPI call. See
OpenAPI Reference → Getting Started
for the base URL and request format, and
Authentication for the full signing rules.
workflow.apply.submit supports apiVersion 2.0.0 with an optional workflowId naming a published workflow. Leave workflowId empty for automatic eventType + source routing. For a tenant with multiple published Web3 workflows, confirm the intended published workflow before integration.
workflow.execution.status and
read the Case decision (caseStatus /
riskScore) as described in
Workflow Lifecycle API → Status & Case Decision.
Request
Variant 1 — walletAddress only
{
"eventType": "WEB3_TRANSACTION_EVENT",
"source": "API",
"data": {
"chain": "TRON",
"token": "USDT",
"walletAddress": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
"customerId": "demo-customer-001",
"clientTransactionId": "tx-demo-001",
"amount": 1000,
"transactionCurrencyId": "USDT"
}
}
Variant 2 — txHash only
{
"eventType": "WEB3_TRANSACTION_EVENT",
"source": "API",
"data": {
"chain": "TRON",
"token": "USDT",
"txHash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
"customerId": "demo-customer-001",
"clientTransactionId": "tx-demo-001",
"amount": 1000,
"transactionCurrencyId": "USDT"
}
}
Variant 3 — both walletAddress and
txHash
{
"eventType": "WEB3_TRANSACTION_EVENT",
"source": "API",
"data": {
"chain": "TRON",
"token": "USDT",
"walletAddress": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
"txHash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
"customerId": "demo-customer-001",
"clientTransactionId": "tx-demo-001",
"amount": 1000,
"transactionCurrencyId": "USDT"
}
}
Top-level Fields
| Field | Type | Required | Description |
|---|---|---|---|
eventType |
String | Yes |
Fixed value: WEB3_TRANSACTION_EVENT
|
source |
String | Yes |
Fixed value: API
|
data |
Object | Yes | Transaction data (see below) |
| workflowId | String | No | apiVersion 2.0.0 only; published workflow ID. Empty means automatic routing. |
data Fields
| Field | Type | Required | Description |
|---|---|---|---|
chain |
String | Yes |
Chain identifier, e.g. TRON, ETH,
BTC, BSC, POLYGON
|
token |
String | Yes |
Token symbol, e.g. USDC, USDT,
ETH, BTC
|
walletAddress |
String | At least one | Counterparty wallet address |
txHash |
String | At least one | On-chain transaction hash |
clientTransactionId |
String | No | The ID that identifies this transaction in your system, for audit trails |
customerId |
String | Pending confirmation | Stable customer identifier. The Web3 guide marks this optional; the general submit contract requires it. Include it until engineering confirms the Web3 requirement. |
amount |
Number | No |
Transaction amount (denominated in
transactionCurrencyId)
|
transactionCurrencyId |
String | No |
Currency code (e.g. USDC, USDT,
USD)
|
-
Provide at least one of
walletAddressandtxHash; both may be provided together. -
When both are provided, they must belong to the same chain and
match
chain. -
The length of
kytScreenResultsin the response follows from this: one entry when a single value is provided, two entries when both are provided.
Response
The endpoint returns synchronously. Only
HTTP 200 with apiCode: 200 means the
request was accepted; an HTTP 4xx/5xx means the gateway did not accept
the request and the body carries no data. Once accepted,
the business result is distinguished by data.output.code:
| code | action | Meaning | Client handling |
|---|---|---|---|
100000 |
approve |
No risk hit | Pass as normal |
100001 |
reject |
Risk hit | Follow your hit-handling flow (block, log a risk event, etc.) |
100002 |
reject |
KYT screening did not complete; reason in
data.output.data.message
|
Does not indicate a risk hit |
Two core fields in the response:
-
data.output.action— decision:approve/reject -
data.output.data.kytScreenResults— array of screening records: one entry when onlywalletAddressor onlytxHashis provided; two entries when both are provided (tell them apart by each record'saddressfield); an empty array[]whencode = 100002, with the reason indata.output.data.message.
Approve (100000)
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"executionId": "exec-7208f03b931e4897",
"requestId": "req-1780120490039",
"sync": true,
"status": "COMPLETED",
"output": {
"code": 100000,
"action": "approve",
"data": {
"kytScreenResults": [
{
"address": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
"chain_name": "TRON",
"risk_level": "low",
"risk_score": 0,
"is_detected": false
}
]
}
}
}
}
Reject — risk hit (100001)
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"executionId": "exec-8e7624be1d8d439a",
"requestId": "req-1780120982607",
"sync": true,
"status": "COMPLETED",
"output": {
"code": 100001,
"action": "reject",
"data": {
"kytScreenResults": [
{
"address": "TJJLiCdAEN3Ve4aHdgDeUgPnccJki3qCR7",
"chain_name": "TRON",
"risk_level": "critical",
"risk_score": 90,
"is_detected": true
}
]
}
}
}
}
KYT screening not completed (100002)
kytScreenResults is an empty array and
data.output.data.message gives the reason (only present in
this case).
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"executionId": "exec-417cec75a77046d6",
"requestId": "req-1780138193703",
"sync": true,
"status": "COMPLETED",
"output": {
"code": 100002,
"action": "reject",
"data": {
"message": "Wallet address does not match the specified chain",
"kytScreenResults": []
}
}
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
apiCode |
Number |
Gateway-layer return code; 200 means the request was
accepted. Other values: see Error Codes in the OpenAPI
Reference.
|
apiMsg |
String | Gateway-layer message |
data.executionId |
String | Unique ID of the workflow execution instance |
data.requestId |
String | Trace ID for this single request |
data.sync |
Boolean |
Whether the response is synchronous; always true for
this endpoint
|
data.status |
String |
Workflow execution status; COMPLETED on normal
completion
|
data.output.action |
String |
Decision: approve / reject (note:
reject does not necessarily mean a risk hit —
see the business-code table above)
|
data.output.code |
Number | Business code; see the table above (100000 / 100001 / 100002) |
data.output.data.message |
String |
Returned only when code = 100002; explains why KYT
screening did not complete
|
data.output.data.kytScreenResults |
Array |
KYT on-chain screening results; length rule as above (empty array
when code = 100002)
|
kytScreenResults[] fields
| Field | Type | Description |
|---|---|---|
address |
String |
The wallet address that was screened (when both
walletAddress + txHash are provided,
each record's address differs — use this field to tell them
apart)
|
chain_name |
String |
Chain identifier, matching data.chain in the
request
|
risk_level |
String |
Risk level, e.g. low, critical
|
risk_score |
Number | Risk score |
is_detected |
Boolean | Whether this address hit a risk |
Code Examples
All examples follow the same principle:
serialize the request body to a string once, compute the signature on
that string, then send the very same string as the HTTP body. The example body corresponds to Variant 1 (walletAddress
only); to call with Variant 2 or 3, just swap the
walletAddress / txHash fields — the rest
of the code is unchanged.
Java:
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Web3TransactionDemo {
private static final String GATEWAY = "https://api-gateway-platform.width.com/gateway";
private static final String API_KEY = System.getenv("WIDTH_API_KEY");
private static final String API_SECRET = System.getenv("WIDTH_API_SECRET");
private static final String DOMAIN_ID = System.getenv("WIDTH_DOMAIN_ID");
public static void main(String[] args) throws Exception {
String body = """
{"eventType":"WEB3_TRANSACTION_EVENT","source":"API","data":{\
"chain":"TRON",\
"token":"USDT",\
"walletAddress":"TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",\
"customerId":"demo-customer-001",\
"clientTransactionId":"tx-demo-001",\
"amount":1000,\
"transactionCurrencyId":"USDT"}}""";
String sign = sign(body, API_SECRET);
String url = GATEWAY + "?api=workflow.apply.submit"
+ "&apiVersion=1.0.0"
+ "&apiKey=" + URLEncoder.encode(API_KEY, StandardCharsets.UTF_8)
+ "&sign=" + sign;
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.header("x-domain", DOMAIN_ID)
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println("HTTP " + response.statusCode());
System.out.println(response.body());
}
private static String sign(String body, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
}
}
Python:
import hmac
import hashlib
import json
import os
import requests
GATEWAY = "https://api-gateway-platform.width.com/gateway"
API_KEY = os.environ["WIDTH_API_KEY"]
API_SECRET = os.environ["WIDTH_API_SECRET"]
DOMAIN_ID = os.environ["WIDTH_DOMAIN_ID"]
payload = {
"eventType": "WEB3_TRANSACTION_EVENT",
"source": "API",
"data": {
"chain": "TRON",
"token": "USDT",
"walletAddress": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
"customerId": "demo-customer-001",
"clientTransactionId": "tx-demo-001",
"amount": 1000,
"transactionCurrencyId": "USDT",
},
}
# Serialize once. Sign and send use the same bytes.
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
sign = hmac.new(
API_SECRET.encode("utf-8"),
body.encode("utf-8"),
hashlib.sha256,
).hexdigest()
response = requests.post(
GATEWAY,
params={
"api": "workflow.apply.submit",
"apiVersion": "1.0.0",
"apiKey": API_KEY,
"sign": sign,
},
headers={
"Content-Type": "application/json",
"x-domain": DOMAIN_ID,
},
data=body.encode("utf-8"), # same bytes used when signing
)
print(response.status_code, response.text)
Node.js:
const crypto = require('crypto');
const GATEWAY = 'https://api-gateway-platform.width.com/gateway';
const API_KEY = process.env.WIDTH_API_KEY;
const API_SECRET = process.env.WIDTH_API_SECRET;
const DOMAIN_ID = process.env.WIDTH_DOMAIN_ID;
const payload = {
eventType: 'WEB3_TRANSACTION_EVENT',
source: 'API',
data: {
chain: 'TRON',
token: 'USDT',
walletAddress: 'TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw',
customerId: 'demo-customer-001',
clientTransactionId: 'tx-demo-001',
amount: 1000,
transactionCurrencyId: 'USDT',
},
};
// Serialize once. Sign and send use the same bytes.
const body = JSON.stringify(payload);
const sign = crypto
.createHmac('sha256', API_SECRET)
.update(body, 'utf8')
.digest('hex');
const url = new URL(GATEWAY);
url.searchParams.set('api', 'workflow.apply.submit');
url.searchParams.set('apiVersion', '1.0.0');
url.searchParams.set('apiKey', API_KEY);
url.searchParams.set('sign', sign);
(async () => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-domain': DOMAIN_ID,
},
body,
});
console.log(response.status, await response.text());
})();
Error Codes
Gateway errors use apiCode: 999001 means a signature error; 999302 means an invalid API name/version; 999305 means an invalid or disabled API key; 999401 means service timeout. Correct credentials or signing before retrying, and query recovery after a potentially accepted submit. Business outcomes use data.output.code: 100000 (no risk hit), 100001 (risk hit), and 100002 (KYT screening incomplete). A 100002 outcome must not be treated as a completed clean screening.
apiCode, the 9999xx series)
are listed under
OpenAPI Reference → Error Codes and apply to
every endpoint. Business-layer result codes
(data.output.code, the 10000x series) are specific to
this endpoint — see the table at the top of
Response above.
FAQ
Q: My signature keeps failing — what should I check first?
A: 99% of SIGN_ERROR cases come from a mismatch between the
body bytes used to sign and the body bytes actually sent. Make sure you:
build the JSON string → compute the signature on it → send the
same string as the HTTP body. Between signing and
sending, do not pretty-print, do not serialize again, do not append a
trailing newline.
Q: Must the request body be minified (whitespace stripped)?
A: No. The body may keep any newlines/indentation, as long as the bytes you sign and the bytes you send are exactly the same.
Q: Can I screen an address before the transaction is broadcast on-chain (pre-transaction screening)?
A: Yes. Use Variant 1 — provide walletAddress only,
without txHash. Screening is keyed on the address itself,
so no on-chain transaction is required: screen the counterparty address
first, then decide whether to proceed with the transfer.
Continuous Monitoring
Asynchronous review for existing customers. Always non-blocking. Use
eventType: "PERIODIC_FLOW_EVENT".
Endpoint:
workflow.apply.submit
POST
Request
{
"eventType": "PERIODIC_FLOW_EVENT",
"source": "API",
"data": {
"customerId": "C_abc123",
"customerType": "INDIVIDUAL",
"name": "John Smith",
"email": "john@example.com",
"dateOfBirth": "1990-01-15",
"countryOfNationality": "SG",
"idType": "NATIONAL_ID",
"idNumber": "S9012345A",
"riskLevel": "LOW",
"startTime": 1776243366000,
"lastStartTime": 1776243366000
}
}Response
Returns immediately with executionId. Poll
workflow.execution.status until terminal state; read the
Case decision (caseStatus / riskScore) as
described in
Workflow Lifecycle API → Status & Case Decision.
{
"apiCode": 200,
"data": {
"executionId": "exec-7a3f1b2c4d5e6f78",
"requestId": "req-1776243366618",
"status": "RUNNING"
}
}
Full data field reference
| Field | Type | Description |
|---|---|---|
customerId |
String | Your unique customer identifier |
customerType |
String | INDIVIDUAL / CORPORATE |
name |
String | Full name or company name |
phone |
String | E.164 format |
email |
String | Customer email |
salutation |
String | Mr / Ms / Mrs |
aliasName |
String | Known alias or trading name |
formerRegisteredName |
String | Previous registered name |
sex |
String | MALE / FEMALE |
dateOfBirth |
String | YYYY-MM-DD |
countryOfNationality |
String | ISO 2-letter |
countryOfResidence |
String | ISO 2-letter |
countryOfBirth |
String | ISO 2-letter |
address |
String | Full address |
idType |
String |
NATIONAL_ID / INTERNATIONAL_PASSPORT /
DRIVING_LICENCE
|
idNumber |
String | Document number |
idIssuedDate |
String | YYYY-MM-DD |
idExpiryDate |
String | YYYY-MM-DD |
companyName |
String | Corporate only |
entityType |
String |
From config avira_entity_types
|
incorporationNumber |
String | Corporate only |
incorporationDate |
String |
YYYY-MM-DD, corporate only
|
incorporationExpiry |
String |
YYYY-MM-DD, corporate only
|
countryOfIncorporation |
String | ISO 2-letter, corporate only |
countryOfOperations |
String | ISO 2-letter, corporate only |
imoNumber |
String | IMO number for shipping entities |
isIncorporated |
Boolean | Whether entity is incorporated |
isRegulatedEntity |
Boolean | Whether entity is regulated |
ownershipStructureLayer |
String |
From config ownership_structure_layer
|
industry |
String |
From config avira_ssic
|
occupation |
String |
From config avira_ssoc
|
website |
String | Company website URL |
onboardingMode |
String | ONLINE / OFFLINE |
paymentMode |
String | BANK_TRANSFER / CASH |
productComplexity |
String |
From config product_service_complexity
|
bankAccounts |
String | Associated bank account info |
transactionBehaviour |
String |
From config transaction_behaviour
|
riskLevel |
String | LOW / MEDIUM / HIGH |
startTime |
Long | Epoch ms — current review start |
customerCategory |
String | RETAIL / CORPORATE |
lastStartTime |
Long | Epoch ms — previous review start |