集成指南
托管核驗鏈接、名單篩查、Quick Scan 與工作流 Webhook 的完整集成參考。
... 表示結構示意,不能直接作為 JSON 提交。Quick Scan 調用使用 apiVersion=1.0.0。核驗鏈接
生成一條一次性網頁鏈接,終端用戶點開就進入托管的 KYC 或 KYB 流程。鏈接怎麼送到用戶手裡由你決定——郵件、聊天工具、任何現有渠道都行,用戶在瀏覽器里把資料填完。如果你自己沒有網站或 App 可以嵌 SDK,就走這條路。
接口:
sdk.launch.link.generate
POST
使用 apiVersion 1.0.0。簽名方式、網關地址和
x-domain 請求頭與其他 OpenAPI 接口完全一致(見身份驗證)。
請求
{
"email": "enduser@example.com",
"customerId": "cust_internal_id",
"eventType": "KYC_EVENT"
}
| 字段 | 類型 | 必填 | 描述 |
|---|---|---|---|
email |
字符串 | 是 | 終端用戶郵箱 |
customerId |
字符串 | 是 | 你的內部用戶 ID(對同一客戶保持穩定) |
eventType |
字符串 | 是 |
鏈接打開哪一類流程:KYC_EVENT(個人)或
KYB_EVENT(企業)
|
響應
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"msg": "success",
"code": 200,
"data": {
"url": "https://id-link-platform.width.com/?linkToken=71a2743a-ca3c-436f-bc24-af3a73405a58",
"expiresAt": 1786679539986
}
}
}
| 字段 | 描述 |
|---|---|
data.data.url |
要交給終端用戶的鏈接。查詢參數
linkToken
標識這一次邀請——整條 URL 視同密鑰,只發給你在
email 里填的那個地址。
|
data.data.expiresAt |
失效時刻,毫秒時間戳。鏈接自生成起 7 天 內有效;過期後再調一次本接口換一條新的。 |
email
再調一次本接口,前一條立刻作廢——終端用戶手裡那條當場就打不開了,而且沒有任何辦法恢復。所以重新簽發之前先確認對方沒在填;如果他正填到一半,把新鏈接補發給他。用不同郵箱簽出來的鏈接互不影響。
錯誤處理
eventType 填了不支持的值時,錯誤信息在
data 里,網關層的 apiCode 仍然是
200 — 判斷成敗要讀 data.code,不是 HTTP
狀態碼:
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"msg": "Invalid eventType: TRANSACTION_EVENT",
"code": 400,
"data": {}
}
}
名單篩查
把一個待篩查主體(個人或企業)送進平台做名單篩查。平台會按客戶號建檔,在你的工作流里跑完篩查,命中就產生告警並開一個案件交給分析員裁定。使用
eventType: "MANUAL_CASE_EVENT"。
customerId
下次再來時直接復用。如果只想臨時查一下、不建客戶也不開案件,請改用Quick Scan 一次性篩查。
接口:
workflow.apply.submit
POST
POST 到網關,query string 照常帶
api=workflow.apply.submit、apiVersion=2.0.0、apiKey 和
sign,再加上裝著你的 domain ID 的
x-domain 請求頭。請求體是 UTF-8 編碼的 JSON,Content-Type: application/json;sign 是對 實際發出去的那份字節做
HMAC-SHA256
——先把請求體序列化成字節,對這份字節簽名,再把同一份字節發出去,別讓框架在簽完之後又重新序列化一遍。完整規則和各語言代碼見身份驗證。
請求
{
"eventType": "MANUAL_CASE_EVENT",
"source": "API",
"workflowId": "{workflowId}",
"data": {
// subject fields, see below
}
}
| 字段 | 類型 | 必填 | 描述 |
|---|---|---|---|
eventType |
字符串 | 是 |
固定填 MANUAL_CASE_EVENT。該事件在平台界面上顯示為
人工。
|
source |
字符串 | 是 |
服務端對接固定填 API。
|
workflowId |
字符串 | 本場景填 | 這次篩查跑哪條工作流。工作流 ID 在平台的工作流列表裡取。不傳也能跑——平台會自動路由到最新發佈的那條——但名單篩查這個場景請把工作流寫明,免得別人後來發佈了新版本,你的提交在你不知情的情況下換了流程。 |
data |
對象 | 是 |
被篩查的主體。字段直接放在
data 的第一層,不要再往下嵌。
|
主體字段
| 字段 | 必填 | 描述 |
|---|---|---|
customerId |
是 | 客戶號。平台按這個鍵建檔或關聯到已有檔案,人工裁定的結論也記在這個鍵上——同一客戶的多個案件共享已經裁定過的結果。請用對該客戶長期不變的號;跨案件記憶全靠它。 |
name |
是 | 主體名,整條作為一個字符串傳——不要拆成姓和名兩個字段。個人傳英文全名,企業傳公司註冊名。 |
aliasName |
建議 |
主體的其它名字,比如原文名(中文名等)。每個名字都會單獨跑一遍篩查,原文名對命中準確性幫助顯著。多個名字用英文逗號分隔——也就是說別名本身不能帶逗號:寫成
Chen, Dawei 會被當成兩個名字,請寫
Chen Dawei。這條只管 aliasName;name
不會被拆。
|
customerType |
是 |
個人填 individual,企業填
corporate。
|
sex |
建議(個人) |
性別。F / M 或英文全稱都可以。
|
dateOfBirth |
建議(個人) |
出生日期。完整日期(1966-03-15)或只給年份(1966)都可以。
|
countryOfNationality |
建議(個人) | 國籍。ISO 3166 兩位或三位碼均可,平台自動歸一。 |
countryOfResidence |
可選 | 居住地國家,企業則為主營業地國家。 |
occupation |
可選(個人) | 職業 / 職務。展示在案件頁上。 |
idType, idNumber |
可選(個人) | 主體的證件類型與號碼,須成對提供,且只傳真實身份證件(護照、身份證等),不要傳內部編號。生效時機:僅在該客戶首次提交、平台還沒有他的檔案時隨篩查一起送去做證件比對;檔案建起來之後以檔案上維護的證件為準,事件里再傳不生效。 |
countryOfIncorporation |
建議(企業) |
註冊國。未傳時以 countryOfResidence 兜底。
|
incorporationDate |
可選(企業) | 成立日期,完整日期或只給年份均可。會寫入客戶檔案並在案件頁展示;當前版本不送篩查引擎參與比對。 |
incorporationNumber |
可選(企業) | 公司註冊號。參與名單記錄的註冊號比對——對企業客戶這是權重最高的證據,所以同樣寧缺毋濫,只傳真實註冊號。 |
個人的 sex、dateOfBirth、countryOfNationality
和企業的 countryOfIncorporation、incorporationNumber
會作為佐證參與比對與降分排序:傳得越全,同名噪音越少。字段缺失時按中性處理,不會誤傷主體。
響應
本接口是異步的:提交被接受即返回,篩查在平台側繼續跑。
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"executionId": "exec-xxxxxxxxxxxxxxxx",
"requestId": "req-xxxxxxxxxxxxx",
"status": "RUNNING"
}
}
| 字段 | 描述 |
|---|---|
data.executionId |
本次執行的編號。請留存:向平台方問進度或排查問題時用它,用workflow.execution.status輪詢結果時也傳它。 |
data.status |
提交成功時為
RUNNING。篩查結果與風險評分最終體現在平台的案件頁上。
|
提交失敗時返回網關標準錯誤結構。常見原因是簽名與實際發出的字節不一致、缺
x-domain 請求頭、請求體不是合法 JSON、必填字段缺失。
完整示例
個人主體:
{
"eventType": "MANUAL_CASE_EVENT",
"source": "API",
"workflowId": "{workflowId}",
"data": {
"customerId": "CIF-0001234",
"name": "CHEN, DAWEI",
"aliasName": "陳大偉",
"customerType": "individual",
"sex": "M",
"dateOfBirth": "1968",
"countryOfNationality": "TW",
"countryOfResidence": "TW",
"occupation": "Director"
}
}
企業主體:
{
"eventType": "MANUAL_CASE_EVENT",
"source": "API",
"workflowId": "{workflowId}",
"data": {
"customerId": "CIF-0005678",
"name": "HUAYUAN INTERNATIONAL TRADING CO., LTD",
"aliasName": "華遠國際貿易有限公司",
"customerType": "corporate",
"countryOfIncorporation": "HK",
"incorporationDate": "2003",
"incorporationNumber": "0691355",
"countryOfResidence": "HK"
}
}
Quick Scan 一次性篩查
Quick Scan 提供一次性合規篩查能力,無需創建客戶或配置工作流。提交一個姓名,系統會同時調用多個篩查引擎(World-Check、Dow Jones、ArtemiScan、互聯網搜索、內部監控名單),返回命中結果,適合做臨時的制裁 / PEP(Politically Exposed Person,政治公眾人物)姓名核查。
api / apiVersion /
apiKey / sign)加 x-domain
請求頭,以及同樣的請求體 HMAC-SHA256 簽名。基礎地址與請求格式見
OpenAPI 參考 → 快速入門,完整簽名規則見身份驗證章節。
調用流程
-
quickscan.submit— 提交篩查,立即返回id(異步執行) -
quickscan.detail— 輪詢狀態,DONE時返回命中摘要 -
quickscan.hits— 按引擎分頁獲取完整命中詳情(可選)
接口列表
| API 名稱 | 說明 | 超時 |
|---|---|---|
quickscan.submit
POST
|
提交篩查 | 30 秒 |
quickscan.detail
POST
|
查詢狀態和結果 | 10 秒 |
quickscan.hits
POST
|
分頁獲取命中詳情 | 10 秒 |
quickscan.submit — 提交篩查
請求
{
"data": {
"name": "Vladimir Putin",
"customerType": "Individual",
"providers": ["WORLD_CHECK", "DOW_JONES", "ARTEMISCAN", "INTERNET_SEARCH"],
"description": "AML compliance check",
"providerSettings": {
"artemiscan": { "sensitivity": 0.6 },
"worldCheck": { "matchStrength": "STRONG", "maxHits": 20 },
"dowJones": { "searchType": "Precise", "maxHits": 20 },
"internetSearch": { "resultCount": 20 }
}
}
}
data 字段
| 字段 | 類型 | 必填 | 說明 |
|---|---|---|---|
name |
字符串 | 是 | 篩查對象姓名 |
customerType |
字符串 / 整數 | 是 |
"Individual" 或 "Business"(也兼容整數
1 / 2)
|
providers |
字符串數組 | 是 | 篩查引擎列表(至少一個) |
description |
字符串 | 否 | 備注說明 |
providerSettings |
對象 | 否 | 各引擎參數(省略則用默認值) |
可選引擎(providers)
| 引擎 | 說明 | 消耗 Credit |
|---|---|---|
WORLD_CHECK |
Refinitiv World-Check 制裁 / PEP 名單 | 是 |
DOW_JONES |
Dow Jones Risk & Compliance 名單 | 是 |
ARTEMISCAN |
ArtemiScan 綜合風險篩查 | 是 |
INTERNET_SEARCH |
互聯網公開信息搜索 | 是 |
WATCHLIST |
內部監控名單(租戶自定義) | 否(免費) |
引擎參數(providerSettings)
| 引擎 | 參數 | 默認值 | 可選值 |
|---|---|---|---|
artemiscan |
sensitivity |
0.6 |
0~1(越大匹配越寬松) |
worldCheck |
matchStrength |
STRONG |
EXACT / STRONG / WEAK |
worldCheck |
maxHits |
20 |
5 / 10 / 20 / 50 |
dowJones |
searchType |
Precise |
Precise / Near / Broad |
dowJones |
maxHits |
20 |
5 / 10 / 20 / 50 |
internetSearch |
resultCount |
20 |
10 / 20 / 50 |
響應
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"code": 200,
"id": "qs-b701733c3eca438fb27b68b0ed05ad9d",
"status": "RUNNING"
}
}
| 字段 | 說明 |
|---|---|
data.id |
篩查記錄 ID,用於後續查詢 |
data.status |
RUNNING(篩查進行中)
|
quickscan.detail — 查詢結果
請求
{
"data": {
"id": "qs-b701733c3eca438fb27b68b0ed05ad9d"
}
}
響應 — 進行中
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"code": 200,
"id": "qs-b701733c3eca438fb27b68b0ed05ad9d",
"status": "RUNNING"
}
}
響應 — 完成,命中數 ≤ 50(內聯返回)
當 totalHits ≤ 50 時,hits
字段內聯返回全部命中詳情:
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"code": 200,
"id": "qs-b701733c3eca438fb27b68b0ed05ad9d",
"status": "DONE",
"totalHits": 31,
"truncated": false,
"hitCounts": {
"WORLD_CHECK": 7,
"DOW_JONES": 3,
"ARTEMISCAN": 1,
"INTERNET_SEARCH": 20
},
"hits": {
"WORLD_CHECK": [ { ... }, { ... } ],
"DOW_JONES": [ { ... } ],
"ARTEMISCAN": [ { ... } ],
"INTERNET_SEARCH": [ { ... }, { ... } ]
}
}
}
響應 — 完成,命中數大於 50
當總命中數大於 50 時,hits 為 null、truncated
為 true,需使用 quickscan.hits 分頁獲取詳情:
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"code": 200,
"id": "qs-xxx",
"status": "DONE",
"totalHits": 128,
"truncated": true,
"hitCounts": {
"WORLD_CHECK": 45,
"DOW_JONES": 33,
"ARTEMISCAN": 30,
"INTERNET_SEARCH": 20
},
"hits": null
}
}
字段說明
| 字段 | 類型 | 說明 |
|---|---|---|
status |
字符串 |
RUNNING / DONE / FAILED
|
totalHits |
整數 | 全部引擎命中總數 |
truncated |
布爾值 |
true = 命中過多,需用
quickscan.hits 分頁獲取
|
hitCounts |
對象 |
各引擎命中數量 { "WORLD_CHECK": 7, ... }
|
hits |
對象 / null |
命中詳情(僅 totalHits ≤ 50 時返回)
|
quickscan.hits — 分頁獲取命中詳情
當 quickscan.detail 返回
truncated: true 時,使用此接口按引擎分頁獲取命中詳情。truncated: false
時也可用此接口按需獲取單個引擎的結果。
請求
{
"data": {
"id": "qs-b701733c3eca438fb27b68b0ed05ad9d",
"provider": "WORLD_CHECK",
"page": 1,
"pageSize": 20
}
}
| 字段 | 類型 | 必填 | 說明 |
|---|---|---|---|
id |
字符串 | 是 | 篩查記錄 ID |
provider |
字符串 | 是 |
引擎名稱(WORLD_CHECK / DOW_JONES /
ARTEMISCAN / INTERNET_SEARCH /
WATCHLIST)
|
page |
整數 | 否 |
頁碼(默認 1)
|
pageSize |
整數 | 否 |
每頁條數(默認 20,最大 100)
|
響應
{
"apiCode": 200,
"apiMsg": "OK",
"data": {
"code": 200,
"id": "qs-b701733c3eca438fb27b68b0ed05ad9d",
"totalHits": 7,
"truncated": true,
"hits": {
"WORLD_CHECK": [
{
"firstName": "Vladimir",
"lastName": "PUTIN",
"category": "PEP",
"subCategory": "PEP - Current",
"addresses": [ { "country": { "code": "RUS", "name": "Russian Federation" } } ],
"dobs": [ { "dateOfBirth": "1952-10-07" } ]
}
]
}
}
}
| 字段 | 說明 |
|---|---|
totalHits |
該引擎的總命中數 |
truncated |
是否還有更多頁(true = 還有下一頁)
|
hits |
當前頁命中詳情 { "PROVIDER": [...] }
|
代碼示例
端到端示例:提交、輪詢到 DONE,若
truncated 再分頁獲取。注意簽名規則 —
先把請求 body
序列化成字符串,再對這個字符串計算簽名,然後把同一個字符串作為 HTTP
body 發出去。
Python:
import hmac, hashlib, json, os, time, urllib.request, urllib.error
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"]
def sign(body: str) -> str:
return hmac.new(API_SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
def post(api: str, body: str) -> dict:
url = f"{GATEWAY}?api={api}&apiVersion=1.0.0&apiKey={API_KEY}&sign={sign(body)}"
req = urllib.request.Request(url, data=body.encode(), method='POST',
headers={'Content-Type': 'application/json', 'x-domain': DOMAIN_ID})
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read().decode())
if result.get("apiCode") != 200 or result.get("data", {}).get("code") != 200:
raise RuntimeError(f"WIDTH request failed: {result}")
return result
# 1. Submit a screening
submit_body = json.dumps({"data": {
"name": "Vladimir Putin",
"customerType": "Individual",
"providers": ["WORLD_CHECK", "DOW_JONES", "ARTEMISCAN"],
"providerSettings": {
"worldCheck": {"matchStrength": "STRONG", "maxHits": 20},
"dowJones": {"searchType": "Precise", "maxHits": 20},
"artemiscan": {"sensitivity": 0.6}
}
}}, separators=(',', ':'))
result = post("quickscan.submit", submit_body)
scan_id = result["data"]["id"]
print(f"Submitted: {scan_id}")
# 2. Poll for the result
while True:
detail_body = json.dumps({"data": {"id": scan_id}}, separators=(',', ':'))
detail = post("quickscan.detail", detail_body)
status = detail["data"]["status"]
print(f"Status: {status}")
if status in ("DONE", "FAILED"):
break
time.sleep(3)
if status == "FAILED":
raise RuntimeError(f"Screening failed: {scan_id}")
# 3. If truncated, paginate
if detail["data"].get("truncated"):
for provider, count in detail["data"]["hitCounts"].items():
page = 1
while True:
hits_body = json.dumps({"data": {
"id": scan_id, "provider": provider, "page": page, "pageSize": 20
}}, separators=(',', ':'))
hits = post("quickscan.hits", hits_body)
items = hits["data"]["hits"][provider]
print(f"{provider} page {page}: {len(items)} hits")
if not hits["data"]["truncated"]:
break
page += 1
else:
# Result already inlined
for provider, items in detail["data"]["hits"].items():
print(f"{provider}: {len(items)} hits")
cURL:
提交篩查
curl -X POST '{baseUrl}?api=quickscan.submit&apiVersion=1.0.0&apiKey={ak_xxx}&sign={sign}' \
-H 'Content-Type: application/json' \
-H 'x-domain: {domainId}' \
-d '{"data":{"name":"Vladimir Putin","customerType":1,"providers":["WORLD_CHECK","DOW_JONES"]}}'
查詢結果
curl -X POST '{baseUrl}?api=quickscan.detail&apiVersion=1.0.0&apiKey={ak_xxx}&sign={sign}' \
-H 'Content-Type: application/json' \
-H 'x-domain: {domainId}' \
-d '{"data":{"id":"qs-xxx"}}'
分頁獲取命中
curl -X POST '{baseUrl}?api=quickscan.hits&apiVersion=1.0.0&apiKey={ak_xxx}&sign={sign}' \
-H 'Content-Type: application/json' \
-H 'x-domain: {domainId}' \
-d '{"data":{"id":"qs-xxx","provider":"WORLD_CHECK","page":1,"pageSize":20}}'
錯誤碼
網關層錯誤碼(apiCode)
| apiCode | apiMsg | 原因 |
|---|---|---|
200 |
OK |
請求成功 |
999001 |
SIGN_ERROR |
簽名校驗失敗 |
999301 |
API_INVALID_REQUEST |
請求格式錯誤 |
999302 |
API_NOT_FOUND |
API 名稱錯誤 |
999304 |
RATE_LIMIT_TRY_AGAIN |
觸發限流 |
999305 |
TENANT_NOT_FOUND |
apiKey 對應租戶不存在
|
業務層錯誤碼(data.code)
| code | 含義 |
|---|---|
200 |
成功 |
400 |
參數錯誤(缺少必填字段、provider 無效等) |
403 |
Credit 額度不足 |
404 |
篩查記錄不存在 |
500 |
服務內部錯誤 |
常見問題
Q:提交後多久能拿到結果?
A:通常 3~10 秒。World-Check 和 Dow Jones 一般 2~5
秒返回,互聯網搜索可能稍慢。建議每 3 秒輪詢一次
quickscan.detail。
Q:truncated: true 是甚麼意思?
A:當總命中數超過 50 條時,quickscan.detail
不會在響應中內聯全部命中詳情(避免響應體過大)。此時
hits 為 null,需通過
quickscan.hits 按引擎分頁獲取。
Q:每次篩查消耗多少 Credit?
A:每個引擎消耗 1 個 Credit(WATCHLIST
免費)。例如選擇 3 個引擎 = 消耗 3 個 Credit。
Q:customerType 有甚麼影響?
A:"Individual" 和 "Business"
決定了篩查引擎的匹配策略。Business
類型會額外匹配公司名稱、註冊號等字段。也兼容整數
1(Individual)/ 2(Business)。
Q:簽名總是不通過?
A:確保簽名時用的 body 字節與實際發送的完全一致。一次性構造 JSON 字符串 → 簽名 → 發送同一個字符串。不要 pretty-print,不要再次序列化。
Q:可以同時提交多個篩查嗎?
A:可以。每次
quickscan.submit 調用都是獨立的,各自返回不同的
id。可並行提交,獨立輪詢結果。
Webhook 集成
Webhook 讓平台在工作流內部回調你自己的服務器 — 與 OpenAPI 的拉取模型相對,是「推送」方向。本節涵蓋配置、請求格式、簽名校驗、重試語義與參考實現。
概念
平台里的 Webhook 是一份可復用的 HTTP 配置,工作流節點可以引用它發起調用。兩條調用路徑共用同一份配置:
| 路徑 | 觸發時機 | 模式 | 是否重試 | 是否解析響應 |
|---|---|---|---|---|
| Webhook 動作節點 | 工作流執行到該節點時 | 同步(工作流等待) | 節點級可配 | 是 — 按 JsonPath 解析為輸出 |
| Decision Webhook 鈎子 |
Decision 節點決策落定為 approve /
reject 後的可選回調
|
異步 Fire-and-forget | 否 | 否(失敗僅審計) |
兩條路徑都讀同一份
HttpServiceDefinition(URL、method、headers、簽名等),一次配置兩處可用。
在平台配置 Webhook(UI)
路徑:侧栏 → Tools → Webhook → + New Webhook
| 字段 | 是否必填 | 說明 |
|---|---|---|
| Webhook 名稱 | 是 | 展示名,節點編輯器里通過此名引用 |
| HTTP 方法 | 是 |
POST(默認)/ PUT /
PATCH / GET / DELETE。GET
和 DELETE 不發送 body
|
| Webhook URL | 是 |
絕對 URL,支持 {var}
佔位符替換,運行時由 input map 提供,例如
https://api.example.com/users/{userId}
|
| 請求頭 | 可選 |
{name, defaultValue} 列表;value 同樣支持
{var} 佔位符
|
| 輸入 | 可選 | 聲明的輸入字段,作為 JSON 請求體的頂層 key |
| 輸出 | 可選 |
{paramName, jsonPath, paramType, defaultValue}
列表,從響應體提取後作為節點輸出(僅同步路徑生效)
|
| 簽名 | 可選 | 見下文「簽名校驗」 |
保存後會得到一個 Webhook ID,工作流節點編輯器通過此 ID 引用 Webhook。
請求格式
請求頭
-
Webhook 上配置的 headers 會原樣發送(先做
{var}替換)。 - 啓用簽名時,額外注入兩個簽名頭(見「簽名校驗」)。
-
對帶 body 的方法(POST/PUT/PATCH),
Content-Type默認application/json; charset=utf-8;如需覆蓋,在 Webhook 的 Headers 區添加Content-Type項即可。
請求體(僅 POST/PUT/PATCH)
inputs map 直接序列化成 JSON 作為 body — 聲明的輸入字段成為頂層 key:
POST /receive HTTP/1.1
Content-Type: application/json; charset=utf-8
{
"customerId": "cust-001",
"action": "approve",
"score": 86
}
GET 和 DELETE 不發送 body。
佔位符替換
Webhook URL 和 header value 支持
{var}(單大括號)替換。替換源是該次調用解析出的 inputs
map。
簽名校驗
平台對外發請求時使用 HMAC 簽名,便於接收方驗證身份。
配置項(每個 Webhook 獨立)
| 字段 | 取值 |
|---|---|
type |
NONE(默認)/ HMAC_SHA256
|
payloadFormat |
TIMESTAMP_BODY(默認)/ BODY
|
secret |
共享密鑰;落庫前加密,保存後無法再查看,請妥善備份 |
signatureHeader |
簽名頭名,默認 X-Webhook-Signature
|
timestampHeader |
時間戳頭名,默認 X-Webhook-Timestamp
|
簽名規則
algorithm = HMAC_SHA256
timestamp = current Unix epoch in milliseconds, as a decimal string
content = if payloadFormat == BODY → body
else (TIMESTAMP_BODY) → timestamp + "\n" + body
signature = lowercase hex( HMAC(secret, content) )
-
body是發到對端的原始字節 — 帶 body 的方法下是 序列化後的JSON.stringify(inputs);GET / DELETE 時為空字符串""。 -
secret和content都按 UTF-8 編碼處理。 -
平台同時發送
X-Webhook-Timestamp(或您自定義的時間戳頭),接收方按相同規則可重新計算簽名。
接收方校驗流程
-
從
X-Webhook-Timestamp取出時間戳; -
(建議)若
|now - timestamp| > 5 分钟,拒絕請求(防重放); - 用共享 secret 重新計算簽名;
-
與
X-Webhook-Signature做等長比較(防時序攻擊)。
參考實現:Node.js
參考驗簽器要求時間戳為 ASCII 十進制毫秒數,簽名為 64 個小寫十六進制字符,與上方簽名規則一致。格式不合法的請求頭會被拒絕。
const crypto = require('crypto');
function verify(req, secret) {
const timestamp = req.header('X-Webhook-Timestamp');
const signature = req.header('X-Webhook-Signature');
if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;
if (!/^\d+$/.test(timestamp) || !/^[0-9a-f]{64}$/.test(signature)) return false;
if (!Number.isSafeInteger(Number(timestamp))) return false;
// Replay protection (5-minute window)
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;
// payloadFormat = TIMESTAMP_BODY (default)
// Capture req.rawBody before any JSON parser; preserve the exact bytes.
if (!Buffer.isBuffer(req.rawBody)) return false;
const content = Buffer.concat([Buffer.from(`${timestamp}\n`, 'utf8'), req.rawBody]);
const expected = crypto
.createHmac('sha256', secret)
.update(content, 'utf8')
.digest('hex');
// Constant-time comparison
const a = Buffer.from(signature, 'utf8');
const b = Buffer.from(expected, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
參考實現:Python
import hmac, hashlib, time, re
def verify(request_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("X-Webhook-Timestamp")
sig = headers.get("X-Webhook-Signature")
if not isinstance(ts, str) or not re.fullmatch(r"[0-9]+", ts):
return False
if not isinstance(sig, str) or not re.fullmatch(r"[0-9a-f]{64}", sig):
return False
try:
timestamp = int(ts)
except ValueError:
return False
if abs(int(time.time() * 1000) - timestamp) > 5 * 60 * 1000:
return False
content = f"{ts}\n".encode() + request_body # payloadFormat = TIMESTAMP_BODY
expected = hmac.new(secret.encode(), content, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)
響應處理(僅 Webhook 動作節點)
只有同步的 Webhook 動作節點會檢查響應並按 outputs 解析;Decision 鈎子不解析響應。
| 結果 | 行為 |
|---|---|
2xx |
成功。按 Webhook 的 outputs JsonPath 列表解析
body,命中的字段成為節點輸出
|
4xx |
失敗。默認不重試(除非該狀態碼被顯式列入
retryOnStatus)
|
5xx |
失敗。按 maxAttempts 退避重試
|
| 網絡異常 / 超時 |
失敗。按 maxAttempts 退避重試
|
輸出解析
對每條 outputs
{paramName, jsonPath, paramType, defaultValue}:
- 響應 body 按 JSON 解析;
- 用 JsonPath 表達式取值;
-
按
paramType類型轉換後,以paramName暴露為節點輸出; -
若 JsonPath 未命中、或 body 非 JSON,使用
defaultValue(未配置則為null)。
// Response body
{ "data": { "score": 92, "reason": "OK" } }
| paramName | jsonPath | 結果 |
|---|---|---|
score |
$.data.score |
92 |
reason |
$.data.reason |
"OK" |
重試語義
| 字段 | 默認 | 說明 |
|---|---|---|
maxAttempts |
1(不重試)
|
限制在 [1, 5] 範圍
|
backoffMs |
500 |
指數退避,delay = backoffMs * 2^(attempt-1)
|
retryOnStatus |
空 |
除默認的 5xx + 網絡異常 + 超時之外,額外要重試的 HTTP
狀態碼(常用 408、429)
|
按默認 backoffMs=500、maxAttempts=5
計算,最壞情況累計退避 ~7.5 秒(0.5s + 1s + 2s + 4s)。
Decision Webhook 鈎子
Decision 節點可選地在決策落地為 approve /
reject 之後觸發一次 Webhook 推送。此路徑是
發送後不等待響應:
- 異步發送,工作流不會等響應;
- 不重試,失敗僅記審計,不阻斷決策流程;
- 不解析響應體;
-
決策元數據
action/code/reason/tags會自動合併到 payload — 僅當您的 inputs 字段裡沒聲明它們時。
接收方實踐建議
-
冪等 — 同步路徑在網絡抖動時可能重發,請按
executionId(或您自定義 ID)去重; - 快速響應 — 平台每次嘗試的 HTTP 超時默認 10 秒,耗時操作請先排隊、立即 ACK;
-
狀態碼語義 —
2xx表示成功;4xx表示永久失敗(不重試);5xx表示瞬時失敗(會重試)。給邏輯失敗返 2xx 會被當成成功; - 每次都校驗簽名 — 驗簽通過前不要信任 body;
- 拒絕過期時間戳 — 防重放;
-
聯調時打全
X-Webhook-Signature和X-Webhook-Timestamp— 方便比對。
故障排查
| 現象 | 可能原因 |
|---|---|
| 接收方返回 401 / 403 |
簽名不匹配 — 檢查算法、payload 格式、secret;確認
body 是發到對端的原始字節(不是格式化後的
JSON)
|
| POST 請求接收方沒收到 body | 方法是 GET / DELETE;或解析後的 inputs map 為空 |
outputs 全部為 null
|
響應不是 JSON、或 JsonPath 未命中。在 Webhook 編輯器
Output 區調整 JsonPath
|
| Decision 節點沒有觸發 Webhook |
決策沒有落到 approve / reject;或
triggerOn 不包含當前 action;或
hook.enabled = false
|
Webhook URL is blank after placeholder resolve
|
URL 里的 {var} 沒出現在 inputs 里 — 在 Inputs
區聲明,或確保上游節點提供
|
| Signature secret 報錯 | secret 為空或解密失敗 — 在 Webhook 編輯器 Signature 區重新保存 secret |
速查
-
簽名默認頭名:
X-Webhook-Signature、X-Webhook-Timestamp(每個 Webhook 可自定義) -
默認算法:
HMAC_SHA256(啓用簽名時) -
默認 payload 格式:
TIMESTAMP_BODY→timestamp + "\n" + body -
默認重試:關閉(
maxAttempts=1);啓用時backoffMs=500 - 每次 HTTP 超時:10 秒