Por que o mesmo webhook pode rodar duas vezes
Os provedores repetem as entregas, então o seu receptor pode ver o mesmo evento mais de uma vez. A Stripe diz isso na documentação e sugere registrar os ids de evento que você já processou (Stripe: eventos duplicados). Quando o seu endpoint demora demais ou devolve um erro, o provedor envia o evento de novo, e a Stripe continua repetindo uma entrega em modo live por até três dias, com espera exponencial (Stripe: retentativas automáticas). Se nada lembra que o evento já foi tratado, o trabalho por trás roda de novo: uma segunda cobrança, um segundo pedido, uma segunda linha no CRM. Numa loja, um webhook que roda duas vezes pode enviar duas remessas ou dois cupons para um único pedido.
Uma chave estável, reservada uma única vez
A correção é uma chave que não muda entre retentativas e que é reservada uma única vez. Cada retentativa da Stripe traz uma assinatura e um carimbo de tempo novos, então o cabeçalho stripe-signature não pode ser a chave. O id do evento pode: o modelo usa evt: mais o id do corpo e, quando o corpo não tem id, usa sha256: mais o SHA-256 do corpo bruto. A reserva é uma única instrução sobre uma chave primária, INSERT ... ON CONFLICT (key) DO NOTHING RETURNING key, então só uma entrega recebe a linha, mesmo quando duas chegam ao mesmo tempo. Se volta uma linha, o passo de negócio roda. Se não volta nenhuma, o evento já tinha sido visto: o receptor responde 200 mesmo assim, e a cobrança, o pedido ou a linha do CRM não são criados uma segunda vez. A ordem das verificações também importa para a segurança. Verifique primeiro a assinatura do provedor (Stripe: verificar eventos) e depois reserve a chave, para que uma requisição forjada não consiga reservar um id de evento real. O modelo não verifica assinaturas, então essa verificação fica antes dele.
O que aconteceu quando rodamos
Rodamos o modelo de n8n abaixo contra Postgres na nossa própria máquina, com n8n 2.42.6 e Postgres 16. Enviamos o evento A, depois o evento A de novo com um cabeçalho de assinatura diferente, depois o evento B. O n8n registrou 3 execuções e a tabela de negócio recebeu 2 linhas. A repetição de A recebeu uma resposta 200 e não rodou o passo de negócio, então a retentativa foi respondida e nada foi gravado duas vezes. Depois enviamos duas vezes um corpo sem id: a chave foi o SHA-256 do corpo bruto, e a segunda chamada seguiu o ramo de duplicado. Num teste separado, enviamos ao Postgres 8 reservas simultâneas da mesma chave com o SQL que esta página gera: uma recebeu a linha, e o passo de negócio rodou uma vez.
Como levar isso para o n8n
Quem cuida do n8n pode importar o fluxo que exportamos depois desse teste, sem o id de credencial do laboratório, apontá-lo para o Postgres e trocar o passo de negócio pelos próprios nós. O nó Webhook usa o modo de resposta "Using Respond to Webhook node", então quem responde é o ramo que rodou. O nó Code monta a chave, um nó Postgres faz a reserva, e "Row Returned?" manda um evento novo para o passo de negócio e uma repetição para "Respond 200 duplicate".
Modelo de fluxo do n8n (Webhook, Code, Postgres, IF, Respond to Webhook)
O fluxo exportado como arquivo .json, pronto para importar.
-
Crie as tabelas
CREATE TABLE IF NOT EXISTS processed_events ( key text PRIMARY KEY, first_seen timestamptz NOT NULL DEFAULT now() ); -- Tabela de negócio de exemplo usada pelo modelo. Troque pelo seu próprio passo. CREATE TABLE IF NOT EXISTS business_log ( id bigserial PRIMARY KEY, event_key text NOT NULL, event_type text, created_at timestamptz NOT NULL DEFAULT now() );
-
Configure o ambiente e as credenciais
O n8n auto-hospedado bloqueia por padrão os módulos nativos no nó Code. Defina
NODE_FUNCTION_ALLOW_BUILTIN=cryptono contêiner do n8n para querequire('crypto')funcione. Depois de importar, abra os dois nós Postgres e escolha a sua credencial do Postgres (host, porta, banco de dados, usuário, senha). O n8n também pode salvar dados de execução, incluindo o corpo do webhook, conforme as suas configurações de execução. Se o corpo tiver dados pessoais, revise essas configurações e a retenção delas. -
Importe, conecte o seu passo, publique
No n8n, importe o arquivo .json. Troque "Business Step (insert business_log)" pelos seus próprios nós, mantenha-os no ramo true de "Row Returned?" e publique o fluxo. Coloque a sua verificação de assinatura antes do nó Code se o seu provedor assina as requisições.
-
Teste com uma entrega repetida
Envie o mesmo evento duas vezes e um evento diferente uma vez. Em Executions você deve ver a primeira e a terceira execução passarem pelo passo de negócio, e a segunda seguir "Respond 200 duplicate".
-
Limpe as chaves antigas
A limpeza é um passo separado, fora do fluxo do webhook. Guarde as chaves por mais tempo do que o provedor consegue reenviar. A Stripe permite um reenvio manual pelo Dashboard por até 15 dias e pela CLI por até 30 dias (Stripe: retentativas manuais). Um fluxo agendado do n8n ou uma tarefa cron pode rodar:
DELETE FROM processed_events WHERE first_seen < now() - interval '30 days';
Limite: a chave é reservada antes de o passo de negócio rodar. Se o passo de negócio falhar depois da reserva, uma retentativa desse evento é tratada como duplicada. Adicione um caminho de erro que rode DELETE FROM processed_events WHERE key = $1 para essa chave, ou corrija e reenvie o evento manualmente.
O que é guardado e o que fica no seu navegador
A chave desta página é calculada com crypto.subtle.digest no seu navegador, e o script da página não faz nenhuma requisição de rede com o que você cola. No Postgres, processed_events guarda a chave e um carimbo de tempo. A chave é o id do evento do provedor, ou um SHA-256 do corpo quando não há id. A tabela de exemplo business_log guarda a chave e o tipo de evento, e o seu próprio passo de negócio decide o que mais é guardado. O planejador acima gera a mesma reserva que o modelo do n8n: INSERT ... ON CONFLICT (key) DO NOTHING RETURNING key, um ramo conforme a linha devolvida e um DELETE de limpeza separado. Estas notas descrevem o código desta página e do modelo. Não são uma certificação.