O que é um webhook: como sistemas avisam uns aos outros sem ficar perguntando
Webhook é uma chamada HTTP que um sistema dispara quando algo acontece, invertendo o polling; como receber, validar e processar com segurança.
Neste artigo
Existe uma pergunta que todo sistema que depende de outro precisa responder: como eu sei que algo aconteceu do outro lado? A resposta ingênua é perguntar de novo, e de novo, e de novo, até a resposta mudar. A resposta que a maioria dos serviços sérios adotou — Stripe, GitHub, Twilio, WhatsApp Business, praticamente qualquer plataforma que expõe eventos — é a oposta: em vez de você perguntar, o outro lado avisa. Esse mecanismo de aviso tem nome, webhook, e apesar de ser conceitualmente simples — é só uma requisição HTTP —, ele carrega um conjunto de armadilhas que só aparecem em produção, sob carga, com um atacante curioso testando a URL.
Este artigo explica o que é um webhook, por que ele existe, como está estruturado por dentro, e — a parte que a maioria dos tutoriais pula — os cinco problemas que todo endpoint de webhook mal escrito acaba tendo: segurança, duplicidade, lentidão, ordem e ambiente de teste. Entender essas cinco coisas separa um endpoint de webhook que funciona na demonstração de um que aguenta produção.
O problema do polling: ficar perguntando "já aconteceu?"#
A forma mais óbvia de saber se algo mudou é perguntar em intervalos. Isso se chama polling: a cada alguns segundos ou minutos, seu sistema faz uma requisição a uma API perguntando "o status do pedido 1042 mudou?", recebe a resposta, descarta se não mudou nada, e pergunta de novo daqui a pouco. É a estratégia mais simples de implementar e por isso é onde quase todo mundo começa.
O custo do polling cresce em duas direções ao mesmo tempo. Perguntar com pouca frequência significa descobrir o evento tarde — se você checa a cada cinco minutos, a notícia de que um pagamento foi aprovado pode levar quase cinco minutos para chegar, mesmo que o evento real tenha acontecido no primeiro segundo. Perguntar com muita frequência resolve a latência, mas multiplica o número de requisições: mil clientes checando o status de mil pedidos a cada segundo é um milhão de chamadas por segundo que, na esmagadora maioria das vezes, respondem "nada mudou". É desperdício de rede, de CPU do lado que responde, e de dinheiro em qualquer API cobrada por chamada.
Polling também é um jogo de adivinhação de frequência. Não existe intervalo certo, porque a frequência ideal depende de quando o evento acontece de verdade, e isso é justamente o que você não sabe de antemão. Ajustar o intervalo é sempre um compromisso entre gastar recursos à toa e demorar para reagir — um trade-off que some completamente quando o problema é resolvido de outro jeito.
A inversão: não me ligue, eu ligo para você#
O webhook resolve o problema invertendo quem inicia a conversa. Em vez de você perguntar repetidamente se algo aconteceu, você registra uma vez uma URL sua junto ao provedor, e é o provedor quem faz a chamada — exatamente no momento em que o evento ocorre, nem antes nem depois. Isso costuma ser descrito com uma frase do mundo do cinema: don't call us, we'll call you — não nos ligue, nós ligamos para você. Em engenharia de software esse padrão é conhecido como Hollywood principle, e é a mesma ideia por trás de callbacks e de arquiteturas orientadas a eventos: o controle de "quando algo acontece" migra de quem consome para quem produz.
Tecnicamente, um webhook é só uma requisição HTTP comum, só que na direção contrária ao que a maioria dos desenvolvedores está acostumado. Você está habituado a escrever código que faz chamadas HTTP para uma API — seu servidor é o cliente, a API é o servidor. No webhook, seu servidor vira o lado que recebe a chamada: ele expõe um endpoint HTTP público, e é o provedor de eventos quem age como cliente, disparando um POST para esse endereço sempre que algo relevante acontece.
A consequência direta é latência quase nula e zero desperdício. Não existe intervalo de checagem porque não existe checagem — o aviso chega no instante em que o evento ocorre, e nenhuma requisição é feita quando nada muda, porque nesse caso simplesmente nenhuma requisição é enviada. É a troca de um problema de frequência por um problema de disponibilidade: agora seu endpoint precisa estar de pé para receber a chamada quando ela vier, o que introduz uma nova classe de cuidados, que é o assunto da segunda metade deste artigo.
Anatomia de um webhook#
Toda integração via webhook tem três peças fixas. Primeiro, uma URL de destino, que você cadastra no painel do provedor ou via API — algo como https://seusite.com/webhooks/pagamento. Segundo, o evento em si, entregue como um POST HTTP cujo corpo é um payload, quase sempre JSON, descrevendo o que aconteceu. Terceiro, um ou mais headers de assinatura, que permitem provar que aquele POST realmente veio do provedor esperado e não foi forjado por qualquer pessoa que descobriu a URL.
Um payload típico carrega o tipo do evento, um identificador único e os dados relevantes. Um evento de pagamento aprovado de uma plataforma de cobrança, por exemplo, chega mais ou menos assim:
``json { "id": "evt_1PabcXYZ", "type": "payment.succeeded", "created": "2026-08-14T13:05:22Z", "data": { "payment_id": "pay_9f2c", "amount": 4990, "currency": "brl", "customer_id": "cus_88a1" } } ``
O id do evento é o detalhe mais importante do payload, e é o que menos chama atenção à primeira vista. Ele existe para permitir que quem recebe o evento distinga "este é um evento novo" de "este eu já processei antes" — uma necessidade que fica clara na seção sobre entrega duplicada, mais adiante.
O header de assinatura acompanha o POST por fora do corpo, normalmente com um nome como X-Signature ou Stripe-Signature, e carrega um hash calculado sobre o payload usando um segredo que só você e o provedor conhecem. É esse header que transforma "qualquer POST que chega nessa URL" em "um POST que eu confio que veio de quem eu esperava" — sem ele, o endpoint aceita qualquer coisa que qualquer um mandar.
Exemplos do mundo real#
Pagamento aprovado é o exemplo mais citado, e por um bom motivo. Quando um cliente paga uma cobrança, a plataforma de pagamento — Stripe, PagSeguro, Mercado Pago — dispara um webhook payment.succeeded para o backend do lojista, que então libera o pedido, envia o e-mail de confirmação e atualiza o estoque. Sem o webhook, o lojista precisaria perguntar repetidamente "esse pagamento já foi aprovado?" para cada cobrança pendente, o que é exatamente o cenário de polling descrito acima.
Push para um repositório é outro caso canônico. Quando alguém dá git push num repositório no GitHub, o GitHub pode disparar um webhook push para uma URL configurada — é assim que pipelines de CI/CD sabem que devem começar a rodar, sem que nenhum servidor de CI precise ficar consultando "tem código novo?" a cada poucos segundos.
Mensagem recebida em uma plataforma de mensageria segue o mesmo padrão. Um número de WhatsApp Business ou uma integração de chat dispara um webhook para o backend da empresa sempre que um cliente manda uma mensagem, permitindo que um chatbot ou um atendente humano responda quase em tempo real, sem manter uma conexão de polling aberta o tempo todo.
Webhook vs API tradicional: quem liga para quem#
A diferença entre webhook e uma API tradicional não está no protocolo — os dois usam HTTP — mas em quem inicia a chamada. Numa API tradicional, você é sempre o cliente: seu código decide quando perguntar, monta a requisição e espera a resposta. No webhook, os papéis se invertem para aquele fluxo específico: o provedor de eventos é quem assume o papel de cliente HTTP, e o seu sistema passa a ser o servidor que responde.
Essa inversão explica por que webhook precisa de infraestrutura pública e API tradicional não precisa. Para consumir uma API você só precisa conseguir sair — fazer requisições para fora, o que qualquer máquina atrás de NAT ou firewall corporativo consegue fazer sem configuração extra. Para receber um webhook, você precisa conseguir ser alcançado — um endpoint com endereço público, HTTPS válido, e sem bloqueio de firewall na direção de entrada. É por isso que testar webhook em localhost exige um passo extra, tratado mais adiante.
Na prática, os dois modelos convivem na mesma integração. Você usa a API tradicional para consultar dados sob demanda ou para acionar ações — criar uma cobrança, disparar uma mensagem — e usa webhook para ser avisado quando o resultado dessa ação, ou de uma ação de terceiros, muda de estado. Um não substitui o outro; eles resolvem direções opostas do mesmo problema de comunicação entre sistemas.
Os desafios que todo mundo subestima#
Até aqui, um webhook parece trivial: registre uma URL, receba um POST, leia o JSON. É exatamente essa aparência de simplicidade que faz tanta gente subestimar os problemas reais, que só aparecem quando o endpoint está exposto na internet e recebendo tráfego de verdade.
Segurança: sua URL é pública, então verifique a assinatura#
O endpoint de webhook é, por definição, uma URL pública que aceita POST de fora. Isso significa que qualquer pessoa que descubra ou adivinhe essa URL pode mandar um payload forjado — um falso "pagamento aprovado" para liberar um pedido sem pagar, por exemplo. Confiar cegamente no conteúdo de um payload só porque ele "parece" vir do provedor certo é o erro de segurança mais comum em integrações de webhook.
A defesa é verificar a assinatura HMAC que vem no header, sempre, antes de processar qualquer coisa. O provedor calcula um hash do corpo da requisição usando um segredo compartilhado, previamente combinado com você, e manda esse hash no header. Do seu lado, você recalcula o mesmo hash sobre os mesmos bytes recebidos, usando o mesmo segredo, e compara com o que veio no header. Se os dois hashes não baterem, o payload foi alterado ou não veio de quem diz ter vindo, e a requisição deve ser rejeitada sem processar nada. Um exemplo de verificação em Node.js:
```javascript const crypto = require("crypto");
function verificarAssinatura(payloadBruto, assinaturaRecebida, segredo) { const hashCalculado = crypto .createHmac("sha256", segredo) .update(payloadBruto) // bytes crus, antes do JSON.parse .digest("hex");
// comparação em tempo constante: evita vazar o hash por timing return crypto.timingSafeEqual( Buffer.from(hashCalculado), Buffer.from(assinaturaRecebida) ); } ```
Dois detalhes desse trecho não são acidente. A verificação usa o corpo bruto da requisição, antes de qualquer JSON.parse, porque a assinatura foi calculada sobre os bytes exatos que o provedor enviou — reformatar o JSON, mesmo sem mudar o conteúdo lógico, muda os bytes e quebra a comparação. E a comparação usa timingSafeEqual em vez de um simples ===, porque comparar strings caractere a caractere com early-exit vaza, por tempo de resposta, quantos caracteres iniciais bateram — uma diferença medível que um atacante paciente pode explorar para descobrir a assinatura correta aos poucos.
Entrega não confiável: o mesmo evento pode chegar mais de uma vez#
Provedores de webhook não têm garantia de que sua chamada chegou e foi processada com sucesso. Se o seu endpoint não responder com um status de sucesso (2xx) dentro de um tempo curto — timeout de rede, seu servidor caiu, seu servidor demorou demais —, o provedor assume que a entrega falhou e tenta de novo, geralmente várias vezes, com espaçamento crescente entre tentativas.
Isso cria uma possibilidade que muita gente não antecipa: o mesmo evento chegar mais de uma vez no seu endpoint. Pode acontecer de o seu processamento ter, na verdade, funcionado perfeitamente da primeira vez, mas a resposta 200 ter se perdido na rede antes de voltar ao provedor — do ponto de vista dele, a tentativa falhou, então ele reenvia um evento que você já processou. Se o seu código trata cada webhook recebido como um evento novo e o executa de novo sem checar nada, um pagamento aprovado pode liberar dois pedidos, ou enviar dois e-mails de confirmação, ou debitar duas vezes um saldo interno.
A solução é fazer do processamento uma operação idempotente, usando o id do evento para deduplicar. Antes de executar qualquer efeito colateral, o endpoint verifica se aquele id de evento já foi processado — uma tabela simples com o id como chave única resolve isso — e, se já foi, responde 200 sem repetir o trabalho. É o mesmo princípio de idempotência que se aplica a qualquer operação que pode ser tentada mais de uma vez pela mesma causa: a segunda execução com a mesma entrada não deve produzir um efeito adicional.
Responder rápido: valide, enfileire, processe depois#
O provedor espera uma resposta rápida, e "rápido" costuma significar poucos segundos. Se o seu endpoint segura a conexão aberta fazendo trabalho pesado — chamando outros serviços, gerando um PDF, mandando um e-mail de forma síncrona — antes de responder, corre o risco real de estourar o timeout do provedor. E um timeout, como visto acima, é tratado como falha de entrega, o que dispara um reenvio de um evento que, na verdade, ainda está sendo processado — abrindo caminho para duplicidade e para uma fila de retries crescendo sobre um endpoint que já está sobrecarregado.
A prática correta é separar recebimento de processamento. O handler do webhook faz o mínimo indispensável de forma síncrona — verificar a assinatura, validar o formato básico do payload, checar duplicidade pelo id do evento — e, assim que confirma que o evento é legítimo e novo, coloca o trabalho pesado numa fila (Redis, SQS, RabbitMQ, o que já existir no seu stack) e responde 200 imediatamente. Um worker separado consome essa fila no seu próprio ritmo, sem pressão de timeout externo. O endpoint de webhook vira, na prática, uma porta de entrada fina, não o lugar onde o trabalho de verdade acontece.
Ordem não garantida#
Eventos de webhook podem chegar fora da ordem em que aconteceram. Retries, múltiplos workers processando em paralelo do lado do provedor, e variação de latência de rede significam que um evento "pedido cancelado" pode, em casos raros, chegar ao seu endpoint antes de um "pedido criado" referente ao mesmo pedido, mesmo que a criação tenha ocorrido primeiro na realidade.
Depender da ordem de chegada para inferir a ordem real dos fatos é um erro sutil e comum. A correção é usar um timestamp ou um número de sequência que vem dentro do próprio payload — nunca a ordem de chegada na rede — para decidir qual é o estado mais recente. Um processamento que sempre sobrescreve o estado com "o que chegou por último" sem checar esse timestamp corre o risco de deixar um pedido marcado como criado depois de já ter sido cancelado.
Testar em ambiente de desenvolvimento local#
Um webhook exige uma URL pública, e localhost não é público — esse é o obstáculo prático número um de quem está integrando pela primeira vez. O provedor de eventos vive na internet e não tem como alcançar http://localhost:3000 rodando na sua máquina, então o fluxo natural de "recebi o evento, vejo no meu debugger" simplesmente não funciona sem um passo a mais.
A solução mais comum é um túnel que expõe a porta local com uma URL pública temporária, como ngrok ou Cloudflare Tunnel: a ferramenta cria um endereço público, do tipo https://abc123.ngrok.io, que encaminha todo tráfego recebido de volta para a porta local do seu servidor de desenvolvimento. Você cadastra esse endereço temporário no provedor enquanto desenvolve, e troca pela URL de produção real antes de publicar.
Muitos provedores também oferecem ferramentas de teste e replay direto no próprio painel, que permitem disparar manualmente um evento de exemplo para uma URL registrada, ou reenviar um evento real que já ocorreu, sem precisar esperar que a condição de negócio aconteça de novo. Vale a pena conhecer essa ferramenta específica de cada provedor antes de depender só de túnel — ela costuma ser mais rápida para testar casos específicos, como um payload malformado ou um tipo de evento raro.
Boas práticas de quem recebe#
- Endpoint dedicado por tipo de integração — não misture o webhook de pagamento com o de outro provedor no mesmo handler; cada integração tem seu próprio segredo e formato.
- Responda rápido, sempre
2xxpara eventos aceitos — valide, enfileire, responda; trabalho pesado vai para um worker assíncrono. - Verifique a assinatura em toda requisição, usando o corpo bruto e comparação em tempo constante, antes de tocar no conteúdo do payload.
- Deduplique pelo
iddo evento — trate cada processamento como idempotente, porque reenvio é esperado, não exceção. - Registre logs com o
iddo evento em cada etapa — recebido, validado, enfileirado, processado — para reconstruir o que aconteceu quando algo falhar na investigação.
Boas práticas de quem envia#
- Assine todo payload com um segredo por integração, e documente claramente o algoritmo e o formato do header para quem consome.
- Retries com backoff crescente, não imediato e infinito — um espaçamento que aumenta a cada tentativa evita bombardear um endpoint que já está com problema.
- Timeout curto na chamada de saída — não vale a pena segurar a conexão esperando um consumidor lento; é melhor falhar rápido e deixar o mecanismo de retry cuidar disso.
- Um painel de reenvio manual para o cliente da integração, permitindo disparar de novo um evento específico sem depender de suporte técnico.
Fechando#
Um webhook é, por baixo de tudo, apenas um POST HTTP — mas é um POST que inverte quem inicia a conversa: em vez de você perguntar repetidamente se algo mudou, o provedor avisa no instante em que muda, eliminando o desperdício e a latência do polling. Essa simplicidade de superfície esconde cinco problemas que só aparecem em produção: a URL pública exige verificação de assinatura antes de confiar em qualquer payload; a entrega não é garantida, então o mesmo evento pode chegar duas vezes e o processamento precisa ser idempotente; o provedor espera resposta rápida, então trabalho pesado vai para uma fila, não para dentro do handler; a ordem de chegada não reflete a ordem real dos fatos; e testar em desenvolvimento exige expor a máquina local através de um túnel. Nenhum desses cinco pontos é opcional — cada um, ignorado, vira um incidente específico e reconhecível: pedido duplicado, dado desatualizado sobrescrevendo um mais novo, ou uma URL forjada liberando algo que nunca foi pago. Tratar o webhook com o mesmo rigor de qualquer outra fronteira pública do seu sistema é o que separa uma integração que funciona na demonstração de uma que aguenta produção.
