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.
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ément | Exemple GitHub | Exemple Stripe | Point d’implémentation |
|---|---|---|---|
| Endpoint | POST /webhooks/github | POST /webhooks/stripe | Routage séparé |
| ID d’événement | X-GitHub-Delivery | event.id | Clé d’idempotence |
| Type d’événement | X-GitHub-Event | event.type | Dispatch handler |
| Header de signature | X-Hub-Signature-256 | Stripe-Signature | Vérification |
| Entrée vérifiée | raw body | raw body | Ordre du parser |
| Réponse de succès | 2xx rapide | 2xx rapide | Stocker 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.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Permission receipt Claude Code : portée, preuves et rollback
Modèle de permission receipt pour Claude Code : actions autorisées, limites d'approbation, commandes de preuve, rollback et CTAs revenus.
Agent Harness securise pour Claude Code et Codex : permissions, verification et rollback
Construisez un Agent Harness pratique pour Claude Code et Codex avec politiques, plan, verification et recuperation.
Sous-agents Claude Code : guide pratique pour déléguer sans perdre le contrôle
Guide pratique des sous-agents Claude Code pour répartir articles et code : règles, prompts, pièges et checklist.