交易筛查与持续监控
法币交易筛查、Web3 交易筛查与持续客户监控。
交易筛查
提交交易数据进行 AML 风险评估。使用
eventType: "TRANSACTION_EVENT"。
TRANSACTION_EVENT
用于法币交易筛查(银行转账、支付等链下交易)。如需筛查区块链钱包地址或链上交易,请改用
WEB3_TRANSACTION_EVENT,见
Web3 交易筛查。
接口:
workflow.apply.submit
POST
请求
{
"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"
}
}
异步响应
立即返回 executionId。轮询
workflow.execution.status 获取最终结果 —
caseStatus / riskScore 的读法见
工作流生命周期 API → 查询状态与审核结论。
{
"apiCode": 200,
"data": {
"executionId": "exec-7a3f1b2c4d5e6f78",
"requestId": "req-1776243366618",
"status": "RUNNING"
}
}
同步响应
等待工作流完成后内联返回决策结果:
{
"apiCode": 200,
"data": {
"executionId": "exec-7a3f1b2c4d5e6f78",
"status": "COMPLETED",
"durationMs": 12,
"output": {
"action": "approve",
"code": 100000
}
}
}
| 字段 | 描述 |
|---|---|
status |
COMPLETED = 已决策, TIMEOUT = 超时,
FAILED = 错误
|
durationMs |
处理耗时(毫秒) |
output.action |
approve / reject / review
|
output.code |
决策码(默认: approve=100000, reject=100010; 可配置) |
Web3 交易筛查
提交一笔
Web3(链上)交易事件,基于钱包地址(walletAddress)或交易哈希(txHash)进行区块链
KYT(Know Your Transaction,交易/地址风险筛查)筛查 —
二者至少传一个,也可同时传,响应中返回筛查结果与决策。使用
eventType: "WEB3_TRANSACTION_EVENT"。
接口:
workflow.apply.submit
POST
api / apiVersion /
apiKey / sign)加 x-domain
请求头,以及同样的请求体 HMAC-SHA256 签名。基础地址与请求格式见
OpenAPI 参考 → 快速入门,完整签名规则见身份验证章节。
workflow.apply.submit 支持 apiVersion 2.0.0,可选传入 workflowId 以指定已发布工作流。workflowId 留空时按 eventType + source 自动路由。若租户存在多个已发布的 Web3 工作流,请在对接前确认要调用的工作流。
workflow.execution.status,Case 结论(caseStatus
/ riskScore)的读法见
工作流生命周期 API → 查询状态与审核结论。
请求
方式一 — 只传 walletAddress
{
"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"
}
}
方式二 — 只传 txHash
{
"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"
}
}
方式三 — 同时传 walletAddress 与
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"
}
}
顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
eventType |
String | 是 |
固定值:WEB3_TRANSACTION_EVENT
|
source |
String | 是 |
固定值:API
|
data |
Object | 是 | 交易数据(见下) |
| workflowId | String | 否 | 仅 apiVersion 2.0.0;已发布工作流 ID。留空则自动路由。 |
data 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
chain |
String | 是 |
链标识,如
TRON、ETH、BTC、BSC、POLYGON
|
token |
String | 是 |
代币符号,如
USDC、USDT、ETH、BTC
|
walletAddress |
String | 至少其一 | 对手方钱包地址 |
txHash |
String | 至少其一 | 链上交易哈希 |
clientTransactionId |
String | 否 | 您系统中标识本次交易的 ID,用于审计追踪 |
customerId |
String | 待确认 | 稳定的客户标识。Web3 指南标为可选,通用提交契约则要求必填。工程团队确认 Web3 要求前请提供此字段。 |
amount |
Number | 否 |
交易金额(按 transactionCurrencyId 计价)
|
transactionCurrencyId |
String | 否 |
货币代码(如
USDC、USDT、USD)
|
-
walletAddress与txHash至少填一个,也可以同时填写。 -
同时填写时,二者必须属于同一条链,且与
chain一致。 -
响应中
kytScreenResults数组长度由此决定:单填则为 1 条,同时填则为 2 条。
响应
接口同步返回。HTTP 200 + apiCode: 200
才表示请求被受理;HTTP 4xx/5xx 表示请求未被网关受理,响应体不会包含
data。请求被受理后,业务结果由
data.output.code 区分:
| code | action | 含义 | 客户处理要点 |
|---|---|---|---|
100000 |
approve |
未命中风险 | 正常放行 |
100001 |
reject |
命中风险 | 按命中流程处理(拉黑、记风险事件等) |
100002 |
reject |
KYT 筛查未完成,原因见
data.output.data.message
|
不代表命中风险 |
响应中两个核心字段:
-
data.output.action— 决策结果:approve/reject -
data.output.data.kytScreenResults— 筛查记录数组:仅传walletAddress或仅传txHash时为 1 条;同时传时为 2 条(通过每条记录的address字段区分);code = 100002时为空数组[],data.output.data.message给出未完成原因。
通过(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
}
]
}
}
}
}
命中(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 筛查未完成(100002)
kytScreenResults 为空数组,data.output.data.message
给出未完成原因(仅在此场景下出现)。
{
"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": []
}
}
}
}
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
apiCode |
Number |
网关层返回码,200 表示请求被受理;其他值见 OpenAPI
参考的错误码表。
|
apiMsg |
String | 网关层返回信息 |
data.executionId |
String | 工作流执行实例的唯一 ID |
data.requestId |
String | 单次请求的链路追踪 ID |
data.sync |
Boolean |
是否为同步响应,本接口固定 true
|
data.status |
String |
工作流执行状态,正常完成为 COMPLETED
|
data.output.action |
String |
决策结果:approve / reject(注意
reject 不一定代表命中,详见上文业务码对照表)
|
data.output.code |
Number | 业务码,见上文对照表(100000 / 100001 / 100002) |
data.output.data.message |
String |
仅当 code = 100002 时返回,说明 KYT
筛查未完成的原因
|
data.output.data.kytScreenResults |
Array |
KYT 链上筛查结果,长度规则见上文(code = 100002
时为空数组)
|
kytScreenResults[] 字段
| 字段 | 类型 | 说明 |
|---|---|---|
address |
String |
被筛查的钱包地址(同时传 walletAddress +
txHash 时,每条记录的地址不同,请以此字段区分)
|
chain_name |
String |
链标识,对应请求中的 data.chain
|
risk_level |
String |
风险等级,如 low、critical
|
risk_score |
Number | 风险评分 |
is_detected |
Boolean | 该地址是否命中风险 |
代码示例
所有示例都遵循同一原则:先把请求 body
序列化成字符串,再对这个字符串计算签名,然后把同一个字符串作为 HTTP
body 发出去。示例 body 对应方式一(只传
walletAddress);如需按方式二或方式三调用,只需替换 body
中的 walletAddress /
txHash 字段,其余代码逻辑不变。
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());
})();
错误码
网关错误使用 apiCode:999001 表示签名错误;999302 表示 API 名称或版本无效;999305 表示 API key 无效或停用;999401 表示服务超时。重试前先修正凭证或签名;若 submit 可能已被受理,先做恢复查询。业务结果使用 data.output.code:100000(未命中风险)、100001(命中风险)、100002(KYT 筛查未完成)。不得将 100002 视为已完成且无风险的筛查。
apiCode,9999xx 系列)在
OpenAPI 参考 → 错误码统一列出,适用于所有接口;业务层结果码(data.output.code,10000x
系列)是本接口专属,见上文 响应 顶部的对照表。
常见问题
Q:签名总是不通过,应该先查什么?
A:99% 的 SIGN_ERROR
都是因为「签名用的 body 字节」与「实际发送的 body
字节」不一致。请确保:构造 JSON 字符串 → 对它计算签名 →
把同一个字符串作为 HTTP body 发送。签名与发送之间不要
pretty-print、不要再调一次序列化、不要补尾部换行。
Q:请求 body 必须压缩(去空格)吗?
A:不需要。body 里可以保留任意换行/缩进,只要您签名时用的字节与发送时用的字节完全相同即可。
Q:交易还没上链,能先筛查这个地址有没有风险吗?
A:可以。使用方式一 — 只传 walletAddress、不传
txHash。筛查以地址本身为对象,不要求交易已经上链:可先筛查对手方地址,再决定是否发起转账。
持续监控
对存量客户的异步持续审查。始终为异步模式。使用
eventType: "PERIODIC_FLOW_EVENT"。
接口:
workflow.apply.submit
POST
请求
{
"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
}
}响应
立即返回 executionId。轮询
workflow.execution.status 直至终态;Case 结论(caseStatus
/ riskScore)的读法见
工作流生命周期 API → 查询状态与审核结论。
{
"apiCode": 200,
"data": {
"executionId": "exec-7a3f1b2c4d5e6f78",
"requestId": "req-1776243366618",
"status": "RUNNING"
}
}
完整字段参考
| 字段 | 类型 | 描述 |
|---|---|---|
customerId |
String | 客户唯一标识 |
customerType |
String | INDIVIDUAL / CORPORATE |
name |
String | 全名或公司名称 |
phone |
String | E.164 格式 |
email |
String | 客户邮箱 |
salutation |
String | Mr / Ms / Mrs |
aliasName |
String | 别名或商业名称 |
formerRegisteredName |
String | 曾用注册名称 |
sex |
String | MALE / FEMALE |
dateOfBirth |
String | YYYY-MM-DD |
countryOfNationality |
String | ISO 双字母 |
countryOfResidence |
String | ISO 双字母 |
countryOfBirth |
String | ISO 双字母 |
address |
String | 完整地址 |
idType |
String |
NATIONAL_ID / INTERNATIONAL_PASSPORT /
DRIVING_LICENCE
|
idNumber |
String | 证件号码 |
idIssuedDate |
String | YYYY-MM-DD |
idExpiryDate |
String | YYYY-MM-DD |
companyName |
String | 仅企业 |
entityType |
String |
取自配置 avira_entity_types
|
incorporationNumber |
String | 仅企业 |
incorporationDate |
String |
YYYY-MM-DD, 仅企业
|
incorporationExpiry |
String |
YYYY-MM-DD, 仅企业
|
countryOfIncorporation |
String | ISO 双字母, 仅企业 |
countryOfOperations |
String | ISO 双字母, 仅企业 |
imoNumber |
String | 航运实体的 IMO 编号 |
isIncorporated |
Boolean | 是否已注册 |
isRegulatedEntity |
Boolean | 是否受监管 |
ownershipStructureLayer |
String |
取自配置 ownership_structure_layer
|
industry |
String |
取自配置 avira_ssic
|
occupation |
String |
取自配置 avira_ssoc
|
website |
String | 公司网站 URL |
onboardingMode |
String | ONLINE / OFFLINE |
paymentMode |
String | BANK_TRANSFER / CASH |
productComplexity |
String |
取自配置 product_service_complexity
|
bankAccounts |
String | 关联银行账户信息 |
transactionBehaviour |
String |
取自配置 transaction_behaviour
|
riskLevel |
String | LOW / MEDIUM / HIGH |
startTime |
Long | Epoch 毫秒 — 本次审查开始时间 |
customerCategory |
String | RETAIL / CORPORATE |
lastStartTime |
Long | Epoch 毫秒 — 上次审查开始时间 |