區塊鏈 Webhook 設定:簽章驗證、去重與重放

透過 HTTP 建立地址訂閱,驗證原始請求主體簽章,按事件 ID 去重並復原保留的相符事件或缺失區塊。

關注一個 EVM 錢包地址,在你的 HTTPS 端點接收其原生幣轉帳、代幣轉帳與相符的合約日誌,用於錢包活動通知或智慧合約事件監控。開發者與 AI Agent 使用同一套 HTTP 訂閱 API。對於 ERC-20 USDT / USDC 付款通知,請參考穩定幣收款接收端。

這篇指南協助你完成的任務

一個訂閱結合了一個 HTTPS 接收 URL、一把簽名金鑰、多個關注的 EVM 地址以及一個必填的 chains 物件。地址適用於該物件中的每條鏈。使用帶 x-api-key 標頭的 API;帳戶中的任何有效 key 都可以管理其全部訂閱。開始之前請先取得 API key。推送 OpenAPI 列出了每項操作與 Webhook 結構定義。

接入錢包地址活動

  1. 部署一個接收端,以驗證原始請求主體,按 id 持久化儲存事件,並在 10 秒內確認處理。
  2. 讀取 GET /v1/push/chains,然後使用你的 HTTPS URL 與所選鏈建立訂閱。儲存回傳的 id 與 secret。
  3. 新增錢包地址。等待 applied_version >= change_version,並記錄每條鏈的 applied_from_block;比對從該處開始。
  4. 處理轉帳與日誌,並復原缺口或被替換的區塊。在付款處理中使用通知前,先篩選代幣合約、收款地址與整數金額。

選擇 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。計費地址日已扣除帳戶免費地址額度。

有關 CU 計量與換算,請參閱計費規則與定價頁面。

相關資源

最後更新:

本頁目錄