Zum Hauptinhalt springen
LernenLeitfädenVerarbeiten Sie Agentenzahlungen mit Webhooks
LEITFADEN

Die Webhook-Muster, nach denen Entwickler am häufigsten fragen.

15 Minuten
KURZE ANTWORT

Ein zuverlässiger Webhook-Handler führt vier Dinge in der Reihenfolge aus: Überprüfen Sie die Signatur gegen den Rohkörper (webhooks.verify in Node, das dokumentierte HMAC an anderer Stelle), deduplizieren Sie nach Ereignis-ID, stellen Sie die Arbeit in eine Hintergrundwarteschlange und geben Sie 200 zurück. Lang laufende Arbeiten erfolgen im Worker, mit Wiederholungen und Idempotenz auf der Warteschicht. Da es kein Fehlerereignis gibt, läuft eine geplante Überprüfung Jobs ab, die auf eine Zahlung warten, und gleicht sie aus.

VORAUSSETZUNGEN

Bevor Sie beginnen.

  • Ein funktionierendes Agentenprofil und das Signaturgeheimnis aus Ihrem Dashboard (Einstellungen - Webhooks).
  • Ein Web-Framework mit Zugriff auf den Rohkörper - Express mit express.raw, FastAPI, Flask usw. Automatisches Parsen von JSON-Middleware bricht die Signaturverifizierung.
  • Eine Job-Warteschlange: BullMQ (Node) oder Celery/arq (Python). Der Webhook gibt schnell 200 zurück und die Warteschlange erledigt die langsame Arbeit.
  • Eine Datenbank mit einer Upsert-Primitiv (Postgres funktioniert; Redis SET NX funktioniert auch für kurzlebige Deduplikation).
  • Ein öffentlicher HTTPS-Endpunkt - in Entwicklung, ngrok oder eine Bereitstellungsvorschau. Der Sender wird nicht an private URLs liefern.
SCHRITT 1 VON 4

Die Signatur verifizieren.

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
SCHRITT 2 VON 4

Machen Sie den 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)
SCHRITT 3 VON 4

In die Warteschlange einfügen und schnell 200 zurückgeben.

Der Webhook-Endpunkt sollte innerhalb einer Sekunde antworten. Alles, was langsamer ist, lädt zu Zeitüberschreitungen und Wiederholungen ein. Das Muster ist: überprüfen, in die Warteschlange stellen, antworten. Die Warteschlange führt die tatsächliche Lieferung in einem Worker mit Wiederholungen, exponentiellem Backoff und eigener Idempotenz aus. BullMQ und Celery unterstützen beide eine Job-ID pro Job, die eine versehentliche erneute Einreihung desselben Ereignisses verhindert.

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 folgt derselben Struktur auf der Python-Seite - registrieren Sie die Aufgabe mit einer deterministischen Job-ID und lassen Sie die Warteschlange die Wiederholungen verwalten. Die Hauptbeschränkung besteht darin, dass das Einreihen selbst schnell sein muss (eine einzige Hin- und Rückfahrt zu Redis); blockieren Sie niemals den Webhook bei Remote-Aufrufen.

SCHRITT 4 VON 4

Verarbeiten Sie eine Zahlung, die niemals ankommt.

Es gibt kein Fehler-Webhook - wenn ein Käufer aufgibt, kommt kein Ereignis an, und der Agent bleibt in 'awaiting_payment' stecken. Erkennen Sie es selbst: Führen Sie einen geplanten Sweep über Jobs durch, die zu lange gewartet haben, versöhnen Sie sich mit der Kette mit transactions.get, falls es tatsächlich abgeschlossen wurde, geben Sie dann gehaltene Ressourcen frei, verschieben Sie den Job in einen terminalen unbezahlten Zustand und (falls angemessen) zeigen Sie das Ergebnis dem Benutzer an.

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"])
HÄUFIGE FALLSTRICK

Fünf Fehler, die Ereignisse verwerfen oder duplizieren.

Den Body analysieren, bevor die Signatur überprüft wird

HMAC muss über die Rohbytes berechnet werden, die der Absender signiert hat. Wenn Ihr Framework JSON automatisch analysiert, bevor Ihr Handler ausgeführt wird, stimmen die Bytes, die Sie lokal signieren, nicht mit den Bytes überein, die der Absender signiert hat (unterschiedliche Leerzeichen, Schlüsselreihenfolge, Kodierung), und jede Signatur wird ungültig aussehen. Konfigurieren Sie die Route, um den Rohkörper zu empfangen (Express: express.raw, Flask: request.get_data), zuerst verifizieren, dann analysieren.

Die eigentliche Arbeit innerhalb des Webhook-Handlers erledigen

Webhooks haben aggressive Retry-Politiken. Wenn Ihr Handler 30 Sekunden benötigt, um die Arbeit zu liefern, wird das Timeout des Senders ausgelöst und der Webhook wird erneut gesendet - jetzt haben Sie zwei Zustellungen für dieselbe Zahlung in der Luft. Immer: überprüfen, in die Warteschlange stellen, 2xx zurückgeben. Die tatsächliche Arbeit läuft in einem Hintergrundarbeiter, der so lange dauern kann, wie er benötigt.

HTTP-Status verwenden, um Geschäftslogik zu kommunizieren

Wenn Ihr Handler einen 4xx zurückgibt, wenn der Benutzer nicht mehr in Ihrem System existiert, behandelt der Sender dies als 'ungültige Anfrage' und hört auf, es erneut zu versuchen. Wenn er für denselben Zustand 5xx zurückgibt, versucht der Sender es für immer erneut und Ihre Warteschlange füllt sich. Geben Sie 200 zurück, sobald Sie das Ereignis sicher gespeichert haben (oder es als Duplikat erkannt haben); verwenden Sie Warteschlangenlogik, nicht HTTP-Status, um Geschäftsentscheidungen auszudrücken.

Idempotenz auf einem Payload-Hash statt auf der Ereignis-ID

Zwei verschiedene Ereignisse über denselben Agenten (ein payment.received und ein späteres payment.sent) haben unterschiedliche Körper und benötigen berechtigterweise eine separate Verarbeitung. Wenn Ihre Dedupe auf einem Körper-Hash basiert, können Sie eines von ihnen fallen lassen. Dedupe auf der X-Blockchain0x-Event-Id (einzigartig pro Lieferung) und lassen Sie den Ereignistyp bestimmen, was Ihr Handler tut.

Erwarte ein separates Bestätigungsereignis

Die gelieferten Ereignisse sind payment.received, payment.sent, wallet.deployed und webhook.test - es gibt kein separates Bestätigungsereignis. payment.received wird ausgelöst, wenn die Überweisung in einem Block ist, was Ihr Signal für die meisten Arbeiten ist. Für etwas Teures oder Irreversibles, rufen Sie transactions.get ab und wenden Sie Ihre eigene Bestätigungsgrenze an; warten Sie nicht auf ein Ereignis, das nicht existiert.

NÄCHSTE SCHRITTE

Sobald Webhooks wasserdicht sind.

Webhooks sind der schwierige Teil. Mit den vier oben genannten Mustern ist die verbleibende Arbeit größtenteils operationell: eine Testumgebung, die die Fehlerpfade testet, Ausgabensteuerungen, damit ein upstream-Agent Ihren Handler nicht überflutet, und eine abschließende Sicherheitsüberprüfung.

Vollständige Referenz unter docs.blockchain0x.com. Webhook-Glossar: Zahlungsmandat. Produktoberfläche: Payment API.

Letzte Überprüfung: 2026-05-15. Veröffentlicht unter CC BY 4.0.

Webhooks, denen Sie unter Last vertrauen können.

Unterzeichnet, wiederholt, idempotent. Kostenlos starten.