Salta al contenuto principale
ImparaGuideGestisci i pagamenti degli agenti con i webhook
GUIDA

I modelli di webhook di cui i sviluppatori chiedono di più.

15 minuti
RISPOSTA BREVE

Un gestore webhook affidabile esegue quattro operazioni in ordine: verifica la firma rispetto al corpo raw (webhooks.verify in Node, l'HMAC documentato altrove), deduplica per id evento, mette in coda il lavoro a una coda di lavoro in background e restituisce 200. Il lavoro a lungo termine avviene nel worker, con ripetizioni e idempotenza a livello di coda. Poiché non c'è un evento di fallimento, una pulizia programmata scade i lavori bloccati in attesa di pagamento e li riconcilia.

PREREQUISITI

Prima di iniziare.

  • Un profilo agente funzionante e il segreto di firma dal tuo dashboard (Impostazioni - Webhooks).
  • Un framework web con accesso al corpo grezzo - Express con express.raw, FastAPI, Flask, ecc. Il middleware di parsing automatico JSON interrompe la verifica della firma.
  • Una coda di lavoro: BullMQ (Node) o Celery/arq (Python). Il webhook restituisce 200 rapidamente e la coda esegue il lavoro lento.
  • Un database con una primitiva upsert (Postgres funziona; Redis SET NX funziona anche per deduplica a breve termine).
  • Un endpoint HTTPS pubblico - in sviluppo, ngrok o un'anteprima di distribuzione. Il mittente non consegnerà a URL privati.
PASSO 1 DI 4

Verifica la firma.

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.

TypeScript
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");
});
Python
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)) <= 300
PASSO 2 DI 4

Rendi l'handler idempotente.

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.

TypeScript
// 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);
}
Python
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)
PASSO 3 DI 4

Metti in coda e restituisci 200 velocemente.

L'endpoint del webhook dovrebbe rispondere entro un secondo. Qualsiasi cosa più lenta invita a timeout e ripetizioni. Il modello è: verifica, metti in coda, rispondi. La coda esegue la consegna effettiva in un lavoratore con ripetizioni, backoff esponenziale e la propria idempotenza. BullMQ e Celery supportano entrambi ID per lavoro, il che previene la ri-inserimento accidentale dello stesso evento.

TypeScript (BullMQ)
// 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);
});
Python (Celery)
# 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", 200

arq segue la stessa forma sul lato Python - registra il compito con un id di lavoro deterministico e lascia che la coda gestisca i tentativi. La chiave restrizione è che l'inserimento stesso deve essere veloce (un solo round trip a Redis); non bloccare mai il webhook su chiamate remote.

PASSO 4 DI 4

Gestisci un pagamento che non arriva mai.

Non esiste un webhook di fallimento - se un acquirente abbandona, non arriva alcun evento, e l'agente rimane bloccato in 'awaiting_payment'. Quindi rilevalo tu stesso: esegui una pulizia programmata sui lavori che hanno atteso troppo a lungo, riconcilia contro la catena con transactions.get nel caso si sia effettivamente concluso, quindi rilascia le risorse trattenute, sposta il lavoro in uno stato terminale non pagato e (se appropriato) mostra il risultato all'utente.

TypeScript
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 });
  }
}
Python
# 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"])
ERRORI COMUNI

Cinque errori che eliminano o duplicano eventi.

Analisi del corpo prima di verificare la firma

L'HMAC deve essere calcolato sui byte grezzi firmati dal mittente. Se il tuo framework analizza automaticamente il JSON prima che il tuo gestore venga eseguito, i byte che firmi localmente non corrisponderanno ai byte firmati dal mittente (spazi bianchi diversi, ordine delle chiavi, codifica) e ogni firma sembrerà non valida. Configura la rotta per ricevere il corpo grezzo (Express: express.raw, Flask: request.get_data), verifica prima, poi analizza.

Fare il lavoro reale all'interno del gestore del webhook

I webhooks hanno politiche di retry aggressive. Se il tuo gestore impiega 30 secondi per consegnare il lavoro, il timeout del mittente scatta e il webhook viene reinviato - ora hai due consegne in volo per lo stesso pagamento. Sempre: verifica, accoda, restituisci 2xx. Il lavoro effettivo viene eseguito in un worker in background che può impiegare tutto il tempo necessario.

Utilizzare lo stato HTTP per comunicare la logica aziendale

Se il tuo gestore restituisce un 4xx quando l'utente non esiste più nel tuo sistema, il mittente lo tratta come 'richiesta non valida' e smette di riprovare. Se restituisce un 5xx per la stessa condizione, il mittente riprova all'infinito e la tua coda si riempie. Restituisci 200 una volta che hai salvato in modo sicuro l'evento (o riconosciuto come duplicato); utilizza la logica della coda, non lo stato HTTP, per esprimere decisioni aziendali.

Idempotenza su un hash del payload invece dell'ID dell'evento

Due eventi diversi riguardanti lo stesso agente (un pagamento ricevuto e un pagamento inviato successivamente) hanno corpi diversi e necessitano legittimamente di un'elaborazione separata. Se la tua deduplica si basa su un hash del corpo, puoi scartarne uno. Deduplica sull'X-Blockchain0x-Event-Id (unico per consegna) e lascia che il tipo di evento guidi ciò che fa il tuo gestore.

Aspettando un evento di conferma separato

Gli eventi forniti sono payment.received, payment.sent, wallet.deployed e webhook.test - non esiste un evento di conferma separato. payment.received si attiva quando il trasferimento è in un blocco, che è il tuo segnale per la maggior parte del lavoro. Per qualcosa di costoso o irreversibile, interroga transactions.get e applica la tua soglia di conferma; non aspettare un evento che non esiste.

PROSSIMI PASSI

Una volta che i webhook sono a prova di proiettile.

I webhook sono la parte difficile. Con i quattro schemi sopra in atto, il lavoro rimanente è per lo più operativo: un ambiente di test che esercita i percorsi di errore, controlli di spesa affinché un agente upstream non inondi il tuo gestore, e una revisione finale della sicurezza.

Riferimento completo su docs.blockchain0x.com. Glossario dei webhook: mandato di pagamento. Superficie di prodotto: Payment API.

Ultima revisione: 2026-05-15. Pubblicato sotto CC BY 4.0.

Webhook di cui puoi fidarti sotto carico.

Firmato, ripetuto, idempotente. Gratuito per iniziare.