跳轉到主要內容

集成指南

托管核驗鏈接、名單篩查、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": {}
  }
}
本接口只服務身份核驗。交易類與 Web3 類事件沒有可跳轉的托管網頁流程,請改用 workflow.apply.submit 提交。

名單篩查

把一個待篩查主體(個人或企業)送進平台做名單篩查。平台會按客戶號建檔,在你的工作流里跑完篩查,命中就產生告警並開一個案件交給分析員裁定。使用 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 ——先把請求體序列化成字節,對這份字節簽名,再把同一份字節發出去,別讓框架在簽完之後又重新序列化一遍。完整規則和各語言代碼見身份驗證。

篩查會消耗第三方名單篩查供應商的配額,所以每一次提交都當成花錢的動作來對待:調用前先去重;萬一調用超時,先用workflow.apply.query查上一次提交跑到哪一步了,別直接重發。額度到底怎麼扣,以 管理控制台 → 賬單 用量頁上的數字為準(見用量與配額),別照自己的理解推算;正式放量接入之前,先小批量跑一兩筆,比一比那一頁跑前跑後的差值。

請求

{
  "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,政治公眾人物)姓名核查。

Quick Scan 與其餘 OpenAPI 接口共用同一網關基礎地址、同一套 query-string 請求信封(api / apiVersion / apiKey / sign)加 x-domain 請求頭,以及同樣的請求體 HMAC-SHA256 簽名。基礎地址與請求格式見 OpenAPI 參考 → 快速入門,完整簽名規則見身份驗證章節。

調用流程

  1. quickscan.submit — 提交篩查,立即返回 id(異步執行)
  2. quickscan.detail — 輪詢狀態,DONE 時返回命中摘要
  3. 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。

請求格式

請求頭

請求體(僅 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) )

接收方校驗流程

  1. 從 X-Webhook-Timestamp 取出時間戳;
  2. (建議)若 |now - timestamp| > 5 分钟,拒絕請求(防重放);
  3. 用共享 secret 重新計算簽名;
  4. 與 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}:

// 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 推送。此路徑是 發送後不等待響應:

需要根據響應分支? 請改用 Webhook 動作節點。

接收方實踐建議

  1. 冪等 — 同步路徑在網絡抖動時可能重發,請按 executionId(或您自定義 ID)去重;
  2. 快速響應 — 平台每次嘗試的 HTTP 超時默認 10 秒,耗時操作請先排隊、立即 ACK;
  3. 狀態碼語義 — 2xx 表示成功;4xx 表示永久失敗(不重試);5xx 表示瞬時失敗(會重試)。給邏輯失敗返 2xx 會被當成成功;
  4. 每次都校驗簽名 — 驗簽通過前不要信任 body;
  5. 拒絕過期時間戳 — 防重放;
  6. 聯調時打全 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

速查