Advanced (अपडेट: 3/6/2026)

Claude Code से production Webhook बनाएं: signature, idempotency और retries

Claude Code से production Webhook बनाएं: raw body, signature verification, idempotency, retries, tests और runbook.

Claude Code से production Webhook बनाएं: signature, idempotency और retries

Webhook वह तरीका है जिससे कोई external service event होने पर आपकी application को HTTP request भेजती है। Payment complete होना, GitHub push, form submission, subscription change, CRM update या SaaS status change जैसे cases में Webhook बहुत उपयोगी होता है।

Production में Webhook सिर्फ JSON पढ़ने वाला POST endpoint नहीं है। आपको raw body यानी request body के original bytes बचाने होते हैं, provider की signature verify करनी होती है, idempotency रखनी होती है ताकि वही event दोबारा आए तो business action एक बार ही चले, और heavy काम को retry queue में भेजना होता है। Failure के बाद replay tool और runbook भी चाहिए।

अगर आप Claude Code को केवल “Webhook बना दो” कहेंगे, तो demo जल्दी बन सकता है, लेकिन वह JSON parse पहले कर सकता है या duplicate delivery को दो बार process कर सकता है। बेहतर prompt में failure rules भी लिखें। Related guides: Claude Code API development, Secrets management, security best practices, और queue system

Provider contract

ItemGitHub exampleStripe exampleImplementation point
EndpointPOST /webhooks/githubPOST /webhooks/stripeअलग route
Event IDX-GitHub-Deliveryevent.ididempotency key
Event typeX-GitHub-Eventevent.typehandler routing
Signature headerX-Hub-Signature-256Stripe-Signatureauthenticity check
Verification inputraw bodyraw bodybody parser order
Success responsefast 2xxfast 2xxsave then queue

Official references साथ रखें: GitHub Webhooks, GitHub delivery validation, Stripe Webhooks, Stripe webhook signatures, Express express.raw, और 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"]

Claude Code prompt

Express + TypeScript में GitHub Webhook receiver implement करें।

Requirements:
- POST /webhooks/github add करें
- Webhook routes पर express.raw({ type: "*/*" }) से raw body preserve करें
- JSON parse signature verification के बाद करें
- X-Hub-Signature-256 को HMAC SHA-256 से verify करें
- X-GitHub-Delivery को idempotency key बनाएं
- Accepted event को processing से पहले event store में save करें
- Same delivery id को दो बार process न करें
- Quickly 202 return करें और heavy work retry queue में process करें
- node:test से valid signature, invalid signature और duplicate delivery cover करें
- Saved deliveries के लिए replay script add करें
- Secret को WEBHOOK_SECRET environment variable से पढ़ें

Runnable 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

Local sender और 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

Replay के लिए url, headers और body को बिना reformat किए save करें। HMAC signature exact bytes पर बनती है, इसलिए नया newline भी verification fail करा सकता है।

Practical use cases

Payments में Stripe event order confirm कर सकता है, failed invoice पर access pause कर सकता है और receipt भेज सकता है। Development workflow में GitHub push और pull_request previews, docs और internal notification trigger करते हैं। Forms और CRM में external ID duplicate tickets रोकता है। अपने SaaS से outgoing webhook भेजते समय भी signed payload, delivery logs, timeout, retry और manual resend चाहिए।

Common pitfalls

पहला pitfall है JSON parse को signature verification से पहले चलाना। दूसरा है 2xx return करने से पहले heavy काम करना, जिससे provider retry करता है। तीसरा है हर request पर नया UUID बनाकर idempotency key समझ लेना। चौथा है original delivery save न करना और केवल logs पर निर्भर रहना। Secret या पूरा payload logs में न डालें।

Production checklist

  • Webhook routes पर ही raw body preserve होता है।
  • Invalid signature 401, invalid JSON 400, accepted work 202 देता है।
  • Provider delivery ID idempotency key है।
  • Event store raw body, headers, status, attempts और last error रखता है।
  • Retry queue में limit, backoff और final failure alert है।
  • Replay script saved delivery से काम करता है।
  • Secrets environment variable या secret manager से आते हैं।

Summary

Production Webhook छोटा endpoint है, लेकिन उसके अंदर security, reliability और operations छिपे होते हैं। Claude Code से बेहतर output चाहिए तो prompt में provider contract, raw body, signature verification, idempotency, queue, tests, replay और runbook शुरू से दें।

ClaudeCodeLab के Products में reusable templates हैं, और team adoption के लिए Training उपलब्ध है। Practical test में raw body और idempotency tests पहले लिखने से Claude Code की बाद की corrections काफी कम हुईं।

#Claude Code #Webhook #API design #security #async processing
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.

हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.

Masa

लेखक के बारे में

Masa

Claude Code workflow और team adoption पर काम करने वाला engineer.