Webhook 推送订阅

接收所选链上的地址活动,管理订阅、校验 webhook 签名,了解投递语义与 CU 计费。

一个订阅包含一个 HTTPS 接收 URL、一把签名密钥、一组关注的 EVM 地址与必填的 chains 对象。地址对该对象中的每条链生效。通过 x-api-key 请求头调用 API,本账户任意有效 key 都能管理账户内的全部订阅。开始前先获取 API key。全部操作与 webhook schema 见推送 OpenAPI。

创建订阅

调用 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/addresses/remove" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' -d @addresses.json

每次增删最多 10,000 个地址。输入须为小写地址或通过 EIP-55 校验的混合大小写地址,无效输入会拒绝整批。重复地址计为 unchanged,重复发送同一增删请求是安全的。列表使用 limit 与 page_token 分页,next_page_token: null 表示最后一页。

修改返回 change_version。轮询 GET /subscriptions/{subscription_id},直到 applied_version >= change_version;每条链的 applied_from_block 表示生效块。新地址不追溯匹配历史事件。删地址从生效块起停止新的匹配,先前已匹配的事件仍可能送达。

通过 PATCH /subscriptions/{subscription_id} 的 JSON Merge Patch 修改 url、key_id、status 或 chains:链对象用于新增或修改,null 用于删链,至少保留一条链。offline 停止监听与投递并保留配置;改回 online 从当前生效块开始匹配,不补下线期间的事件。DELETE 永久删除订阅。

事件格式

每个 POST 的正文包含 type: push.events、created_at 与 data。data 包含 subscription_id、单条 chain、complete_through_block 与 events。按链记录进度:同一区块可能跨多条消息,单个事件的块号不是整块完成标记。

{
  "type": "push.events",
  "created_at": "2026-10-02T03:00:05Z",
  "data": {
    "subscription_id": 48213,
    "chain": "bsc_mainnet",
    "complete_through_block": 64000121,
    "events": [
      {
        "id": "evt_ak5pcuhp27nqor33ghe5rksiu4",
        "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_qrirplrzttdrf4a5azk47zavei",
        "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_ccdxyd2g7pldjo4clal3oh63vi",
        "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.gapfrom_block 到 to_block 区间无法有效投递,reason: retention_expired;使用 Data API 或 eth_getLogs 补扫。
chain.incident已投递的 from_block–to_block 区间被替换;按 tx_hash 核对并处理后续规范链事件。

同一订阅内按事件 id 去重,跨订阅按 ref 与 type 去重。忽略未知字段与未知事件类型。涉及资金操作时自行核验链上事实。

签名校验

请求头为 webhook-id、webhook-timestamp、webhook-signature 与 bv-subscription-id。只从你创建的订阅中选择密钥,拒绝未知 ID。先以请求原始字节校验 webhook-id.webhook-timestamp.raw-body 的 HMAC-SHA256,再解析 JSON。签名格式为 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 请求头。

投递、重试与重放

投递语义为至少一次。每个订阅的单条链按区块与块内顺序投递,失败批次会阻塞该链后续事件。不同链进度独立,可以并发 POST。同一批重试保留 webhook-id,但批次变化可能产生新 ID,因此按事件去重,不按批次去重。

10 秒内返回任意 2xx 表示已持久处理。重定向不跟随,3xx 与 410 都按失败处理。失败后的重试间隔依次为立即、5 秒、30 秒、2 分钟、10 分钟、30 分钟、1 小时,之后每小时重试。429 的 Retry-After 可延长等待,按最长一小时处理。投递停住时查看每条链的 condition、last_error 与 next_attempt_at。condition 包括 receiver_failing、insufficient_balance 与 key_revoked;最后一种需 PATCH key_id 到本账户另一把有效 key。

未送达事件超出保留窗口后过期,产生 subscription.gap。POST /subscriptions/{subscription_id}/replay 接收 chain 与 from_block,参考 GET /push/chains 的 replayable_from_block 与订阅进度选择起点。重放投递已匹配的事件,不能补地址或链加入前的事件。影响已投递区块的重组会产生 chain.incident,随后重投规范链事件;链停止时查看链列表的 halted。

通过 GET /subscriptions/{subscription_id}/events?chain=... 查询已送达数据事件,可附加 from_block、to_block、limit 与 page_token。历史行包含 event、replay_epoch、orphaned 与 delivered_at,orphaned: true 表示所在区块后来被替换。区块范围错误与重试处理见错误参考。

计费与示例

权重来自 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 计量与换算见计费规则及定价页。

最后更新:

本页目录