Por trás de todo o vocabulário — eventos, entregas, assinaturas —, um webhook é apenas uma requisição HTTP. Se você já entende o que é um webhook e como ele funciona, o próximo passo é saber ler uma entrega de ponta a ponta: cada linha, do método ao corpo, carrega informação que resolve depuração, deduplicação e segurança. Vamos dissecar uma requisição real, parte por parte.
Uma entrega típica, crua, se parece com isto:
POST /webhooks/github HTTP/1.1
Host: sua-app.com
User-Agent: GitHub-Hookshot/f05835d
Content-Type: application/json
Content-Length: 7291
X-GitHub-Event: push
X-GitHub-Delivery: 72d3162e-cc78-11e3-81ab-4c9367dc0958
X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
{
"ref": "refs/heads/main",
"before": "9049f1265b7d61be4a8904a9a27120d2064dab3b",
"after": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"repository": { "full_name": "acme/api" },
"pusher": { "name": "octocat" },
"commits": [ "..." ]
}Três blocos: a linha de requisição (método e caminho), os headers e o corpo. Cada um responde a uma pergunta diferente: o que fazer com isso?, isso é confiável e é novo? e o que exatamente aconteceu?
A linha de requisição: por que quase sempre POST
Webhooks transportam um fato novo — um evento — e por isso usam POST: é o método HTTP para enviar dados que o servidor deve processar. O caminho (/webhooks/github no exemplo) é escolhido por você ao cadastrar a URL; muitos times criam um caminho por provedor, o que simplifica roteamento, logs e permissões.
A exceção clássica é a verificação de endpoint. Alguns provedores, antes de enviar qualquer evento, confirmam que a URL é sua. A Meta (WhatsApp Business, Instagram) faz um GET de handshake: GET /webhooks?hub.mode=subscribe&hub.verify_token=SEU_TOKEN&hub.challenge=158201444. Seu servidor deve conferir o hub.verify_token e responder 200 com o valor de hub.challenge no corpo. Se o seu endpoint só aceita POST, essa verificação falha — e a integração nem começa.
Os headers, grupo a grupo
Os headers são a parte mais subestimada de uma entrega. Eles se dividem em três grupos com funções distintas.
Identificação do evento e da entrega
Antes de abrir o corpo, os headers já dizem que tipo de evento chegou e qual entrega é essa:
X-GitHub-Event— o tipo do evento (push,pull_request…). Permite rotear o processamento sem parsear o JSON.X-GitHub-Delivery— um UUID único por entrega. Se o GitHub reenviar o mesmo evento, o UUID muda; é o identificador que você cita num ticket de suporte.webhook-id— no padrão Standard Webhooks (adotado por um número crescente de provedores), esse header identifica o evento e se mantém igual em retries: é a chave ideal para deduplicação.
A distinção importa: um ID de entrega muda a cada tentativa; um ID de evento não. Para implementar idempotência você quer o segundo — se o provedor só oferece o primeiro, use o id de dentro do corpo.
Assinatura e segurança
Seu endpoint é público, então qualquer pessoa pode enviar um POST para ele. O header de assinatura é o que prova que a requisição veio mesmo do provedor: um HMAC calculado sobre o corpo com um segredo que só vocês dois conhecem. Cada provedor embala isso de um jeito:
| Provedor | Header | Formato / algoritmo |
|---|---|---|
| GitHub | X-Hub-Signature-256 | HMAC-SHA256 do corpo, em hex, com prefixo sha256= |
| Stripe | Stripe-Signature | t=timestamp,v1=hex — HMAC-SHA256 sobre {t}.{corpo}, o timestamp entra no cálculo |
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 do corpo, codificado em Base64 |
| Mercado Pago | x-signature | ts=…,v1=hex — HMAC-SHA256 sobre um manifest com data.id, x-request-id e o timestamp |
| Standard Webhooks | webhook-signature | v1,base64 — HMAC-SHA256 sobre {id}.{timestamp}.{corpo} |
Repare no padrão: quando o timestamp participa do cálculo (Stripe, Mercado Pago, Standard Webhooks), o provedor está fechando a porta para replay attacks — reenvio de uma entrega antiga capturada. O mecanismo completo de validação, incluindo comparação em tempo constante e tolerância de relógio, está em Segurança de webhooks: assinatura HMAC e boas práticas.
Conteúdo e metadados
Content-Type— quase sempreapplication/json, mas não conte com isso cegamente. O GitHub pode ser configurado paraapplication/x-www-form-urlencoded, entregando o JSON dentro de um campopayload=. Sistemas legados (e alguns gateways de pagamento antigos) ainda enviam XML.Content-Length— o tamanho do corpo em bytes. Útil para detectar payloads truncados por proxies e para definir limites no seu servidor.User-Agent— identifica o remetente (GitHub-Hookshot/f05835d,Stripe/1.0 (+https://stripe.com/docs/webhooks)…). Bom para filtros grosseiros e estatística, mas é falsificável — nunca o use como autenticação.
O corpo: envelope, evento e dados
Apesar de cada provedor ter seu esquema, a maioria converge para o mesmo envelope: um identificador, um tipo, um timestamp e os dados do evento. É o contrato mínimo para você rotear, deduplicar e ordenar.
{
"id": "evt_1QxK2mLkdIwHu7ix", // deduplicação
"type": "payment.approved", // roteamento
"created_at": "2026-07-25T14:32:07Z", // ordenação / tolerância
"data": { "...": "..." } // o evento em si
}A grande divisão está no conteúdo de data. Há duas filosofias:
- Payload "gordo" (fat payload) — o evento carrega o objeto completo. GitHub e Stripe funcionam assim: o push traz os commits, o evento de pagamento traz o objeto
payment_intentinteiro. Você processa sem chamadas extras, mas o dado pode estar defasado se eventos chegarem fora de ordem. - Payload "magro" (thin payload) — o evento traz só referências, e você consulta a API para obter o estado atual. É o modelo do Mercado Pago:
{
"id": 117554765,
"type": "payment",
"action": "payment.updated",
"date_created": "2026-07-25T14:32:07Z",
"data": { "id": "1316643861" }
}O thin payload obriga uma chamada à API (GET /v1/payments/1316643861) antes de qualquer decisão — mais latência e mais um ponto de falha, porém o estado lido é sempre o atual, e um payload vazado expõe menos dados. Saber em qual filosofia seu provedor se encaixa define a arquitetura do seu consumidor.
Raw body: a pegadinha número um
A assinatura HMAC é calculada sobre os bytes exatos do corpo. Não sobre "o JSON equivalente" — sobre a sequência de bytes. Se o seu framework parseia o corpo antes de você validar (o clássico express.json() global), o que sobra é um objeto JavaScript; re-serializá-lo com JSON.stringify muda espaços, ordem de chaves ou escapes — e a assinatura nunca mais bate. É de longe a causa mais comum de "validação de webhook falhando só em produção".
// ❌ com o parser global, os bytes originais se perdem
app.use(express.json());
// ✅ opção 1 — raw body apenas na rota de webhook
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
(req, res) => {
// req.body é um Buffer com os bytes exatos
const ok = verificarAssinatura(req.body, req.headers["stripe-signature"]);
if (!ok) return res.sendStatus(400);
res.sendStatus(200);
processarDepois(JSON.parse(req.body));
}
);
// ✅ opção 2 — manter express.json() e guardar o raw via "verify"
app.use(
express.json({
verify: (req, _res, buf) => {
req.rawBody = buf;
},
})
);O mesmo princípio vale para Next.js (desative o body parser da rota), Fastify (rawBody via plugin) e qualquer outro framework: valide sobre o buffer bruto, parseie depois.
A resposta: o que o provedor espera de você
- Status 2xx confirma o recebimento.
200e204são os usuais; o corpo da resposta é ignorado pela maioria dos provedores — não gaste tempo montando um. - Rápido: o relógio corre. Timeouts típicos vão de 5 segundos (Shopify) a 10–30 segundos (GitHub, Stripe e outros). Estourou o tempo, a entrega conta como falha — mesmo que seu código tenha terminado depois. Por isso a regra: responda já, processe assíncrono.
- Redirects contam como falha. A maioria dos provedores não segue
3xx. Um redirect dehttpparahttpsou de domínio com/semwwwé o suficiente para "perder" webhooks — cadastre a URL final exata. - 4xx e 5xx alimentam a política de retry. Ambos disparam novas tentativas com backoff, mas falhas persistentes têm consequência: GitHub, Stripe e Shopify desativam ou removem endpoints que só devolvem erro por dias. Um detalhe útil do padrão Standard Webhooks: responder
410 Gonesinaliza "pare de enviar para cá".
Resumo
- Um webhook é POST + headers + corpo; o GET de verificação (challenge da Meta) é a exceção que seu endpoint precisa suportar.
- Headers respondem "o que é, é novo, é confiável": tipo do evento, ID de entrega/evento e assinatura HMAC — cada provedor com seu formato.
- O corpo segue um envelope (id, type, timestamp, data) e vem "gordo" (GitHub, Stripe) ou "magro" (Mercado Pago) — a escolha muda sua arquitetura.
- Valide a assinatura sobre o raw body (bytes exatos), nunca sobre o JSON re-serializado.
- Responda 2xx em milissegundos, sem redirect; erros repetidos geram retries e podem desativar seu endpoint.