Les modèles de webhook dont les développeurs parlent le plus.
Un gestionnaire de webhook fiable effectue quatre actions dans l'ordre : vérifier la signature contre le corps brut (webhooks.verify dans Node, le HMAC documenté ailleurs), dédupliquer par ID d'événement, mettre le travail en file d'attente dans une file d'attente en arrière-plan et renvoyer 200. Les travaux de longue durée se déroulent dans le travailleur, avec des réessais et de l'idempotence au niveau de la file d'attente. Comme il n'y a pas d'événement d'échec, un balayage programmé expire les tâches bloquées en attente de paiement et les réconcilie.
Avant de commencer.
- Un profil d'agent fonctionnel et le secret de signature de votre tableau de bord (Paramètres - Webhooks).
- Un framework web avec accès au corps brut - Express avec
express.raw, FastAPI, Flask, etc. Le middleware d'analyse automatique JSON casse la vérification de la signature. - Une file d'attente de tâches : BullMQ (Node) ou Celery/arq (Python). Le webhook renvoie 200 rapidement et la file d'attente effectue le travail lent.
- Une base de données avec une primitive d'upsert (Postgres fonctionne ; Redis SET NX fonctionne également pour la déduplication à courte durée de vie).
- Un point de terminaison HTTPS public - en développement, ngrok ou un aperçu de déploiement. L'expéditeur ne livrera pas à des URL privées.
Vérifier la signature.
The signature is HMAC-SHA256 over {t}.{rawBody} with your webhook secret, hex-encoded, in the X-Blockchain0x-Signature header (t=<unix>,v1=<hex>), inside a 5-minute replay window. In Node, webhooks.verify from @blockchain0x/node does it and returns a discriminated union; in other languages compute the same HMAC and compare in constant time. Raw-body access matters: if the bytes you sign locally differ from the bytes that arrived, it fails.
import express from "express";
import { webhooks } from "@blockchain0x/node";
const app = express();
// Raw body so the HMAC matches the exact bytes on the wire.
app.use(express.raw({ type: "application/json" }));
app.post("/webhooks/payment", (req, res) => {
const result = webhooks.verify({
headers: req.headers,
rawBody: req.body, // Buffer, raw bytes
secret: process.env.BLOCKCHAIN0X_WEBHOOK_SECRET!,
});
// Discriminated union: branch on ok, no try/catch.
if (!result.ok) return res.status(400).json({ code: result.code });
// result.eventType / result.eventId are now set.
handleEvent(result);
res.status(200).send("ok");
});import hmac, hashlib, os, time
from flask import request
SECRET = os.environ["BLOCKCHAIN0X_WEBHOOK_SECRET"].encode()
# In Node, webhooks.verify does this. In Python, verify by hand against the
# documented algorithm: HMAC-SHA256 over "{t}.{rawBody}", 300s replay window.
def verify_signature(raw_body: bytes) -> bool:
sig = request.headers.get("X-Blockchain0x-Signature", "")
ts = request.headers.get("X-Blockchain0x-Timestamp", "")
parts = dict(p.split("=", 1) for p in sig.split(",") if "=" in p)
t, v1 = parts.get("t", ts), parts.get("v1", sig)
want = hmac.new(SECRET, t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, v1) and abs(time.time() - int(t)) <= 300Rendez le gestionnaire idempotent.
Webhooks retry on any non-2xx response, and the same event will arrive multiple times under load even when nothing has gone wrong. Dedupe on the event's id using a database upsert. If the row already exists, skip; if it does not, insert and proceed. Postgres makes this a single statement.
// Pseudocode for a Postgres-backed dedupe table. Replace with your DB of choice.
async function processEventOnce(eventId: string, body: object) {
// INSERT ... ON CONFLICT DO NOTHING returns rowCount === 0 on duplicate.
const inserted = await db.query(
"INSERT INTO webhook_events(id) VALUES ($1) ON CONFLICT DO NOTHING",
[eventId],
);
if (inserted.rowCount === 0) return; // Already processed.
await handleEvent(body);
}async def process_event_once(event_id: str, body: dict):
# INSERT ... ON CONFLICT DO NOTHING returns 0 rows on duplicate.
inserted = await db.execute(
"INSERT INTO webhook_events (id) VALUES ($1) ON CONFLICT DO NOTHING",
event_id,
)
if inserted == "INSERT 0 0": # asyncpg-style status
return # Already processed.
await handle_event(body)Mettez en file d'attente et renvoyez 200 rapidement.
Le point de terminaison du webhook doit répondre en moins d'une seconde. Tout ce qui est plus lent invite aux délais d'attente et aux nouvelles tentatives. Le modèle est : vérifier, mettre en file d'attente, répondre. La file d'attente exécute la livraison réelle dans un travailleur avec des nouvelles tentatives, un retour en arrière exponentiel et sa propre idempotence. BullMQ et Celery prennent tous deux en charge l'ID par travail, ce qui empêche la réinsertion accidentelle du même événement.
// Express handler: verify, enqueue, return 200 fast.
import { Queue } from "bullmq";
import { webhooks } from "@blockchain0x/node";
const paymentQueue = new Queue("payments");
app.post(
"/webhooks/payment",
express.raw({ type: "application/json" }),
async (req, res) => {
const result = webhooks.verify({
headers: req.headers,
rawBody: req.body,
secret: process.env.BLOCKCHAIN0X_WEBHOOK_SECRET!,
});
if (!result.ok) return res.status(400).json({ code: result.code });
await paymentQueue.add(result.eventType, { raw: req.body.toString() }, {
jobId: result.eventId, // Idempotency key.
removeOnComplete: true,
attempts: 5,
backoff: { type: "exponential", delay: 1000 },
});
res.status(200).send("ok");
},
);
// Worker file:
import { Worker } from "bullmq";
new Worker("payments", async (job) => {
await handleEvent(job.data);
});# Flask handler enqueues to Celery (or arq) and returns 200 quickly.
from celery import Celery
from flask import request
celery = Celery("payments", broker=os.environ["REDIS_URL"])
@celery.task(bind=True, max_retries=5)
def handle_payment_event(self, event_type, raw):
try:
process_event_once(event_type, raw)
except Exception as exc:
raise self.retry(exc=exc, countdown=2 ** self.request.retries)
@app.post("/webhooks/payment")
def webhook():
raw = request.get_data()
if not verify_signature(raw):
abort(401)
event_id = request.headers.get("X-Blockchain0x-Event-Id", "")
event_type = request.headers.get("X-Blockchain0x-Event-Type", "")
handle_payment_event.apply_async(args=[event_type, raw.decode()], task_id=event_id)
return "ok", 200arq suit la même structure du côté Python - enregistrez la tâche avec un identifiant de tâche déterministe et laissez la file d'attente gérer les réessais. La contrainte clé est que l'enfilement lui-même doit être rapide (un seul aller-retour à Redis) ; ne bloquez jamais le webhook sur des appels distants.
Gérez un paiement qui n'atterrit jamais.
Il n'y a pas de webhook d'échec - si un acheteur abandonne, aucun événement n'arrive, et l'agent reste bloqué dans 'awaiting_payment'. Donc, détectez-le vous-même : effectuez un balayage programmé sur les travaux qui ont attendu trop longtemps, réconciliez avec la chaîne avec transactions.get au cas où cela se serait réellement réglé, puis libérez les ressources retenues, déplacez le travail vers un état impayé terminal, et (si approprié) présentez le résultat à l'utilisateur.
async function sweepStaleAwaitingPayment() {
for (const job of await findJobsAwaitingPaymentOlderThan("1h")) {
// Reconcile against the chain before giving up.
const tx = job.txHash ? await client.transactions.get(job.txHash) : null;
if (tx) { markJobPaid(job.id); continue; } // It actually settled.
// 1. Release any held resources tied to the job.
releaseHeldResources(job.id);
// 2. Move it out of 'awaiting_payment' into a terminal 'unpaid' state.
markJobUnpaid(job.id);
// 3. (Optional) Notify the user, with a fresh payment link.
notifyUser(job.userId, { template: "agent_payment_unpaid", jobId: job.id });
}
}# Run on a schedule - there is no failure webhook to wait for.
def sweep_stale_awaiting_payment():
for job in find_jobs_awaiting_payment_older_than("1h"):
tx = client.transactions.get(job["tx_hash"]) if job.get("tx_hash") else None
if tx:
mark_job_paid(job["id"]) # It actually settled.
continue
release_held_resources(job["id"])
mark_job_unpaid(job["id"])
notify_user(job["user_id"], template="agent_payment_unpaid", job_id=job["id"])Cinq erreurs qui font tomber ou dupliquer des événements.
Analyse du corps avant de vérifier la signature
HMAC doit être calculé sur les octets bruts que l'expéditeur a signés. Si votre framework analyse automatiquement le JSON avant que votre gestionnaire ne s'exécute, les octets que vous signez localement ne correspondront pas aux octets que l'expéditeur a signés (espaces différents, ordre des clés, encodage) et chaque signature semblera invalide. Configurez la route pour recevoir le corps brut (Express : express.raw, Flask : request.get_data), vérifiez d'abord, puis analysez.
Effectuer le travail réel à l'intérieur du gestionnaire de webhook
Les webhooks ont des politiques de réessai agressives. Si votre gestionnaire prend 30 secondes pour livrer le travail, le délai d'attente de l'expéditeur se déclenche et le webhook est renvoyé - maintenant vous avez deux livraisons en cours pour le même paiement. Toujours : vérifiez, mettez en file d'attente, retournez 2xx. Le travail réel s'exécute dans un travailleur en arrière-plan qui peut prendre tout le temps nécessaire.
Utilisation du statut HTTP pour communiquer la logique métier
Si votre gestionnaire retourne un 4xx lorsque l'utilisateur n'existe plus dans votre système, l'expéditeur considère cela comme une 'demande invalide' et cesse de réessayer. S'il retourne un 5xx pour la même condition, l'expéditeur réessaie indéfiniment et votre file d'attente se remplit. Retournez 200 une fois que vous avez en toute sécurité persisté l'événement (ou reconnu comme un duplicata) ; utilisez la logique de file d'attente, pas le statut HTTP, pour exprimer des décisions commerciales.
Idempotence sur un hachage de charge utile au lieu de l'ID d'événement
Deux événements différents concernant le même agent (un payment.received et un payment.sent ultérieur) ont des corps différents et nécessitent légitimement un traitement séparé. Si votre dé-duplication est basée sur un hachage de corps, vous pouvez en supprimer un. Dé-duplication sur le X-Blockchain0x-Event-Id (unique par livraison), et laissez le type d'événement déterminer ce que fait votre gestionnaire.
En attente d'un événement de confirmation séparé
Les événements expédiés sont payment.received, payment.sent, wallet.deployed, et webhook.test - il n'y a pas d'événement de confirmation séparé. payment.received se déclenche lorsque le transfert est dans un bloc, ce qui est votre signal pour la plupart des travaux. Pour quelque chose de coûteux ou d'irréversible, interrogez transactions.get et appliquez votre propre seuil de confirmation ; n'attendez pas un événement qui n'existe pas.
Une fois que les webhooks sont à l'épreuve des balles.
Les webhooks sont la partie difficile. Avec les quatre modèles ci-dessus en place, le travail restant est principalement opérationnel : un environnement de test qui exerce les chemins d'échec, des contrôles de dépenses afin qu'un agent en amont ne submerge pas votre gestionnaire, et un examen final de la sécurité.
Testez les paiements d'agents sans argent réel
Mettre en place des contrôles de dépenses d'agent qui survivent à l'injection d'invite
Sécurisez votre portefeuille d'agent avant de passer en direct
Référence complète à docs.blockchain0x.com. Glossaire des webhooks : mandat de paiement. Surface produit : API de paiement.