交易篩查與持續監控
法幣交易篩查、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 毫秒 — 上次審查開始時間 |