ブロックチェーン Webhook の設定:署名検証、重複排除、再送

HTTP でアドレスの購読を作成し、生のボディに対する署名を検証して、イベント ID の重複を排除し、保持されている一致イベントや欠落したブロックを復旧します。

EVM ウォレットアドレスを監視し、そのネイティブ通貨転送、トークン転送、一致するコントラクトログを HTTPS エンドポイントで受信して、ウォレット活動の通知やスマートコントラクトイベントの監視に利用できます。開発者と AI エージェントは同じ HTTP 購読 API を使用します。ERC-20 USDT / USDC の入金通知については、ステーブルコイン入金の受信を参照してください。

このガイドでできること

購読は、1 つの HTTPS 受信 URL、署名用シークレット、監視対象の EVM アドレス、必須の chains オブジェクトを組み合わせたものです。アドレスは、そのオブジェクト内のすべてのチェーンに適用されます。API は x-api-key ヘッダーを付けて使用してください。アカウント内の有効なキーであれば、どのキーでもそのアカウントのすべての購読を管理できます。開始前に API key を取得してください。Push 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 を照会します。入金の監視や欠落したログのバックフィルに使用してください。

WebSocket の対応状況は、GET /v1/chains の ws と subscriptions を確認してください。ws が false でも、そのチェーンが認証付きの GET /v1/push/chains の一覧に含まれていれば、アドレス Webhook を利用できます。RPC に対応しているだけでは、Push に対応しているとは限りません。

アドレス容量

セルフサービスは購読あたり最大 1,000,000 アドレスに対応し、登録後すぐに利用できます。1 つの購読で、1 つの受信 URL を使って複数のチェーンをカバーします。エンタープライズの容量は購読あたり 10,000,000 / 100,000,000 アドレスです。有効化についてお問い合わせください。開発者と AI エージェントは、同じ容量プランと料金を利用できます。両プランとも、料金に記載された同じアドレス日と配信済みイベントの単価が適用されます。

購読を作成する

GET /v1/push/chains を読み取り、利用可能なチェーンと、確認数の最小値、デフォルト値、最大値を確認してください。ブロックは head - block + 1 >= confirmations になると配信対象になります。各チェーンには {} を指定することでデフォルト値を使用できます。少なくとも 1 つのチェーンが必要です。新しいチェーンが既存の購読に自動で追加されることはありません。

以下の例を create.json として保存し、URL を受信先の URL に置き換えて、チェーン一覧からチェーンを選択してください。URL はポート 443 の HTTPS を使用し、IP アドレスのリテラルではなくホスト名を指定し、ユーザー情報やフラグメントを含めないでください。

{
  "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 が返されます。applied_version >= change_version になるまで GET /subscriptions/{subscription_id} をポーリングするか確認してください。変更は通常、約 1 秒で適用されます。各チェーンの applied_from_block は、オンチェーンのトランザクションとログの照合が始まる有効開始ブロックを示します。新しいアドレスが過去にさかのぼって照合されることはありません。

購読の作成時には、購読リソースが作成されたことを確認するために HTTP 201 が返されます。HTTP 201 は、受信先で Webhook プッシュを受信したことを意味しません。プラットフォームは、作成時やアドレス登録時に検証メッセージやテストメッセージを送信しません。受信先への配信を検証するには、監視対象のアドレスとチェーンで、一致するオンチェーン活動が発生するまで待つ必要があります。

イベント形式

すべての POST には type: push.events、created_at、data が含まれます。data には subscription_id、1 つの chain、complete_through_block、events が含まれます。進捗はチェーンごとに記録してください。1 つのブロックが複数のメッセージにまたがることがあるため、個々のイベントのブロック番号は完了を示しません。各メッセージの上限は 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 は整数の 10 進文字列です。内部のネイティブ通貨転送は含まれません。
token.transfer監視対象アドレスが関わる ERC-20、ERC-721、ERC-1155 の転送。standard、token、token_id、amount、batch_index を確認してください。ERC-1155 のバッチ転送では、項目ごとに 1 つのイベントが生成されます。
log監視対象アドレスが発行元コントラクト、または topics 1–3 に含まれるその他のログ。address、topics、data、matched を確認してください。
subscription.gapfrom_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> です。タイムスタンプのずれは約 5 分まで許容し、定数時間で比較してください。

次の Node.js 関数は、Buffer としての生のボディ、リクエストヘッダー、購読 ID と保存済みシークレットを対応付けたマップを受け取ります。

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 ヘッダーを信頼できません。

最初のイベントを検証する

購読をオンラインに保ってください。アドレスの変更が適用されたら、一致するオンチェーン活動を待ち、受信側でイベントを検証して永続的に保存できることを確認してください。

検証後に監視を停止する

アドレスの監視を停止するには、削除するアドレスを 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 すると、現在の有効開始ブロックから監視を再開し、オフラインだった期間はバックフィルしません。

PATCH /subscriptions/{subscription_id} で JSON Merge Patch を使用すると、url、key_id、status、chains を変更できます。チェーンオブジェクトを指定すると、そのチェーンを追加または更新し、null を指定すると削除します。少なくとも 1 つのチェーンを残す必要があります。購読を完全に削除するには、次を実行してください。

curl --fail-with-body -sS -X DELETE "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

DELETE は購読を完全に削除し、すべてのチェーンで配信を即座に停止して、シークレットとアドレスを破棄します。

配信、再試行、再送

配信は少なくとも 1 回行われます。各購読のチェーン内では、ブロックとブロック内の位置の順に配信されます。バッチが失敗すると、そのチェーンの後続イベントは配信が止まります。異なるチェーンの進捗は独立しており、並行して POST できます。同一バッチの再試行では webhook-id が維持されますが、バッチが変更されると新しい ID になることがあります。重複排除はバッチではなくイベントに対して行ってください。

10 秒以内に返される 2xx は、処理結果が永続化されたことの確認になります。リダイレクトには追従しません。3xx と 410 は失敗です。失敗後は、即時、5 秒、30 秒、2 分、10 分、30 分、1 時間の間隔で再試行し、その後は毎時間再試行します。429 の Retry-After により、待機時間が最大 1 時間まで延びることがあります。配信が停止した場合は、各チェーンの condition、last_error、next_attempt_at を確認してください。条件は receiver_failing、insufficient_balance、key_revoked です。最後の条件では、key_id をアカウント内の別の有効なキーに PATCH する必要があります。

未配信イベントは保持期間を過ぎると期限切れになり、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 から取得します。配信済みデータイベント、成功した履歴リクエスト、アドレス日には、それぞれ別の重み付けが適用されます。履歴以外の管理コール、制御イベント、失敗した配信、自動再試行は無料です。配信された各イベントは 1 回課金されます。利用者による再送と正規チェーンのイベントの再配信には、新たな配信料金が発生します。

アドレス課金は、UTC 日に各購読がオンラインだった間の最大アドレス数に基づき、購読間で共有するアカウントの無料アドレス枠を差し引いた後に適用されます(古い購読から順に割り当てます)。同じアドレスが 2 つの購読にある場合は 2 回数えます。チェーンを追加するとイベント料金は変わりますが、アドレス料金は変わりません。UTC 日の丸一日を通してオフラインだった購読には、アドレス料金は発生しません。

利用量課金単位CU
push.address_day課金対象のアドレス日33
push.history成功した履歴リクエスト25
push.log配信済みデータイベント150
push.native_transfer配信済みデータイベント150
push.token_transfer配信済みデータイベント150

アカウントごとの UTC 日あたりの無料アドレス数: 1000

アカウントごとの UTC 日あたりの無料アドレス枠は、プランに関係なくすべてのサブスクリプショングループで共有されます。各グループについて、その日にオンラインだった間の最大アドレス数を数え、グループ ID の昇順に枠を割り当てます。同じアドレスが二つのグループにある場合は二回数えます。グループ内のチェーン数によってアドレス数が倍増することはありません。一日中オフラインまたは削除済みのグループは数えません。各グループの割り当て枠を差し引いた残りの数に、`push.address_day` CU ウェイト(`method_weights` に記載)を掛けます。現在設定されている枠はアドレス日課金と同じ料金ポリシーに基づきます。アカウントの容量制限でも、グループごとの個別枠でもありません。

例:配信済みの native.transfer イベント 10 件、成功した履歴リクエスト 2 回、課金対象のアドレス日 10 件の利用量は、10 × 150 + 2 × 25 + 10 × 33 = 1880 CU です。課金対象のアドレス日は、アカウントの無料アドレス枠を差し引いた後に数えます。

CU の計測と換算については、課金ルールと料金ページを参照してください。

関連リソース

最終更新:

このページの目次