Advanced (Aktualisiert: 3.6.2026)

Webhooks mit Claude Code implementieren: Signaturen, Idempotenz und Retries

Produktionsreife Webhooks mit Claude Code: raw body, Signaturprüfung, Idempotenz, Retries, Tests und Runbook.

Webhooks mit Claude Code implementieren: Signaturen, Idempotenz und Retries

Ein Webhook informiert deine Anwendung per HTTP, sobald in einem externen Dienst ein Ereignis passiert. Erfolgreiche Zahlungen, GitHub-Pushes, Formularübermittlungen, Abo-Änderungen, CRM-Updates und SaaS-Statuswechsel sind typische Beispiele.

In Produktion ist ein Webhook aber kein kleiner JSON-Endpunkt. Du brauchst raw body handling, provider-spezifische Signaturprüfung, Idempotenz, einen Event Store, eine Retry Queue, Tests, Replay Tooling und ein Runbook für den Betrieb. Wenn du Claude Code nur bittest, “einen Webhook zu bauen”, entsteht leicht Demo-Code, der JSON vor der Signaturprüfung parst oder doppelte Zustellungen zweimal verarbeitet.

Für den größeren API-Kontext passen API-Entwicklung mit Claude Code, Secrets Management, Security Best Practices und Queue-Systeme.

Provider-Vertrag

PunktGitHub-BeispielStripe-BeispielImplementierung
EndpointPOST /webhooks/githubPOST /webhooks/stripeRouting trennen
Event-IDX-GitHub-Deliveryevent.idIdempotenzschlüssel
Event-TypX-GitHub-Eventevent.typeHandler-Auswahl
Signatur-HeaderX-Hub-Signature-256Stripe-SignatureEchtheitsprüfung
Prüfeingaberaw bodyraw bodyParser-Reihenfolge
Erfolgsantwortschnell 2xxschnell 2xxspeichern und enqueuen

Nutze offizielle Quellen: GitHub Webhooks, GitHub delivery validation, Stripe Webhooks, Stripe webhook signatures, Express express.raw und Claude Code best practices.

flowchart LR
  A["Provider<br/>GitHub / Stripe"] --> B["Webhook endpoint<br/>raw body"]
  B --> C["Signature verification"]
  C --> D["Event store"]
  D --> E["Idempotency check"]
  E --> F["Retry queue"]
  F --> G["Domain handler"]
  D --> H["Replay tool"]

Prompt für Claude Code

Implementiere einen GitHub-Webhook-Receiver in Express + TypeScript.

Anforderungen:
- POST /webhooks/github hinzufügen
- Auf Webhook-Routen raw body mit express.raw({ type: "*/*" }) erhalten
- JSON erst nach der Signaturprüfung parsen
- X-Hub-Signature-256 mit HMAC SHA-256 prüfen
- X-GitHub-Delivery als Idempotenzschlüssel verwenden
- Jedes akzeptierte Event vor der Verarbeitung im Event Store speichern
- Dieselbe delivery id nicht zweimal verarbeiten
- Schnell 202 zurückgeben und schwere Arbeit über eine Retry Queue ausführen
- node:test für gültige Signatur, ungültige Signatur und Duplikate ergänzen
- Replay Script für gespeicherte Deliveries hinzufügen
- Secret aus WEBHOOK_SECRET lesen

Ausführbarer Receiver

npm init -y
npm install express
npm install -D typescript tsx @types/node @types/express

src/server.ts:

import crypto from "node:crypto";
import express from "express";

type EventStatus = "queued" | "processing" | "processed" | "failed";

type WebhookEvent = {
  id: string;
  provider: "github";
  type: string;
  headers: Record<string, string>;
  rawBody: Buffer;
  payload: unknown;
  receivedAt: string;
  status: EventStatus;
  attempts: number;
  lastError?: string;
};

export const app = express();
export const eventStore = new Map<string, WebhookEvent>();
export const processedEvents = new Set<string>();
export const retryQueue: string[] = [];

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET ?? "dev-secret-change-me";

app.use("/webhooks", express.raw({ type: "*/*", limit: "1mb" }));
app.use(express.json());

function firstHeader(value: string | string[] | undefined): string | undefined {
  return Array.isArray(value) ? value[0] : value;
}

function safeCompare(leftValue: string, rightValue: string): boolean {
  const left = Buffer.from(leftValue);
  const right = Buffer.from(rightValue);
  return left.length === right.length && crypto.timingSafeEqual(left, right);
}

export function signGitHubBody(
  rawBody: Buffer | string,
  secret = WEBHOOK_SECRET
): string {
  return (
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
  );
}

export function verifyGitHubSignature(
  rawBody: Buffer,
  signatureHeader: string | undefined,
  secret = WEBHOOK_SECRET
): boolean {
  if (!signatureHeader?.startsWith("sha256=")) return false;
  return safeCompare(signGitHubBody(rawBody, secret), signatureHeader);
}

function headersForStorage(req: express.Request): Record<string, string> {
  const result: Record<string, string> = {};
  for (const [key, value] of Object.entries(req.headers)) {
    if (typeof value === "string") result[key] = value;
  }
  return result;
}

app.post("/webhooks/github", (req, res) => {
  const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.from("");
  const signature = firstHeader(req.headers["x-hub-signature-256"]);
  const deliveryId = firstHeader(req.headers["x-github-delivery"]);
  const eventType = firstHeader(req.headers["x-github-event"]) ?? "unknown";

  if (!verifyGitHubSignature(rawBody, signature)) {
    return res.status(401).json({ error: "invalid_signature" });
  }

  if (!deliveryId) {
    return res.status(400).json({ error: "missing_delivery_id" });
  }

  const id = `github:${deliveryId}`;
  if (processedEvents.has(id) || eventStore.has(id)) {
    return res.status(202).json({ id, status: "duplicate" });
  }

  let payload: unknown;
  try {
    payload = JSON.parse(rawBody.toString("utf8"));
  } catch {
    return res.status(400).json({ error: "invalid_json" });
  }

  eventStore.set(id, {
    id,
    provider: "github",
    type: eventType,
    headers: headersForStorage(req),
    rawBody,
    payload,
    receivedAt: new Date().toISOString(),
    status: "queued",
    attempts: 0,
  });

  retryQueue.push(id);
  void processNextEvent();

  return res.status(202).json({ id, status: "queued" });
});

export async function processNextEvent(): Promise<void> {
  const id = retryQueue.shift();
  if (!id) return;
  const event = eventStore.get(id);
  if (!event || event.status === "processed") return;

  event.status = "processing";
  event.attempts += 1;

  try {
    await handleWebhookEvent(event);
    event.status = "processed";
    processedEvents.add(id);
  } catch (error) {
    event.status = "failed";
    event.lastError = error instanceof Error ? error.message : String(error);
    if (event.attempts < 5) {
      const delayMs = Math.min(30_000, 1_000 * 2 ** event.attempts);
      setTimeout(() => {
        event.status = "queued";
        retryQueue.push(id);
        void processNextEvent();
      }, delayMs);
    }
  }
}

async function handleWebhookEvent(event: WebhookEvent): Promise<void> {
  if (event.type === "push") console.log("GitHub push received", event.id);
}

if (process.env.NODE_ENV !== "test") {
  const port = Number(process.env.PORT ?? 3000);
  app.listen(port, () => {
    console.log(`Webhook server listening on http://127.0.0.1:${port}`);
  });
}
WEBHOOK_SECRET=dev-secret-change-me npx tsx src/server.ts

Lokales Senden, Tests und Replay

scripts/send-local-webhook.ts:

import crypto from "node:crypto";

const secret = process.env.WEBHOOK_SECRET ?? "dev-secret-change-me";
const url =
  process.env.WEBHOOK_URL ?? "http://127.0.0.1:3000/webhooks/github";
const body = JSON.stringify({ ref: "refs/heads/main", after: "local-test" });
const signature =
  "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex");

const response = await fetch(url, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-github-event": "push",
    "x-github-delivery": `local-${Date.now()}`,
    "x-hub-signature-256": signature,
  },
  body,
});

console.log(response.status, await response.text());
WEBHOOK_SECRET=dev-secret-change-me npx tsx scripts/send-local-webhook.ts
NODE_ENV=test npx tsx --test test/webhook.test.ts

Für Replay speicherst du url, headers und body unverändert. Eine HMAC-Signatur bezieht sich auf die exakten Bytes; schon ein neu formatiertes JSON kann die Prüfung brechen.

Konkrete Use Cases

Bei Zahlungen bestätigt Stripe Bestellungen, meldet fehlgeschlagene Rechnungen und steuert Zugriffsrechte. In Entwicklungsprozessen lösen GitHub push und pull_request Previews, Dokumentation und interne Benachrichtigungen aus. Bei Formularen und CRM verhindert die externe ID doppelte Tickets. Bei ausgehenden SaaS-Webhooks brauchst du ebenfalls Signaturen, Delivery Logs, Timeouts, Retries und manuelles Resend.

Häufige Fehler

Der häufigste Fehler ist JSON Parsing vor der Signaturprüfung. Der zweite Fehler ist schwere Arbeit vor der 2xx-Antwort, wodurch Provider-Retries entstehen. Der dritte Fehler ist eine selbst generierte Idempotenz-ID pro Request. Der vierte Fehler ist ein Log ohne gespeicherte Original-Delivery. Prüfe außerdem, dass Secrets und komplette Payloads nicht in Logs landen.

Rollout-Checkliste

  • Raw body wird nur auf Webhook-Routen erhalten.
  • Signaturfehler gibt 401, ungültiges JSON 400, akzeptierte Arbeit 202 zurück.
  • Die Provider Delivery ID ist der Idempotenzschlüssel.
  • Event Store hält raw body, headers, Status, Attempts und letzten Fehler.
  • Retry Queue hat Limit, Backoff und Alerting für finalen Fehler.
  • Replay Tooling funktioniert mit gespeicherten Deliveries.
  • Secrets kommen aus Environment oder Secret Manager.

Fazit

Produktions-Webhooks bündeln Sicherheit, Zuverlässigkeit und Betrieb in einem kleinen Endpoint. Claude Code liefert bessere Ergebnisse, wenn der Prompt Provider-Vertrag, raw body, Signatur, Idempotenz, Queue, Tests, Replay und Runbook von Anfang an enthält.

ClaudeCodeLab bietet Vorlagen in Products und Team-Training über Training. In praktischen Tests haben zuerst fixierte raw-body- und Idempotenztests die meisten späteren Korrekturen vermieden.

#Claude Code #Webhook #API-Design #Sicherheit #asynchrone Verarbeitung
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.