Advanced (Atualizado: 03/06/2026)

Implementar Webhooks com Claude Code: assinaturas, idempotência e retries

Webhooks de produção com Claude Code: raw body, assinatura, idempotência, retry, testes e runbook.

Implementar Webhooks com Claude Code: assinaturas, idempotência e retries

Webhook é o mecanismo pelo qual um serviço externo avisa sua aplicação por HTTP quando um evento acontece. Pagamento confirmado, push no GitHub, envio de formulário, mudança de assinatura, atualização no CRM e alteração de status em um SaaS são exemplos comuns.

Em produção, um webhook não é apenas um endpoint que lê JSON. Você precisa preservar o raw body, verificar a assinatura do provedor, garantir idempotência, salvar o evento antes de processar, mover trabalho pesado para uma retry queue e ter uma ferramenta de replay. Se você pedir ao Claude Code apenas “crie um webhook”, ele pode gerar um demo que funciona localmente, mas quebra quando o provedor reenvia a mesma entrega.

Para a base de API, leia também desenvolvimento de API com Claude Code, gestão de secrets, boas práticas de segurança e sistemas de fila.

Contrato do provedor

ItemExemplo GitHubExemplo StripePonto de implementação
EndpointPOST /webhooks/githubPOST /webhooks/stripeRotas separadas
ID do eventoX-GitHub-Deliveryevent.idChave de idempotência
Tipo do eventoX-GitHub-Eventevent.typeEscolha do handler
Header de assinaturaX-Hub-Signature-256Stripe-SignatureVerificação
Entrada verificadaraw bodyraw bodyOrdem do body parser
Resposta de sucesso2xx rápido2xx rápidoSalvar e enfileirar

Use fontes oficiais: GitHub Webhooks, validação de entregas GitHub, Stripe Webhooks, Stripe webhook signatures, Express express.raw e 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 para Claude Code

Implemente um receptor de GitHub Webhook em Express + TypeScript.

Requisitos:
- Adicionar POST /webhooks/github
- Preservar raw body com express.raw({ type: "*/*" }) nas rotas webhook
- Fazer JSON parse somente depois da verificação de assinatura
- Verificar X-Hub-Signature-256 com HMAC SHA-256
- Usar X-GitHub-Delivery como chave de idempotência
- Salvar cada evento aceito antes do processamento
- Não processar duas vezes o mesmo delivery id
- Responder 202 rapidamente e processar via retry queue
- Cobrir assinatura válida, assinatura inválida e duplicados com node:test
- Adicionar replay script para entregas salvas
- Ler o segredo de WEBHOOK_SECRET

Receptor executável

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

Crie 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

Envio local e testes

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

Para replay, salve url, headers e body sem reformatar. A assinatura HMAC é calculada sobre os bytes exatos recebidos.

Casos de uso

Em pagamentos, eventos Stripe confirmam pedidos, pausam acesso após falha de cobrança e disparam recibos. Em desenvolvimento, push e pull_request do GitHub criam previews, documentação e notificações internas. Em formulários e CRM, o ID externo evita tickets duplicados. Em webhooks enviados pelo seu próprio SaaS, use assinatura, logs de entrega, timeouts, retries e reenvio manual.

Armadilhas comuns

A primeira armadilha é parsear JSON antes de verificar a assinatura. A segunda é fazer trabalho pesado antes de responder 2xx, causando reenvios. A terceira é gerar uma chave de idempotência nova a cada request. A quarta é manter apenas logs, sem delivery original para replay. Também evite secrets e payload completo em logs.

Checklist de produção

  • O raw body é preservado apenas nas rotas webhook.
  • Assinatura inválida retorna 401; JSON inválido retorna 400; evento aceito retorna 202.
  • O delivery ID do provedor é a chave de idempotência.
  • O event store salva raw body, headers, status, tentativas e último erro.
  • A retry queue tem limite, backoff e alerta de falha final.
  • O replay script funciona com entregas salvas.
  • Secrets vêm de variável de ambiente ou secret manager.

Resumo

Webhook de produção concentra segurança, confiabilidade e operação em um endpoint pequeno. Claude Code funciona melhor quando o prompt já inclui contrato do provedor, raw body, assinatura, idempotência, fila, testes, replay e runbook.

ClaudeCodeLab oferece modelos em Products e treinamento para equipes em Training. Em testes práticos, começar por raw body e testes de idempotência reduziu bastante o retrabalho gerado depois.

#Claude Code #Webhook #design de API #segurança #processamento assíncrono
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.