Ga naar hoofdinhoud
LerenGidsenBeheer agentbetalingen met webhooks
GIDS

De webhook-patronen waar ontwikkelaars het meest naar vragen.

15 minuten
KORTE ANTWOORD

Een betrouwbare webhook-handler doet vier dingen in volgorde: verifieer de handtekening tegen de raw body (webhooks.verify in Node, de gedocumenteerde HMAC elders), dedupe op basis van event id, zet het werk in een achtergrondwachtrij, en retourneert 200. Langdurig werk gebeurt in de worker, met herhalingen en idempotentie op de wachtrij-laag. Omdat er geen foutgebeurtenis is, time-out een geplande sweep taken die vastzitten in afwachting van betaling en verzoent ze.

VOORWAARDEN

Voordat je begint.

  • Een werkend agentprofiel en het ondertekeningsgeheim van uw dashboard (Instellingen - Webhooks).
  • Een webframework met toegang tot raw-body - Express met express.raw, FastAPI, Flask, enz. Auto-parsing JSON-middleware breekt handtekeningverificatie.
  • Een takenwachtrij: BullMQ (Node) of Celery/arq (Python). De webhook retourneert snel 200 en de wachtrij doet het langzame werk.
  • Een database met een upsert-primitief (Postgres werkt; Redis SET NX werkt ook voor kortlevende dedupe).
  • Een openbaar HTTPS-eindpunt - in ontwikkeling, ngrok of een implementatiepreview. De afzender levert niet aan privé-URL's.
STAP 1 VAN 4

Verifieer de handtekening.

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
STAP 2 VAN 4

Maak de handler 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.

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)
STAP 3 VAN 4

Plaats in de wachtrij en retourneer 200 snel.

De webhook-eindpunt moet binnen een seconde reageren. Alles langzamer nodigt uit tot time-outs en herhalingen. Het patroon is: verifiëren, in de wachtrij zetten, reageren. De wachtrij draait de daadwerkelijke levering in een werker met herhalingen, exponentiële terugval en zijn eigen idempotentie. BullMQ en Celery ondersteunen beide per-taak-ID, wat voorkomt dat hetzelfde evenement per ongeluk opnieuw in de wachtrij wordt gezet.

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 volgt dezelfde structuur aan de Python-kant - registreer de taak met een deterministische job-id en laat de queue de herhalingen afhandelen. De belangrijkste beperking is dat de enqueue zelf snel moet zijn (een enkele ronde naar Redis); blokkeer de webhook nooit bij externe oproepen.

STAP 4 VAN 4

Verwerk een betaling die nooit aankomt.

Er is geen fout-webhook - als een koper verlaat, arriveert er geen evenement, en blijft de agent vastzitten in 'wachten_op_betaling'. Dus detecteer het zelf: voer een geplande sweep uit over banen die te lang hebben gewacht, reconcile tegen de keten met transactions.get voor het geval het daadwerkelijk is vereffend, geef dan vastgehouden middelen vrij, verplaats de baan naar een terminal onbetaalde staat, en (indien van toepassing) toon de uitkomst aan de gebruiker.

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"])
GEMEENSCHAPPELIJKE VALKUILEN

Vijf fouten die evenementen laten vallen of dupliceren.

Het lichaam parseren voordat de handtekening wordt geverifieerd

HMAC moet worden berekend over de ruwe bytes die de afzender heeft ondertekend. Als je framework automatisch JSON ontleedt voordat je handler draait, komen de bytes die je lokaal ondertekent niet overeen met de bytes die de afzender heeft ondertekend (verschillende spaties, sleutelvolgorde, codering) en elke handtekening zal ongeldig lijken. Configureer de route om de ruwe body te ontvangen (Express: express.raw, Flask: request.get_data), verifieer eerst, en parse dan.

Het daadwerkelijke werk doen binnen de webhook-handler

Webhooks hebben agressieve herhaalbeleid. Als je handler 30 seconden nodig heeft om het werk te leveren, wordt de timeout van de afzender geactiveerd en wordt de webhook opnieuw verzonden - nu heb je twee leveringen in de lucht voor dezelfde betaling. Altijd: verifiëren, in de wachtrij plaatsen, 2xx retourneren. Het daadwerkelijke werk draait in een achtergrondwerker die zoveel tijd kan nemen als nodig is.

HTTP-status gebruiken om bedrijfslogica te communiceren

Als je handler een 4xx retourneert wanneer de gebruiker niet meer in je systeem bestaat, beschouwt de afzender dat als 'ongeldig verzoek' en stopt met opnieuw proberen. Als het 5xx retourneert voor dezelfde toestand, probeert de afzender voor altijd opnieuw en vult je wachtrij zich. Retourneer 200 zodra je het evenement veilig hebt opgeslagen (of het als een duplicaat hebt herkend); gebruik wachtrijlogica, niet HTTP-status, om zakelijke beslissingen uit te drukken.

Idempotentie op een payload-hash in plaats van de evenement-ID

Twee verschillende gebeurtenissen over dezelfde agent (een payment.received en een latere payment.sent) hebben verschillende bodies en vereisen legitiem aparte verwerking. Als je dedupe op een body-hash is, kun je er een van hen laten vallen. Dedupe op de X-Blockchain0x-Event-Id (uniek per levering), en laat het type gebeurtenis bepalen wat je handler doet.

Verwacht een afzonderlijk bevestigingsevenement

De verzonden evenementen zijn payment.received, payment.sent, wallet.deployed, en webhook.test - er is geen apart bevestigingsevenement. payment.received wordt geactiveerd wanneer de overdracht in een blok is, wat jouw signaal is voor het meeste werk. Voor iets duur of onomkeerbaar, poll transactions.get en pas jouw eigen bevestigingsdrempel toe; wacht niet op een evenement dat niet bestaat.

VOLGENDE STAPPEN

Zodra webhooks waterdicht zijn.

Webhooks zijn het moeilijke deel. Met de vier bovenstaande patronen op hun plaats, is het resterende werk voornamelijk operationeel: een testomgeving die de foutpaden oefent, bestedingscontroles zodat een upstream-agent uw handler niet overbelast, en een laatste beveiligingsreview.

Volledige referentie op docs.blockchain0x.com. Webhook woordenlijst: betalingsmandaat. Productoppervlak: Betalings-API.

Laatst beoordeeld: 2026-05-15. Gepubliceerd onder CC BY 4.0.

Webhooks waarop u kunt vertrouwen onder belasting.

Getekend, opnieuw geprobeerd, idempotent. Gratis om te beginnen.