Configuration des Webhooks blockchain : signatures, déduplication et replay
Créez des abonnements d'adresses par HTTP, vérifiez les signatures du corps brut, dédupliquez les ID d'événements et récupérez les correspondances conservées ou les blocs manquants.
Surveillez une adresse de wallet EVM et recevez ses transferts natifs, ses transferts de tokens et ses logs de contrats correspondants sur votre point de terminaison HTTPS pour les notifications d'activité de wallet ou la surveillance des événements de smart contracts. Les développeurs et les agents IA utilisent la même API d'abonnement HTTP. Pour les notifications de paiement ERC-20 en USDT / USDC, suivez le récepteur de paiements en stablecoins.
- Première étape : Déployer un récepteur qui vérifie les signatures du corps brut, à l'aide de l'exemple de vérification ci-dessous.
- Terminé quand : Après
applied_version >= change_version, l'activité on-chain correspondante atteint votre récepteur, passe la vérification de signature et est persistée par l'idde l'événement ; la création d'un abonnement n'envoie pas de message de test.
Tâches que ce guide vous aide à accomplir
- Recevoir l'activité d'une adresse de wallet en créant un abonnement authentifié, en ajoutant les adresses surveillées et en vérifiant les événements entrants.
- Surveiller les logs de contrats correspondants en inspectant les événements
logpour les adresses surveillées et en filtrantaddress,topicsetdatadans votre récepteur. - Récupérer après une interruption de livraison en vérifiant la progression de l'abonnement et en rejouant les correspondances conservées, puis en comblant les écarts en dehors de la fenêtre de rejeu.
Un abonnement associe une URL de réception HTTPS, un secret de signature, des adresses EVM surveillées et un objet chains requis. Les adresses s'appliquent à chaque chaîne de cet objet. Utilisez l'API avec un en-tête x-api-key ; toute clé active de votre compte peut gérer l'ensemble de ses abonnements. Obtenez une API key avant de commencer. L'OpenAPI Push répertorie chaque opération et schéma de webhook.
Connecter l'activité des adresses de wallet
- Déployez un récepteur qui vérifie le corps de requête d'origine, persiste les événements par
idet en accuse réception dans les 10 secondes. - Lisez
GET /v1/push/chains, puis créez un abonnement avec votre URL HTTPS et les chaînes sélectionnées. Enregistrez l'idet lesecretrenvoyés. - Ajoutez les adresses de wallet. Attendez que
applied_version >= change_versionet enregistrez l'applied_from_blockde chaque chaîne ; la mise en correspondance commence à partir de là. - Traitez les transferts et les logs, et récupérez les écarts ou blocs remplacés. Filtrez les contrats de tokens, les destinataires et les montants entiers avant d'utiliser les notifications dans le traitement des paiements.
Choisir entre Webhook, WebSocket ou polling
- Webhook envoie les événements des adresses surveillées à un récepteur HTTPS, avec des nouvelles tentatives de livraison et le rejeu des correspondances conservées.
- WebSocket diffuse en flux continu
newHeadset leslogsfiltrés via une connexion persistante. Reconnectez-vous, réabonnez-vous et interrogez les blocs manqués après une déconnexion. - Polling interroge
eth_getLogsdans des plages de blocs bornées avec votre propre curseur ; utilisez-le pour surveiller les paiements ou rétro-remplir les logs manquants.
Vérifiez ws et subscriptions dans GET /v1/chains pour la prise en charge de WebSocket. Si ws est à false, les Webhooks d'adresse restent une option lorsque cette chaîne apparaît dans la liste authentifiée GET /v1/push/chains. La prise en charge RPC à elle seule ne garantit pas la prise en charge de Push.
Capacité d'adresses
Le libre-service prend en charge jusqu'à 1 000 000 d'adresses par abonnement et est disponible dès l'inscription. Un abonnement couvre plusieurs chaînes avec une seule URL de réception. La capacité Entreprise prend en charge 10 000 000 / 100 000 000 d'adresses par abonnement ; contactez-nous pour l'activer. Les développeurs et les agents IA disposent des mêmes options de capacité et des mêmes tarifs. Les deux niveaux appliquent les mêmes tarifs par jour-adresse et événement livré affichés sur la page Tarifs.
Créer un abonnement
Lisez GET /v1/push/chains pour connaître les chaînes disponibles et leurs nombres de confirmations minimal, par défaut et maximal. Un bloc est libéré lorsque head - block + 1 >= confirmations. Chaque chaîne peut utiliser sa valeur par défaut en fournissant {}. Au moins une chaîne est requise ; les nouvelles chaînes ne s'ajoutent pas automatiquement aux abonnements existants.
Enregistrez l'exemple suivant sous le nom create.json, en remplaçant l'URL par votre récepteur et en sélectionnant des chaînes dans la liste des chaînes. L'URL doit utiliser HTTPS sur le port 443, un nom d'hôte plutôt qu'une adresse IP littérale, et ne comporter aucune information d'utilisateur ni fragment.
{
"url": "https://hooks.example.com/push",
"chains": {
"bsc_mainnet": {
"confirmations": 1
},
"base_mainnet": {}
}
}Définissez BLOCKVECTRA_API_KEY dans votre environnement, puis exécutez :
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.jsonUne création réussie renvoie HTTP 201 et un abonnement online sans adresses. Conservez son id numérique et son secret en lieu sûr. Le secret n'est renvoyé que lors de la création ou lors de POST /subscriptions/{subscription_id}/rotate-secret ; la rotation prend effet immédiatement sur toutes les chaînes, sans chevauchement. Aucun message de test n'est envoyé.
Ajouter et lister des adresses
Enregistrez un lot d'adresses sous le nom addresses.json, en remplaçant les adresses d'exemple par celles que vous surveillez :
{
"addresses": [
"0x9725db73f2cd8657f3e1841e5689f210ee54a92d",
"0x99d47bB552ae095159C251836De6A5d524076872"
]
}Définissez SUBSCRIPTION_ID avec l'identifiant d'abonnement renvoyé :
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"Chaque appel d'ajout accepte au maximum 10 000 adresses. Les adresses en entrée sont en minuscules ou en casse mixte valide selon l'EIP-55 ; toute entrée non valide rejette l'intégralité du lot. Les adresses répétées comptent comme unchanged, de sorte que le réenvoi de la même requête d'ajout est sans danger. Les listes d'adresses utilisent limit et page_token ; next_page_token: null marque la dernière page.
L'ajout d'adresses renvoie change_version. Scrutez ou inspectez GET /subscriptions/{subscription_id} jusqu'à ce que applied_version >= change_version ; l'application des modifications prend généralement environ 1 seconde. Le champ applied_from_block de chaque chaîne identifie le bloc effectif à partir duquel les transactions et logs on-chain sont mis en correspondance. Les nouvelles adresses ne sont pas mises en correspondance de manière rétroactive.
La création d'un abonnement renvoie HTTP 201 pour confirmer que la ressource d'abonnement a été créée ; HTTP 201 ne signifie pas que votre récepteur a reçu un push webhook. La plateforme n'envoie pas de message de vérification ou de test lors de la création ou de l'enregistrement d'adresses. Vous devez attendre qu'une activité on-chain correspondante se produise sur les adresses et chaînes surveillées pour vérifier la livraison sur votre récepteur.
Format des événements
Chaque requête POST comporte type: push.events, created_at et data. data contient subscription_id, une chain, complete_through_block et events. Enregistrez la progression par chaîne : un bloc peut s'étendre sur plusieurs messages, de sorte que les numéros de blocs d'événements individuels ne constituent pas un marqueur d'achèvement. Chaque message contient au plus 1 000 événements, 1 Mio et 50 blocs.
{
"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"
}
]
}
]
}
}| Type d'événement | Ce qu'il faut traiter |
|---|---|
native.transfer | Transferts réussis de valeur native au niveau de la transaction principale impliquant une adresse surveillée ; amount est une chaîne décimale entière. Les transferts natifs internes sont exclus. |
token.transfer | Transferts ERC-20, ERC-721 et ERC-1155 impliquant des adresses surveillées ; inspectez standard, token, token_id, amount et batch_index. Les transferts par lots ERC-1155 produisent un événement par élément. |
log | Autres logs désignant une adresse surveillée comme contrat émetteur ou dans les topics 1 à 3 ; inspectez address, topics, data et matched. |
subscription.gap | Une plage de from_block à to_block est indisponible pour la livraison, avec reason: retention_expired ; effectuez un rattrapage avec la Data API ou eth_getLogs. |
chain.reorg | Notification gratuite de réorganisation : les blocs livrés dans from_block–to_block ont été remplacés. Marquez ou ignorez leurs anciens événements par ref, puis conservez les événements canoniques automatiquement relivrés et dédupliquez par id. |
Au sein d'un abonnement, dédupliquez par id d'événement ; d'un abonnement à l'autre, utilisez ref et type. Ignorez les champs et types d'événements inconnus. Vérifiez les faits on-chain avant d'engager toute action financière.
Vérifier les signatures
Les en-têtes sont webhook-id, webhook-timestamp, webhook-signature et bv-subscription-id. Choisissez le secret uniquement parmi les abonnements que vous avez créés ; rejetez les ID inconnus. Vérifiez le HMAC-SHA256 sur webhook-id.webhook-timestamp.raw-body, en utilisant les octets du corps de requête d'origine, avant d'analyser le JSON. La signature est v1,<base64> ; tolérez un décalage d'horodatage d'environ cinq minutes et comparez en temps constant.
Cette fonction Node.js accepte le corps brut sous forme de Buffer, les en-têtes de requête et une table associant les identifiants d'abonnement aux secrets enregistrés :
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);
}Après vérification, analysez le corps, enregistrez durablement le résultat du traitement et renvoyez un code 2xx dans les 10 secondes. L'en-tête de l'identifiant d'abonnement n'est pas fiable tant que la signature n'a pas été vérifiée.
Vérifier votre premier événement
Maintenez l'abonnement en ligne. Une fois la modification d'adresse appliquée, attendez qu'une activité on-chain correspondante se produise et vérifiez que votre récepteur valide et stocke durablement l'événement.
Arrêter l'écoute après vérification
Pour cesser de surveiller des adresses, enregistrez les adresses à retirer dans addresses.json et appelez 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.jsonChaque appel de retrait accepte au maximum 10 000 adresses. Une adresse qui n'est pas actuellement surveillée compte comme unchanged. L'appel renvoie change_version. Dès que applied_version >= change_version, les blocs à partir de ce bloc effectif ne correspondent plus aux adresses retirées. Les événements précédemment appariés (en transit, en cours de nouvelle tentative ou en file d'attente) sont toujours livrés ; les événements déjà livrés ne sont pas retirés.
Pour suspendre temporairement l'écoute sans supprimer la configuration ni les adresses, définissez status sur 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"}'Un abonnement offline arrête l'écoute et la livraison, décharge les adresses de l'index de mise en correspondance et n'engendre aucun frais d'adresse pour toute journée UTC complète passée hors ligne. L'ensemble de la configuration (URL, secret, adresses, chaînes et confirmations) est préservé. Un patch appliquant {"status":"online"} reprend l'écoute à partir du bloc effectif actuel et ne rattrape pas la période hors ligne.
Utilisez JSON Merge Patch sur PATCH /subscriptions/{subscription_id} pour modifier url, key_id, status ou chains : un objet de chaîne l'ajoute ou la met à jour, et null la supprime. Au moins une chaîne doit subsister. Pour supprimer définitivement l'abonnement :
curl --fail-with-body -sS -X DELETE "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"DELETE supprime définitivement l'abonnement, interrompt immédiatement la livraison sur toutes les chaînes et détruit le secret ainsi que les adresses.
Livraison, nouvelles tentatives et rejeu
La livraison s'effectue au moins une fois. La chaîne de chaque abonnement est ordonnée par bloc et position dans le bloc ; les lots en échec bloquent les événements ultérieurs sur cette chaîne. Les différentes chaînes ont une progression indépendante et peuvent émettre des requêtes POST simultanément. Une nouvelle tentative d'un lot identique conserve son webhook-id, mais un lot modifié peut recevoir un nouvel identifiant : dédupliquez les événements, pas les lots.
Tout code 2xx renvoyé dans les 10 secondes accuse réception d'un traitement durable. Les redirections ne sont pas suivies ; les codes 3xx et 410 sont considérés comme des échecs. Après un échec, les nouvelles tentatives suivent des intervalles immédiat, de 5 secondes, 30 secondes, 2 minutes, 10 minutes, 30 minutes et 1 heure, puis toutes les heures. Un code 429 avec Retry-After peut prolonger l'attente jusqu'à une heure. Inspectez condition, last_error et next_attempt_at de chaque chaîne lorsque la livraison s'interrompt. Les conditions sont receiver_failing, insufficient_balance et key_revoked ; cette dernière nécessite de corriger key_id par un patch vers une autre clé de compte active.
Les événements non livrés expirent au-delà de la fenêtre de rétention et produisent subscription.gap. POST /subscriptions/{subscription_id}/replay prend chain et from_block ; consultez replayable_from_block dans GET /push/chains et la progression de l'abonnement. Le rejeu délivre les correspondances existantes et ne peut pas récupérer les événements antérieurs à l'ajout d'une adresse ou d'une chaîne.
chain.reorg vous avertit que des blocs déjà livrés ont été remplacés ; cela n'indique pas un écart de livraison. Les réorganisations de profondeur inférieure à votre nombre de confirmations sont invisibles. Pour les réorganisations affectant des blocs livrés jusqu'à 1 024 blocs de profondeur, les événements canoniques sont automatiquement relivrés avec de nouveaux id. Marquez ou ignorez les événements remplacés par ref, conservez les événements canoniques et dédupliquez par id ; pour les enregistrements de paiement, effectuez le rapprochement par ref et tx_hash. Une réorganisation plus profonde interrompt la chaîne : vérifiez halted dans GET /push/chains ; la relivraison canonique suit après la restauration de la chaîne. Cet événement de contrôle ne fait pas avancer complete_through_block.
Interrogez les événements de données livrés avec GET /subscriptions/{subscription_id}/events?chain=..., en ajoutant facultativement from_block, to_block, limit et page_token. Les lignes d'historique contiennent event, replay_epoch, orphaned et delivered_at ; orphaned: true indique un bloc remplacé ultérieurement. L'accès à l'historique peut renvoyer 402 insufficient_balance (data.reason : balance_exhausted ou free_grant_exhausted), 403 key_cap_exhausted (data.cu_cap), ou 429 rate_limited (key_rate_limit ou free_plan_call_limit). Un code 429 cost_exceeds_burst a pour raison request_exceeds_burst et comporte data.max : augmentez la capacité de rafale (burst) avant de réessayer. Consultez la gestion des erreurs pour les plages non valides et les conseils de nouvelle tentative.
Facturation et exemple
Les poids proviennent de GET /v1/plans. Les événements de données livrés, les requêtes d'historique réussies et les jours-adresses ont des poids distincts ; les appels de gestion autres que l'historique, les événements de contrôle, les livraisons échouées et les nouvelles tentatives automatiques sont gratuits. Chaque événement livré est facturé une seule fois ; le rejeu initié par le client et la relivraison d'événements canoniques entraînent de nouveaux frais de livraison.
La facturation des adresses utilise le plus grand nombre d'adresses de chaque abonnement pendant sa période en ligne de la journée UTC, après déduction de l'allocation d'adresses gratuites du compte partagée entre les abonnements (les abonnements les plus anciens en premier). La même adresse dans deux abonnements compte deux fois ; l'ajout de chaînes modifie les frais d'événements, pas les frais d'adresses. Un abonnement hors ligne pendant toute la journée UTC n'engendre aucun frais d'adresse.
| Utilisation | Unité de facturation | CU |
|---|---|---|
push.address_day | Adresse-jour facturable | 33 |
push.history | Requête d'historique réussie | 25 |
push.log | Événement de données distribué | 150 |
push.native_transfer | Événement de données distribué | 150 |
push.token_transfer | Événement de données distribué | 150 |
Adresses gratuites par compte et par jour UTC : 1000
Quota d'adresses gratuites par compte et par jour UTC, partagé par tous les groupes d'abonnement quel que soit le forfait. Pour chaque groupe, comptabilisez son nombre maximal d'adresses en ligne au cours de cette journée ; allouez le quota par ordre croissant d'identifiant de groupe. La même adresse dans deux groupes compte deux fois ; le nombre de chaînes dans un groupe ne multiplie pas son nombre d'adresses. Un groupe hors ligne ou supprimé pendant toute la journée ne contribue en rien. Pour chaque groupe, le décompte restant après sa part du quota est multiplié par le poids en CU de `push.address_day` dans `method_weights`. Le quota configuré actuel provient de la même politique tarifaire que celle utilisée pour la facturation adresse-jour ; il ne s'agit pas d'une limite de capacité du compte ni d'un quota distinct par groupe.
Exemple : 10 événements native.transfer distribués, 2 requêtes d'historique réussies et 10 adresses-jours facturables coûtent 10 × 150 + 2 × 25 + 10 × 33 = 1880 CU. Les adresses-jours facturables sont décomptées après le quota d'adresses gratuites du compte.
Consultez les règles de facturation et la page Tarifs pour le calcul et la conversion des Compute Units (CU).
Ressources connexes
- Comparez les événements pris en charge, la couverture des chaînes et les tarifs dans la présentation de l'API Webhook blockchain.
Dernière mise à jour :
RPC personnalisé du wallet
Ajoutez une URL RPC BlockVectra à MetaMask ou Rabby. Trouvez les Chain ID et symboles natifs, configurez une API key dans le chemin et gérez une clé dédiée au wallet.
Webhook vs WebSocket
Comparez les notifications d'adresses, les abonnements socket et le polling borné selon la prise en charge des chaînes, la récupération, les prérequis du récepteur et la facturation.