區塊鏈 Webhook 設定:簽章驗證、去重與重放
透過 HTTP 建立地址訂閱,驗證原始請求主體簽章,按事件 ID 去重並復原保留的相符事件或缺失區塊。
關注一個 EVM 錢包地址,在你的 HTTPS 端點接收其原生幣轉帳、代幣轉帳與相符的合約日誌,用於錢包活動通知或智慧合約事件監控。開發者與 AI Agent 使用同一套 HTTP 訂閱 API。對於 ERC-20 USDT / USDC 付款通知,請參考穩定幣收款接收端。
這篇指南協助你完成的任務
- 接收錢包地址活動:透過建立帶驗證的訂閱、新增關注地址並驗證傳入事件。
- 監控相符的合約日誌:檢查關注地址的
log事件,並在接收端篩選address、topics與data。 - 復原中斷的送達:檢查訂閱進度並重放保留的相符事件,然後回填重放範圍之外的缺口。
一個訂閱結合了一個 HTTPS 接收 URL、一把簽名金鑰、多個關注的 EVM 地址以及一個必填的 chains 物件。地址適用於該物件中的每條鏈。使用帶 x-api-key 標頭的 API;帳戶中的任何有效 key 都可以管理其全部訂閱。開始之前請先取得 API key。推送 OpenAPI 列出了每項操作與 Webhook 結構定義。
接入錢包地址活動
- 部署一個接收端,以驗證原始請求主體,按
id持久化儲存事件,並在 10 秒內確認處理。 - 讀取
GET /v1/push/chains,然後使用你的 HTTPS URL 與所選鏈建立訂閱。儲存回傳的id與secret。 - 新增錢包地址。等待
applied_version >= change_version,並記錄每條鏈的applied_from_block;比對從該處開始。 - 處理轉帳與日誌,並復原缺口或被替換的區塊。在付款處理中使用通知前,先篩選代幣合約、收款地址與整數金額。
選擇 Webhook、WebSocket 或輪詢
- Webhook 將關注地址的事件發送到 HTTPS 接收端,支援送達重試與保留相符事件的重放。
- WebSocket 透過持續連線串流傳輸
newHeads與篩選後的logs。斷線後重新連線、重新訂閱並查詢錯過的區塊。 - 輪詢 使用你自己的游標在有界區塊範圍內查詢
eth_getLogs;用於監控付款或回填缺失的日誌。
檢查 GET /v1/chains 中的 ws 與 subscriptions 以確認 WebSocket 支援。如果 ws 為 false,只要該鏈出現在帶驗證的 GET /v1/push/chains 清單中,地址 Webhook 仍然可用。僅具備 RPC 支援並不代表支援推送。
地址容量
自助方案支援每個訂閱最多 1,000,000 個地址,註冊即可使用。一個訂閱涵蓋多條鏈與一個接收 URL。企業容量支援每個訂閱 10,000,000 / 100,000,000 個地址;聯絡我們以開通。開發者與 AI Agent 享有相同的容量方案與價格。兩個層級均沿用定價頁中顯示的相同地址日與已送達事件費率。
建立訂閱
讀取 GET /v1/push/chains 以取得可用鏈及其最小、預設與最大確認數。當 head - block + 1 >= confirmations 時釋放區塊。每條鏈可以透過提供 {} 來使用其預設值。至少需要一條鏈;新鏈不會自動加入現有訂閱。
將以下範例儲存為 create.json,將 URL 替換為你的接收端,並從鏈清單中選擇鏈。該 URL 必須使用連接埠 443 的 HTTPS、主機名稱而非 IP 常值,且不包含使用者資訊或片段(fragment)。
{
"url": "https://hooks.example.com/push",
"chains": {
"bsc_mainnet": {
"confirmations": 1
},
"base_mainnet": {}
}
}在環境中設定 BLOCKVECTRA_API_KEY,然後執行:
PUSH_URL='https://api.blockvectra.com/v1/push'
curl --fail-with-body -sS "$PUSH_URL/chains" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"
curl --fail-with-body -sS "$PUSH_URL/subscriptions" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-H 'Content-Type: application/json' \
-d @create.json > subscription.json建立成功會回傳 HTTP 201 以及一個沒有地址且狀態為 online 的訂閱。安全地儲存其數值 id 與 secret。金鑰僅在建立或 POST /subscriptions/{subscription_id}/rotate-secret 時回傳;輪換會立即在所有鏈上生效,沒有重疊期。不會發送測試訊息。
新增與列出地址
將地址批次儲存為 addresses.json,將範例地址替換為你要關注的地址:
{
"addresses": [
"0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
"0x99d47bB552ae095159C251836De6A5d524076872"
]
}將 SUBSCRIPTION_ID 設定為回傳的訂閱 ID:
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses/add" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-H 'Content-Type: application/json' -d @addresses.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"每次新增呼叫最多接受 10,000 個地址。輸入地址必須為全小寫或有效的 EIP-55 混合大小寫;無效輸入會拒絕整批。重複的地址計為 unchanged,因此重複發送相同的新增請求是安全的。地址清單使用 limit 與 page_token;next_page_token: null 標記為最後一頁。
新增地址會回傳 change_version。輪詢或檢查 GET /subscriptions/{subscription_id} 直到 applied_version >= change_version;變更通常約需 1 秒套用。每條鏈的 applied_from_block 標示出開始比對鏈上交易與日誌的生效區塊。新地址不會回溯比對。
建立訂閱回傳 HTTP 201 用於確認訂閱資源已建立;HTTP 201 並不代表你的接收端已收到任何 Webhook 推送。平台在建立或註冊地址時不會發送驗證或測試訊息。你必須等待關注地址與鏈上發生相符的鏈上活動,才能在接收端驗證送達。
事件格式
每個 POST 均包含 type: push.events、created_at 與 data。data 包含 subscription_id、一條 chain、complete_through_block 與 events。按鏈記錄進度:一個區塊可能跨越多條訊息,因此個別事件的區塊號不是完成標記。每條訊息最多包含 1,000 個事件、1 MiB 與 50 個區塊。
{
"type": "push.events",
"created_at": "2026-10-02T03:00:05Z",
"data": {
"subscription_id": 48213,
"chain": "bsc_mainnet",
"complete_through_block": 64000121,
"events": [
{
"id": "evt_payvsqb6ogymhmehrs2wl5xcky",
"type": "native.transfer",
"ref": "eip155:56:0x7b19944dc683c33e8dedba259cb6939f7271f70f2eeb6ca5e456cccb57cc1eff:tx",
"from": "0xe0a2100d7dad33f70c4bb765323cb96b2400c844",
"to": "0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
"amount": "150000000000000000",
"block_number": 64000120,
"block_hash": "0x327892a3e5699a43981f0fbcc5e490628641d92c040eb0429fb550ba3a73c3bf",
"block_timestamp": "2026-10-02T03:00:00Z",
"tx_hash": "0x7b19944dc683c33e8dedba259cb6939f7271f70f2eeb6ca5e456cccb57cc1eff",
"tx_index": 3,
"matched": [
{
"address": "0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
"role": "to"
}
]
},
{
"id": "evt_lgcdattb6l2k3ejuhe4mtdljkm",
"type": "token.transfer",
"ref": "eip155:56:0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76:7",
"standard": "erc20",
"token": "0x55d398326f99059ff775485246999027b3197955",
"from": "0x0f94e5283c41c29a8f4dff8c17f68bdfb59f07df",
"to": "0x99d47bb552ae095159c251836de6a5d524076872",
"token_id": null,
"amount": "25000000000000000000",
"batch_index": null,
"block_number": 64000121,
"block_hash": "0x6a8146159162f182c091d17eac7d03e95dc92ce80de704c958ca2528306aff15",
"block_timestamp": "2026-10-02T03:00:00Z",
"tx_hash": "0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76",
"tx_index": 5,
"log_index": 7,
"matched": [
{
"address": "0x99d47bb552ae095159c251836de6a5d524076872",
"role": "to"
}
]
},
{
"id": "evt_sgliw3ficdf6gaa6zzx4ew6vni",
"type": "log",
"ref": "eip155:56:0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76:8",
"address": "0xb54ffbe723264b84cf74947127a6914cf87fc593",
"topics": [
"0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
"0x00000000000000000000000099d47bb552ae095159c251836de6a5d524076872",
"0x000000000000000000000000b54ffbe723264b84cf74947127a6914cf87fc593"
],
"data": "0x0000000000000000000000000000000000000000000000000000000000000000",
"block_number": 64000121,
"block_hash": "0x6a8146159162f182c091d17eac7d03e95dc92ce80de704c958ca2528306aff15",
"block_timestamp": "2026-10-02T03:00:00Z",
"tx_hash": "0x3be3448e6f54ebfa928d5997a0cb2d5e9d3186c5ebfa293ad45b7edc72483a76",
"tx_index": 5,
"log_index": 8,
"matched": [
{
"address": "0x99d47bb552ae095159c251836de6a5d524076872",
"role": "topic1"
}
]
}
]
}
}| 事件類型 | 處理內容 |
|---|---|
native.transfer | 涉及關注地址的成功頂層原生價值轉帳;amount 為整數十進位字串。內部原生轉帳不計入。 |
token.transfer | 涉及關注地址的 ERC-20、ERC-721 與 ERC-1155 轉帳;檢查 standard、token、token_id、amount 與 batch_index。ERC-1155 批次轉帳每個項目產生一個事件。 |
log | 其他日誌,其中關注地址為發出合約或出現在 topics 1–3 中;檢查 address、topics、data 與 matched。 |
subscription.gap | 從 from_block 到 to_block 的範圍無法送達,原因為 reason: retention_expired;請使用 Data API 或 eth_getLogs 回填。 |
chain.reorg | 免費重組通知:已送達的 from_block–to_block 區塊已被替換。按 ref 標記或丟棄其舊事件,然後保留自動重新送達的規範鏈事件並按 id 去重。 |
在單一訂閱內,按事件 id 去重;跨訂閱時使用 ref 與 type。忽略未知欄位與事件類型。在採取財務動作之前,請核實鏈上事實。
驗證簽章
標頭包含 webhook-id、webhook-timestamp、webhook-signature 與 bv-subscription-id。僅從你建立的訂閱中挑選金鑰;拒絕未知 ID。在解析 JSON 之前,先以請求原始主體的位元組對 webhook-id.webhook-timestamp.raw-body 驗證 HMAC-SHA256。簽章為 v1,<base64>;允許約五分鐘的時間戳偏差並以常數時間比較。
此 Node.js 函式接受作為 Buffer 的原始主體、請求標頭以及將訂閱 ID 映射到儲存金鑰的 Map:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyPush(rawBody, headers, secrets) {
const subscriptionId = headers['bv-subscription-id'];
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signature = headers['webhook-signature'];
if ([subscriptionId, id, timestamp, signature].some(v => typeof v !== 'string')) return false;
const secret = secrets.get(subscriptionId);
if (typeof secret !== 'string' || !secret.startsWith('whsec_')) return false;
if (!/^\d{10}$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const match = /^v1,([A-Za-z0-9+/]{43}=)$/.exec(signature);
if (!match) return false;
const received = Buffer.from(match[1], 'base64');
const expected = createHmac('sha256', Buffer.from(secret.slice(6), 'base64'))
.update(`${id}.${timestamp}.`).update(rawBody).digest();
return received.length === expected.length && timingSafeEqual(received, expected);
}驗證後,解析主體,持久化處理結果,並在 10 秒內回傳 2xx。在驗證簽章之前,訂閱 ID 標頭是不可信任的。
驗證首個事件
保持訂閱為線上狀態(online)。地址變更套用後,等待相符的鏈上活動,並確認你的接收端驗證並持久儲存該事件。
驗證後停止監聽
若要停止關注地址,請將要移除的地址儲存在 addresses.json 中,並呼叫 POST /subscriptions/{subscription_id}/addresses/remove:
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses/remove" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-H 'Content-Type: application/json' -d @addresses.json每次移除呼叫最多接受 10,000 個地址。目前未被關注的地址計為 unchanged。此呼叫回傳 change_version。一旦 applied_version >= change_version,從該生效區塊起不再比對被移除的地址。先前已比對的事件(在傳輸中、重試中或已排隊)仍會送達;已送達的事件不會撤回。
若要暫時暫停監聽而不刪除設定或地址,請將 status 設定為 offline:
curl --fail-with-body -sS -X PATCH "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"status":"offline"}'offline 訂閱會停止監聽與送達,從比對索引中卸載地址,且在整天維持離線的任何完整 UTC 日內不產生地址費用。所有設定(URL、金鑰、地址、鏈與確認數)均保留。修補 {"status":"online"} 會從目前生效區塊恢復監聽,且不會回填離線期間的內容。
在 PATCH /subscriptions/{subscription_id} 上使用 JSON Merge Patch 來變更 url、key_id、status 或 chains:鏈物件會新增或更新它,而 null 則會將其移除。必須至少保留一條鏈。若要永久刪除訂閱:
curl --fail-with-body -sS -X DELETE "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"DELETE 會永久移除訂閱,立即停止所有鏈上的送達,並銷毀金鑰與地址。
送達、重試與重放
送達語意為至少一次。每個訂閱的鏈按區塊與區塊內的位置排序;失敗的批次會阻塞該鏈上的後續事件。不同的鏈具有獨立的進度,且可以並行 POST。相同批次的重試會保留 webhook-id,但已變更的批次可能會有新的 ID:應按事件去重,而不是按批次去重。
10 秒內的任何 2xx 均確認為持久化處理。不跟隨重新導向;3xx 與 410 均視為失敗。失敗後,重試依序遵循立即、5 秒、30 秒、2 分鐘、10 分鐘、30 分鐘與 1 小時的間隔,之後每小時重試。429 的 Retry-After 可以將等待時間延長至最多一小時。送達停止時,請檢查每條鏈的 condition、last_error 與 next_attempt_at。狀況包含 receiver_failing、insufficient_balance 與 key_revoked;最後一種需要將 key_id 修補至另一個有效的帳戶 key。
未送達的事件在超出保留時間窗後過期並產生 subscription.gap。POST /subscriptions/{subscription_id}/replay 接收 chain 與 from_block;請參考 GET /push/chains 中的 replayable_from_block 與訂閱的進度。重放會送達現有的相符事件,且無法復原在新增地址或鏈之前的事件。
chain.reorg 通知你已送達的區塊已被替換;它並不表示送達缺口。淺於你的確認數的重組是不可見的。對於影響已送達區塊深度達 1,024 個區塊的重組,規範鏈事件會自動以新的 id 重新送達。依 ref 標記或丟棄被替換的事件,保留規範鏈事件並按 id 去重;對於付款記錄,依 ref 與 tx_hash 進行對帳。更深層的重組會使鏈暫停:請檢查 GET /push/chains 中的 halted;鏈恢復後將自動重新送達規範鏈事件。該控制事件不會推進 complete_through_block。
使用 GET /subscriptions/{subscription_id}/events?chain=... 查詢已送達的資料事件,可選擇性新增 from_block、to_block、limit 與 page_token。歷史記錄列包含 event、replay_epoch、orphaned 與 delivered_at;orphaned: true 標記該區塊稍後被替換。歷史查詢准入可能回傳 402 insufficient_balance(data.reason:balance_exhausted 或 free_grant_exhausted)、403 key_cap_exhausted(data.cu_cap),或 429 rate_limited(key_rate_limit 或 free_plan_call_limit)。429 cost_exceeds_burst 帶有原因 request_exceeds_burst 與 data.max:在重試之前請提高突發容量。有關無效範圍與重試指引,請參閱錯誤處理。
計費與範例
權重來自 GET /v1/plans。已送達資料事件、成功的歷史請求與地址日分別具有獨立權重;除歷史查詢外的管理呼叫、控制事件、失敗的送達與自動重試均免費。每個已送達事件僅收取一次費用;客戶重放與規範鏈事件重新送達會產生新的送達費用。
地址計費使用每個訂閱在其處於 UTC 日內線上期間的最大地址數,扣除跨訂閱共享的帳戶免費地址額度(較早建立的訂閱優先)。同一個地址在兩個訂閱中計為兩次;新增鏈會改變事件費用,而不是地址費用。整天處於離線狀態的訂閱不產生該日地址費用。
| 用量 | 計費單位 | CU |
|---|---|---|
push.address_day | 計費地址日 | 33 |
push.history | 成功的歷史查詢請求 | 25 |
push.log | 已送達的資料事件 | 150 |
push.native_transfer | 已送達的資料事件 | 150 |
push.token_transfer | 已送達的資料事件 | 150 |
每帳戶每 UTC 日免費地址數:1000
每帳戶每 UTC 日的免費地址額度,由所有訂閱組共享,與方案無關。對每個組,統計其在該日上線期間的最大地址數;按組 ID 升序分配額度。同一地址在兩個組中計為兩份;組內鏈的數量不會使地址數成倍增加。整日離線或已刪除的組不計入。對每個組,扣除分配給它的額度後,將剩餘地址數乘以 `method_weights` 中的 `push.address_day` CU 權重。目前設定的額度來自地址日計費所用的同一定價政策;它不是帳戶容量上限,也不是每個組各自獨立的額度。
範例:10 個已送達的 native.transfer 事件、2 次成功歷史查詢與 10 個計費地址日,消耗 10 × 150 + 2 × 25 + 10 × 33 = 1880 CU。計費地址日已扣除帳戶免費地址額度。
相關資源
- 在區塊鏈 Webhook API 概覽中比較支援的事件、鏈覆蓋範圍與定價。
最後更新: