Todos os artigos

Fundamentos · 25 de julho de 2026 · 10 min de leitura

Anatomia de uma requisição de webhook: headers, payload e resposta

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:

Entrega de webhook do GitHub (push) · requisição completa
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:

ProvedorHeaderFormato / algoritmo
GitHubX-Hub-Signature-256HMAC-SHA256 do corpo, em hex, com prefixo sha256=
StripeStripe-Signaturet=timestamp,v1=hex — HMAC-SHA256 sobre {t}.{corpo}, o timestamp entra no cálculo
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256 do corpo, codificado em Base64
Mercado Pagox-signaturets=…,v1=hex — HMAC-SHA256 sobre um manifest com data.id, x-request-id e o timestamp
Standard Webhookswebhook-signaturev1,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 sempre application/json, mas não conte com isso cegamente. O GitHub pode ser configurado para application/x-www-form-urlencoded, entregando o JSON dentro de um campo payload=. 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.

Envelope típico
{
  "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_intent inteiro. 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:
Webhook do Mercado Pago · thin payload
{
  "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".

Express · preservando o raw body
// ❌ 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. 200 e 204 sã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 de http para https ou de domínio com/sem www é 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 Gone sinaliza "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.