Advanced (Mis à jour: 03/06/2026)

Implémenter des Webhooks avec Claude Code : signatures, idempotence et retries

Webhooks de production avec Claude Code : raw body, signatures, idempotence, retries, tests et runbook.

Implémenter des Webhooks avec Claude Code : signatures, idempotence et retries

Un webhook permet à un service externe de notifier votre application par HTTP lorsqu’un événement se produit. Paiement confirmé, push GitHub, formulaire envoyé, abonnement modifié, mise à jour CRM ou changement d’état SaaS : tous ces cas utilisent souvent ce modèle.

En production, un webhook n’est pas seulement un endpoint qui lit du JSON. Il faut conserver le raw body, vérifier la signature du fournisseur, garantir l’idempotence, stocker l’événement avant traitement, déléguer les tâches longues à une retry queue et prévoir un outil de replay. Si vous demandez seulement à Claude Code d’ajouter un webhook, vous risquez un code de démonstration qui parse le JSON avant la signature ou qui traite deux fois une livraison rejouée.

Pour le socle API, lisez aussi le développement d’API avec Claude Code, la gestion des secrets, les bonnes pratiques de sécurité et les systèmes de queue.

Contrat fournisseur

ÉlémentExemple GitHubExemple StripePoint d’implémentation
EndpointPOST /webhooks/githubPOST /webhooks/stripeRoutage séparé
ID d’événementX-GitHub-Deliveryevent.idClé d’idempotence
Type d’événementX-GitHub-Eventevent.typeDispatch handler
Header de signatureX-Hub-Signature-256Stripe-SignatureVérification
Entrée vérifiéeraw bodyraw bodyOrdre du parser
Réponse de succès2xx rapide2xx rapideStocker puis enqueuer

Gardez les sources officielles ouvertes : GitHub Webhooks, validation des livraisons GitHub, Stripe Webhooks, Stripe webhook signatures, Express express.raw et 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 pour Claude Code

Implémente un récepteur GitHub Webhook en Express + TypeScript.

Contraintes :
- Ajouter POST /webhooks/github
- Préserver le raw body avec express.raw({ type: "*/*" }) sur les routes webhook
- Parser le JSON uniquement après vérification de signature
- Vérifier X-Hub-Signature-256 avec HMAC SHA-256
- Utiliser X-GitHub-Delivery comme clé d'idempotence
- Stocker chaque événement accepté avant traitement
- Ne jamais traiter deux fois le même delivery id
- Répondre 202 rapidement et traiter via retry queue
- Ajouter des tests node:test pour signature valide, signature invalide et doublon
- Ajouter un replay script pour les livraisons sauvegardées
- Lire le secret depuis WEBHOOK_SECRET

Récepteur exécutable

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

Créez 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

Envoi local et tests

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

Pour le replay, sauvegardez url, headers et body sans reformater le body. Une signature HMAC porte sur les octets exacts reçus.

Cas d’usage

Dans les paiements, Stripe confirme les commandes et signale les factures échouées. Dans le développement, GitHub push et pull_request déclenchent previews, documentation et notifications internes. Dans les formulaires et CRM, l’ID externe évite les tickets en double. Pour les webhooks sortants d’un SaaS, prévoyez aussi signature, journal de livraison, timeout, retries et renvoi manuel.

Pièges fréquents

Le premier piège est de parser le JSON avant la vérification de signature. Le second est de faire le travail lourd avant de répondre 2xx, ce qui provoque des retries côté fournisseur. Le troisième est de générer une nouvelle clé d’idempotence à chaque requête. Le quatrième est de ne garder que des logs, sans raw body rejouable. Enfin, ne mettez jamais les secrets ni les payloads complets dans les logs.

Checklist de mise en production

  • Le raw body est conservé uniquement sur les routes webhook.
  • Signature invalide : 401; JSON invalide : 400; événement accepté : 202.
  • Le delivery ID fournisseur sert de clé d’idempotence.
  • L’event store garde raw body, headers, statut, tentatives et dernière erreur.
  • La retry queue a une limite, un backoff et une alerte de dernier échec.
  • Le replay script fonctionne avec les livraisons sauvegardées.
  • Les secrets viennent d’une variable d’environnement ou d’un secret manager.

Résumé

Un webhook de production concentre sécurité, fiabilité et opérations dans un petit endpoint. Claude Code devient nettement plus utile quand le prompt inclut contrat fournisseur, raw body, signature, idempotence, queue, tests, replay et runbook.

ClaudeCodeLab propose des modèles dans Products et de l’accompagnement d’équipe via Training. Dans mes essais, figer d’abord raw body et idempotence réduit fortement les corrections demandées ensuite à Claude Code.

#Claude Code #Webhook #conception API #sécurité #traitement asynchrone
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.