Verificação de Webhook
Webhooks enviam eventos para sua API Node. Verifique assinaturas HMAC, bloqueie repetições e processe idempotentemente, pois os fornecedores tentam novamente em caso de timeout.
Busque em todas as páginas da documentação
Webhooks enviam eventos para sua API Node. Verifique assinaturas HMAC, bloqueie repetições e processe idempotentemente, pois os fornecedores tentam novamente em caso de timeout.
Cartão de receita de referência rápida - pronto para copiar e colar.
// Express 5 - corpo bruto para Stripe
import express from "express";
import Stripe from "stripe";
const app = express();
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["stripe-signature"] as string;
try {
const event = Stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
// despachar event.type
res.json({ received: true });
} catch {
res.status(400).send("invalid signature");
}
}
);Quando usar isso:
// src/webhooks/stripe.ts
import type { Request, Response } from "express";
import Stripe from "stripe";
import { getStripe } from "../integrations/stripe-client";
import { paymentsQueue } from "../queues/payments";
const stripe = getStripe();
export async function stripeWebhookHandler(req: Request, res: Response) {
const sig = req.headers["stripe-signature"];
if (!sig || !Buffer.isBuffer(req.body)) {
return res.status(400).send("bad request");
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.warn({ msg: "stripe_sig_fail", err: (err as Error).message });
return res.status(400).send("invalid signature");
}
if (await isEventProcessed(event.id)) {
return res.json({ received: true, duplicate: true });
}
await markEventReceived(event.id);
if (event.type === "payment_intent.succeeded") {
await paymentsQueue.add("fulfill", { paymentIntentId: (event.data.object as Stripe.PaymentIntent).id });
}
res.json({ received: true });
}
// Verificação genérica HMAC (estilo Twilio)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyHmacSha256(rawBody: Buffer, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret).update(rawBody).digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}O que isso demonstra:
event.id antes de efeitos colaterais// Monte as rotas de webhook ANTES de express.json() globalmente
const webhookRouter = express.Router();
webhookRouter.post("/stripe", express.raw({ type: "application/json" }), stripeWebhookHandler);
app.use("/webhooks", webhookRouter);
app.use(express.json());addContentTypeParser para buffer bruto na rota do webhook// Rejeitar eventos com mais de 5 minutos (parsers customizados)
const ts = Number(req.header("X-Webhook-Timestamp"));
if (Math.abs(Date.now() / 1000 - ts) > 300) {
return res.status(400).send("stale");
}constructEventimport twilio from "twilio";
const valid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN!,
req.headers["x-twilio-signature"] as string,
fullUrl,
req.body
);| Código | Comportamento do Fornecedor |
|---|---|
| 2xx | Parar repetição |
| 4xx (assinatura inválida) | Parar repetição (corrigir configuração) |
| 5xx / timeout | Repetir com backoff |
timingSafeEqual.| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Polling da API do fornecedor | Webhook indisponível | Necessidade de tempo real, limites de taxa custosos |
| SNS/SQS fan-in | Ingestão de eventos AWS | Stripe simples de endpoint único |
| Relé Svix | UX de verificação multi-fornecedor | Integração apenas com Stripe |
| Webhooks de parceiros mTLS | Contrato B2B exige | Fornecedor SaaS usa HMAC |
HMAC é calculado sobre bytes exatos. JSON.stringify após a análise altera o espaço em branco.
Segredo de assinatura separado por URL de endpoint no painel do fornecedor.
Stripe CLI stripe listen --forward-to localhost:3000/webhooks/stripe.
Afirmar 400 e zero efeitos colaterais no banco de dados quando o cabeçalho é adulterado.
Geralmente nenhuma. Projetar manipuladores idempotentes; usar campos de versão se a ordem importar.
X-Hub-Signature-256 HMAC SHA256 - mesmo padrão de corpo bruto.
app.useBodyParser("raw", { verify: ...}) no caminho do webhook ou middleware dedicado.
Montagem de rota separada sem JWT - a assinatura é a autenticação.
Persistir a carga útil falhada para repetição manual após a correção - não confiar apenas na repetição do fornecedor.
Defesa opcional em profundidade. A verificação HMAC é primária; os IPs do fornecedor mudam.
Versões da Pilha: Esta página foi escrita para Node.js 24.18.0 (LTS Ativo), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 e NestJS 11.
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026