# Configurar Webhooks de blockchain: firmas, deduplicación y reproducción

> Source: https://docs.blockvectra.com/es/guides/webhook-push/

Monitoree una dirección de billetera EVM y reciba sus transferencias nativas, transferencias de tokens y logs de contratos coincidentes en su endpoint HTTPS para notificaciones de actividad de billetera o monitoreo de eventos de contratos inteligentes. Los desarrolladores y agentes de IA utilizan la misma API de suscripción HTTP. Para notificaciones de pago ERC-20 en USDT / USDC, siga el [receptor de pagos con stablecoins](https://docs.blockvectra.com/en/guides/stablecoin-payments/#receive-payments-with-webhooks).

## Tareas que esta guía le ayuda a completar

* [Recibir actividad de direcciones de billetera](#connect-wallet-address-activity) creando una suscripción autenticada, agregando direcciones vigiladas y verificando los eventos entrantes.
* [Monitorear logs de contratos coincidentes](#event-format) inspeccionando eventos `log` para direcciones vigiladas y filtrando `address`, `topics` y `data` en su receptor.
* [Recuperar entregas interrumpidas](#delivery-retries-and-replay) verificando el progreso de la suscripción y reproduciendo coincidencias retenidas, luego recuperando lagunas fuera de la ventana de reproducción.

Una suscripción combina una URL de recepción HTTPS, un secreto de firma, direcciones EVM vigiladas y un objeto `chains` obligatorio. Las direcciones se aplican a cada cadena en ese objeto. Utilice la API con un encabezado `x-api-key`; cualquier clave activa en su cuenta puede gestionar todas sus suscripciones. [Obtenga una API key](https://blockvectra.com/en/get-api-key/) antes de comenzar. La [OpenAPI de Push](https://docs.blockvectra.com/openapi/push.yaml) enumera cada operación y esquema de webhook.

## Conectar la actividad de direcciones de billetera

1. Despliegue un receptor que [verifique el cuerpo original de la solicitud](#verify-signatures), persista los eventos por `id` y confirme su recepción en un plazo de 10 segundos.
2. Consulte `GET /v1/push/chains`, luego [cree una suscripción](#create-a-subscription) con su URL HTTPS y las cadenas seleccionadas. Guarde el `id` y el `secret` devueltos.
3. [Agregue las direcciones de billetera](#add-and-list-addresses). Espere a que `applied_version >= change_version` y registre el `applied_from_block` de cada cadena; la coincidencia comienza allí.
4. Procese transferencias y logs, y [recupere lagunas o bloques reemplazados](#delivery-retries-and-replay). Filtre contratos de tokens, destinatarios y montos enteros antes de utilizar las notificaciones en el procesamiento de pagos.

## Elegir Webhook, WebSocket o polling

* **Webhook** envía eventos de direcciones vigiladas a un receptor HTTPS, con reintentos de entrega y reproducción de coincidencias retenidas.
* **[WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/)** transmite `newHeads` y `logs` filtrados a través de una conexión persistente. Reconecte, vuelva a suscribirse y consulte los bloques perdidos tras una desconexión.
* **[Polling](https://docs.blockvectra.com/en/guides/stablecoin-payments/)** consulta `eth_getLogs` en rangos de bloques delimitados con su propio cursor; utilícelo para monitorear pagos o recuperar logs faltantes.

Verifique `ws` y `subscriptions` en `GET /v1/chains` para comprobar la compatibilidad con WebSocket. Si `ws` es false, los Webhooks de direcciones siguen siendo una opción cuando esa cadena aparece en la lista autenticada de `GET /v1/push/chains`. La compatibilidad con RPC por sí sola no establece la compatibilidad con Push.

## Capacidad de direcciones

El autoservicio admite hasta 1,000,000 de direcciones por suscripción y está disponible al registrarse. Una suscripción cubre múltiples cadenas con una sola URL de recepción. La capacidad empresarial admite 10,000,000 / 100,000,000 de direcciones por suscripción; [contáctenos para habilitarla](https://blockvectra.com/en/contact/). Los desarrolladores y los agentes de IA tienen las mismas opciones de capacidad y precios. Ambos niveles utilizan las mismas tarifas por dirección-día y por evento entregado que se muestran en [precios](https://blockvectra.com/en/pricing/).

## Crear una suscripción

Consulte `GET /v1/push/chains` para ver las cadenas disponibles y sus recuentos mínimo, predeterminado y máximo de confirmaciones. Un bloque se libera cuando `head - block + 1 >= confirmations`. Cada cadena puede usar su valor predeterminado proporcionando `{}`. Se requiere al menos una cadena; las cadenas nuevas no se unen automáticamente a las suscripciones existentes.

Guarde el siguiente ejemplo como `create.json`, reemplazando la URL con su receptor y seleccionando cadenas de la lista de cadenas. La URL debe usar HTTPS en el puerto 443, un nombre de host en lugar de una IP literal y no contener información de usuario ni fragmentos.

```json
{
  "url": "https://hooks.example.com/push",
  "chains": {
    "bsc_mainnet": {
      "confirmations": 1
    },
    "base_mainnet": {}
  }
}
```

Establezca `BLOCKVECTRA_API_KEY` en su entorno y luego ejecute:

```bash
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
```

Una creación exitosa devuelve HTTP 201 y una suscripción `online` sin direcciones. Guarde su `id` numérico y su `secret` de forma segura. El secreto se devuelve solo al momento de la creación o en `POST /subscriptions/{subscription_id}/rotate-secret`; la rotación entra en vigor inmediatamente en todas las cadenas, sin solapamiento. No se envía ningún mensaje de prueba.

## Agregar y listar direcciones

Guarde un lote de direcciones como `addresses.json`, reemplazando las direcciones de ejemplo con las que vigila:

```json
{
  "addresses": [
    "0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
    "0x99d47bB552ae095159C251836De6A5d524076872"
  ]
}
```

Establezca `SUBSCRIPTION_ID` con el ID de suscripción devuelto:

```bash
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"
```

Cada llamada de adición acepta un máximo de 10,000 direcciones. Las direcciones de entrada deben ser en minúsculas o con mayúsculas y minúsculas válidas según EIP-55; una entrada no válida rechaza todo el lote. Las direcciones repetidas cuentan como `unchanged`, por lo que es seguro reenviar la misma solicitud de adición. Las listas de direcciones usan `limit` y `page_token`; `next_page_token: null` marca la última página.

Al agregar direcciones se devuelve `change_version`. Realice sondeos o inspeccione `GET /subscriptions/{subscription_id}` hasta que `applied_version >= change_version`; los cambios normalmente tardan aproximadamente 1 segundo en aplicarse. El `applied_from_block` de cada cadena identifica el bloque efectivo a partir del cual se emparejan las transacciones y los logs on-chain. Las direcciones nuevas no se emparejan retroactivamente.

La creación de una suscripción devuelve HTTP 201 para confirmar que se creó el recurso de suscripción; HTTP 201 no significa que su receptor haya recibido ningún push de webhook. La plataforma no envía mensajes de verificación ni de prueba al momento de la creación o del registro de direcciones. Debe esperar a que ocurra actividad on-chain coincidente en las direcciones y cadenas vigiladas para verificar la entrega en su receptor.

## Formato de los eventos

Cada POST incluye `type: push.events`, `created_at` y `data`. `data` contiene `subscription_id`, una `chain`, `complete_through_block` y `events`. Registre el progreso por cadena: un bloque puede abarcar varios mensajes, por lo que los números de bloque de eventos individuales no son un marcador de finalización. Cada mensaje contiene como máximo 1,000 eventos, 1 MiB y 50 bloques.

```json
{
  "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"
          }
        ]
      }
    ]
  }
}
```

| Tipo de evento     | Qué gestionar                                                                                                                                                                                                                               |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `native.transfer`  | Transferencias exitosas de valor nativo de nivel superior que involucran una dirección vigilada; `amount` es una cadena decimal entera. Se excluyen las transferencias nativas internas.                                                    |
| `token.transfer`   | Transferencias ERC-20, ERC-721 y ERC-1155 que involucran direcciones vigiladas; inspeccione `standard`, `token`, `token_id`, `amount` y `batch_index`. Las transferencias por lotes ERC-1155 generan un evento por elemento.                |
| `log`              | Otros logs que identifican una dirección vigilada como contrato emisor o en los topics 1–3; inspeccione `address`, `topics`, `data` y `matched`.                                                                                            |
| `subscription.gap` | Un rango desde `from_block` hasta `to_block` no está disponible para entrega, con `reason: retention_expired`; recupere con Data API o `eth_getLogs`.                                                                                       |
| `chain.reorg`      | Aviso de reorganización gratuito: los bloques entregados en `from_block`–`to_block` fueron reemplazados. Marque o descarte sus eventos antiguos por `ref`, conserve los eventos canónicos reenviados automáticamente y deduplique por `id`. |

Dentro de una suscripción, deduplique por el `id` del evento; entre suscripciones use `ref` y `type`. Ignore los campos y tipos de evento desconocidos. Verifique los hechos on-chain antes de realizar cualquier acción financiera.

## Verificar firmas

Los encabezados son `webhook-id`, `webhook-timestamp`, `webhook-signature` y `bv-subscription-id`. Elija el secreto únicamente de las suscripciones que haya creado; rechace los IDs desconocidos. Verifique HMAC-SHA256 sobre `webhook-id.webhook-timestamp.raw-body`, utilizando los bytes del cuerpo original de la solicitud, antes de analizar el JSON. La firma es `v1,<base64>`; permita unos cinco minutos de desfase en la marca temporal y compare en tiempo constante.

Esta función de Node.js acepta el cuerpo sin procesar como un `Buffer`, los encabezados de la solicitud y un mapa de IDs de suscripción a secretos almacenados:

```js
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);
}
```

Después de la verificación, analice el cuerpo, persista el procesamiento y devuelva 2xx en un plazo de 10 segundos. El encabezado del ID de suscripción no es confiable hasta que se verifique la firma.

## Verificar su primer evento

Mantenga la suscripción online. Después de que se aplique el cambio de dirección, espere a que haya actividad on-chain coincidente y verifique que su receptor compruebe y almacene el evento de forma duradera.

## Detener la escucha tras la verificación

<a id="stop-listening-and-clean-up" />

Para dejar de vigilar direcciones, guarde las direcciones a eliminar en `addresses.json` y llame a `POST /subscriptions/{subscription_id}/addresses/remove`:

```bash
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
```

Cada llamada de eliminación acepta un máximo de 10,000 direcciones. Una dirección que no se esté vigilando actualmente cuenta como `unchanged`. La llamada devuelve `change_version`. Una vez que `applied_version >= change_version`, los bloques a partir de ese bloque efectivo ya no coincidirán con las direcciones eliminadas. Los eventos previamente emparejados (en tránsito, reintentando o en cola) aún se entregan; los eventos ya entregados no se retiran.

Para pausar temporalmente la escucha sin eliminar la configuración ni las direcciones, establezca `status` en `offline`:

```bash
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"}'
```

Una suscripción `offline` detiene la escucha y la entrega, descarga las direcciones del índice de coincidencia y no incurre en tarifas por dirección durante ningún día UTC completo en que permanezca offline. Toda la configuración (URL, secreto, direcciones, cadenas y confirmaciones) se conserva. Aplicar `{"status":"online"}` reanuda la escucha desde el bloque efectivo actual y no recupera el período offline.

Utilice JSON Merge Patch en `PATCH /subscriptions/{subscription_id}` para cambiar `url`, `key_id`, `status` o `chains`: un objeto de cadena lo agrega o actualiza, y `null` lo elimina. Debe permanecer al menos una cadena. Para eliminar permanentemente la suscripción:

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

`DELETE` elimina permanentemente la suscripción, detiene la entrega en todas las cadenas de inmediato y destruye el secreto y las direcciones.

## Entrega, reintentos y reproducción

La entrega es al menos una vez. La cadena de cada suscripción se ordena por bloque y posición en el bloque; los lotes fallidos bloquean los eventos posteriores en esa cadena. Diferentes cadenas tienen un progreso independiente y pueden realizar llamadas POST concurrentemente. Un reintento de un lote idéntico conserva `webhook-id`, pero un lote modificado puede tener un nuevo ID: deduplique eventos, no lotes.

Cualquier 2xx dentro de los 10 segundos confirma el procesamiento duradero. No se siguen las redirecciones; 3xx y 410 se consideran fallos. Después de un fallo, los reintentos siguen intervalos de inmediato, 5 segundos, 30 segundos, 2 minutos, 10 minutos, 30 minutos y 1 hora, y luego cada hora. Un encabezado `Retry-After` con 429 puede extender la espera hasta por una hora. Inspeccione `condition`, `last_error` y `next_attempt_at` de cada cadena cuando la entrega se detenga. Las condiciones son `receiver_failing`, `insufficient_balance` y `key_revoked`; esta última requiere actualizar `key_id` a otra clave activa de la cuenta.

Los eventos no entregados vencen fuera de la ventana de retención y producen `subscription.gap`. `POST /subscriptions/{subscription_id}/replay` toma `chain` y `from_block`; consulte `replayable_from_block` en `GET /push/chains` y el progreso de la suscripción. La reproducción entrega coincidencias existentes y no puede recuperar eventos anteriores a la adición de una dirección o cadena.

`chain.reorg` le notifica que los bloques ya entregados fueron reemplazados; no indica una brecha de entrega. Las reorganizaciones menos profundas que su recuento de confirmaciones son invisibles. Para reorganizaciones que afectan a bloques entregados de hasta 1,024 bloques de profundidad, los eventos canónicos se vuelven a entregar automáticamente con nuevos `id`. Marque o descarte los eventos reemplazados por `ref`, conserve los eventos canónicos y deduplique por `id`; para registros de pago, reconcilie por `ref` y `tx_hash`. Una reorganización más profunda detiene la cadena: verifique `halted` en `GET /push/chains`; la reentrega canónica sigue después de que se restaure la cadena. El evento de control no hace avanzar `complete_through_block`.

Consulte los eventos de datos entregados con `GET /subscriptions/{subscription_id}/events?chain=...`, agregando opcionalmente `from_block`, `to_block`, `limit` y `page_token`. Las filas de historial contienen `event`, `replay_epoch`, `orphaned` y `delivered_at`; `orphaned: true` marca un bloque reemplazado posteriormente. La admisión al historial puede devolver 402 `insufficient_balance` (`data.reason`: `balance_exhausted` o `free_grant_exhausted`), 403 `key_cap_exhausted` (`data.cu_cap`) o 429 `rate_limited` (`key_rate_limit` o `free_plan_call_limit`). Un 429 `cost_exceeds_burst` tiene el motivo `request_exceeds_burst` y `data.max`: aumente la capacidad de ráfaga antes de reintentar. Consulte el [manejo de errores](https://docs.blockvectra.com/en/errors/) para rangos inválidos y orientación sobre reintentos.

## Facturación y ejemplo

Los pesos provienen de `GET /v1/plans`. Los eventos de datos entregados, las solicitudes de historial exitosas y las direcciones-día tienen pesos separados; las llamadas de gestión distintas del historial, los eventos de control, las entregas fallidas y los reintentos automáticos son gratuitos. Cada evento entregado se cobra una vez; la reproducción solicitada por el cliente y la reentrega de eventos canónicos incurren en nuevos cargos de entrega.

La facturación de direcciones utiliza el mayor recuento de direcciones de cada suscripción durante su período online del día UTC, después de la franquicia gratuita de direcciones de la cuenta compartida entre suscripciones (las suscripciones más antiguas primero). La misma dirección en dos suscripciones cuenta dos veces; agregar cadenas cambia los cargos por eventos, no los cargos por dirección. Una suscripción offline durante todo el día UTC no tiene cargo por dirección.

| Uso | Unidad de facturación | CU |
| --- | --- | --- |
| `push.address_day` | Dirección-día facturable | 33 |
| `push.history` | Solicitud de historial exitosa | 25 |
| `push.log` | Evento de datos entregado | 150 |
| `push.native_transfer` | Evento de datos entregado | 150 |
| `push.token_transfer` | Evento de datos entregado | 150 |

Direcciones gratuitas por cuenta por día UTC: 1000

Franquicia de direcciones gratuitas por cuenta por día UTC, compartida entre todos los grupos de suscripción independientemente del plan. Para cada grupo, se cuenta la cantidad máxima de direcciones mientras estuvo online durante ese día; la franquicia se asigna en orden ascendente de ID de grupo. La misma dirección en dos grupos cuenta dos veces; el número de cadenas en un grupo no multiplica su recuento de direcciones. Un grupo offline o eliminado durante todo el día no aporta nada. Para cada grupo, la cantidad restante tras su parte de la franquicia se multiplica por el peso en CU de `push.address_day` en `method_weights`. La franquicia actualmente configurada procede de la misma política de precios utilizada para el cobro de dirección-día; no es un límite de capacidad de la cuenta ni una franquicia independiente por grupo.

Ejemplo: 10 eventos native.transfer entregados, 2 solicitudes de historial exitosas y 10 direcciones-día facturables cuestan 10 × 150 + 2 × 25 + 10 × 33 = 1880 CU. Las direcciones-día facturables se contabilizan tras la franquicia de direcciones gratuitas de la cuenta.

Consulte las [reglas de facturación](https://docs.blockvectra.com/en/guides/billing-rules/) y la [página de precios](https://blockvectra.com/en/pricing/) para la medición y conversión de CU.

## Recursos relacionados

* Compare eventos compatibles, cobertura de cadenas y precios en la [descripción general de la API de Webhooks de blockchain](https://blockvectra.com/en/webhooks/).
