Todos os artigos
webhook debuggingHTTP errorsauthenticationtroubleshooting

Por que seu webhook devolve 401 ou 403 (e como corrigir)

Um webhook que devolve 401 ou 403 cai num pequeno número de gavetas. Na maioria das vezes nem é um problema de assinatura — é um middleware ou um padrão do framework rejeitando a requisição antes do seu código rodar. Esta é a checklist de diagnóstico que usamos, na ordem em que usamos.

Primeiro, separe 401 de 403

Significam coisas diferentes e a correção é diferente.

401 Unauthorized: o servidor recebeu a requisição, olhou as credenciais (ou a assinatura) e não as aceitou. Seu handler provavelmente rodou, calculou uma assinatura esperada e rejeitou.

403 Forbidden: o servidor recebeu a requisição e se recusou a processá-la por outro motivo. Muitas vezes a requisição nunca chegou ao seu handler — um middleware ou padrão do framework enviou o 403.

Abra a captura de requisições do seu túnel e olhe a resposta. Se você vê o corpo de resposta do seu handler («invalid signature»), é um 401 do seu código. Se a resposta é genérica e seus logs do handler não mostram nada, o framework rejeitou antes do seu handler rodar.

As cinco causas mais prováveis

1. Middleware CSRF (Django, Rails, Laravel)

A proteção CSRF padrão rejeita POSTs sem token de sessão. Provedores de webhook não enviam um. Sintomas: 403, corpo de resposta genérico, sem logs do handler.

Correção: exclua a rota do webhook da proteção CSRF. No Django, @csrf_exempt. No Rails, skip_before_action :verify_authenticity_token, only: [:webhook]. No Laravel, adicione o caminho a VerifyCsrfToken::$except. O guia de webhooks do Django percorre a versão do Django de ponta a ponta.

2. Middleware de auth aplicado de forma ampla demais

Você adicionou middleware de auth globalmente (JWT, checagem de sessão, exigência de API key) e a rota do webhook o herdou. O provedor não envia seu cabeçalho de auth, então o middleware manda um 401 antes do seu handler rodar.

Correção: exclua o caminho do webhook do seu middleware de auth. A autenticação do webhook é a assinatura, não o esquema que protege o resto da sua API.

3. Verificação de assinatura falhando

Seu handler rodou, calculou uma assinatura esperada e não bateu. Cinco subcausas, mais ou menos em ordem decrescente de frequência:

  • Corpo parseado por middleware antes da verificação rodar (o corpo cru se foi).
  • Codificação errada (hex vs base64). Veja o guia de verificação de assinatura.
  • Segredo errado (test vs live, dashboard vs CLI, arquivo env vs runtime).
  • Timestamp velho demais (a assinatura é válida mas rançosa — provavelmente você testa com um payload antigo reenviado).
  • Espaços em branco no fim do segredo carregado de um arquivo env.

4. URL de túnel errada registrada

Você reiniciou seu túnel e a URL rotacionou, mas o dashboard do provedor ainda tem a antiga. O sintoma parece um 401 porque a requisição nunca chegou a você — mas na verdade é uma requisição diferente chegando a um servidor diferente (muitas vezes a sessão de túnel anterior, que agora recusa ou devolve 401).

Correção: confirme que a URL no dashboard do provedor bate com sua sessão de túnel atual. Se precisa de uma URL estável, olhe os túneis com nome ou um subdomínio reservado.

5. Restrições de CORS, content-type ou método

Menos comum em webhooks, mas possível. Se sua rota só aceita application/json e o provedor envia application/x-www-form-urlencoded (Twilio, por exemplo), alguns frameworks dão 415 — mas um mal configurado pode dar 403. Ou sua rota está registrada para GET e o provedor faz POST.

O fluxo de diagnóstico de 90 segundos

Esta é a ordem que seguimos:

  1. Leia o corpo de resposta. Se tem as palavras do seu handler, a requisição chegou ao handler. Passe para depurar a assinatura. Se é uma página de erro genérica do framework, a requisição não chegou. Passe para depurar o middleware.
  2. Cheque os logs do handler. Algum statement de log do seu handler está disparando? Confirma se a requisição chegou a você.
  3. Cheque a URL registrada. Abra o dashboard do provedor. Confirme que a URL bate com seu túnel atual. Confirme que aponta para o caminho certo.
  4. Compare os cabeçalhos de entrada. A captura do túnel mostra os cabeçalhos exatos que o provedor enviou. Compare com o que seu código lê. Cabeçalhos são insensíveis a maiúsculas mas a forma de acessá-los difere por framework — request.headers.get('Stripe-Signature') em alguns, request.META['HTTP_STRIPE_SIGNATURE'] em outros.
  5. Verifique que o corpo está cru no momento da assinatura. Imprima o comprimento do corpo logo antes de calcular a assinatura. Se for zero ou surpreendentemente pequeno, um middleware o comeu.
  6. Confira o segredo de novo. Compare a variável de ambiente no seu runtime com o dashboard. console.log(process.env.WEBHOOK_SECRET.length) — o comprimento bate com o que o dashboard mostra?

Na nossa experiência, os passos 1 e 5 pegam 80% dos casos nos primeiros dois minutos.

Capture a requisição que falha

A ferramenta mais útil aqui é um túnel com captura e replay de requisições. Você não precisa esperar o provedor repetir — você reproduz a requisição capturada contra seu handler local enquanto depura. Cada tentativa é instantânea.

Se você usa um túnel que não captura requisições, está depurando com uma mão amarrada nas costas. Trocar por um que captura (ou rodar tcpdump, ou pôr nginx na frente do seu servidor de dev) se paga na primeira vez que você economiza uma hora.

Como o 401 aparece em cada provedor

  • Stripe: um 401 do seu código significa que a verificação de assinatura falhou. O dashboard do Stripe mostra a entrega como falha e inclui seu corpo de resposta.
  • GitHub: se seu handler devolve 401, o GitHub marca a entrega como falha e repete. A página de entregas recentes mostra a resposta.
  • Shopify: um 401 de um handler de webhook é ok por segurança mas o Shopify vai repetir. Após 19 falhas consecutivas em 48 horas, o Shopify desativa a assinatura.

Para o contexto mais amplo de depuração de webhooks, veja como depurar webhooks em local. Entre na lista de espera do PortPreview para um túnel com captura embutida.

Perguntas frequentes

O que significa um 401 num webhook?
Seu handler recebeu a requisição, avaliou as credenciais ou a assinatura e as rejeitou. O mais comum é uma falha de verificação de assinatura causada por parsear o corpo antes da verificação, a codificação errada (hex vs base64) ou o segredo errado.
Por que provedores de webhook veem um 403 do meu app?
Um 403 geralmente significa que a requisição nunca chegou ao seu handler. A causa mais comum é middleware CSRF (no Django, Rails, Laravel) ou middleware de auth aplicado de forma ampla demais. Exclua a rota do webhook do CSRF e da auth geral da API para que a requisição possa chegar ao seu código de verificação.
Como depuro falhas de auth de webhook sem redisparar eventos?
Use um túnel com captura e replay de requisições. Capture uma entrega do provedor, depois reproduza-a contra seu handler local quantas vezes precisar enquanto depura. Cada replay é instantâneo e não depende do provedor.