Checklist de webhooks de pagamento idempotentes para atualizações de faturamento seguras
Checklist de webhooks de pagamento idempotentes para deduplicar eventos, lidar com reenvios e atualizar com segurança faturas, assinaturas e direitos.

Por que webhooks de pagamento geram atualizações duplicadas
Um webhook de pagamento é uma mensagem que seu provedor de pagamentos envia ao seu backend quando algo importante acontece, como um pagamento bem-sucedido, uma fatura paga, uma renovação de assinatura ou um reembolso. É basicamente o provedor dizendo: “Isso aconteceu. Atualize seus registros.”
Ocorrências duplicadas acontecem porque a entrega de webhooks é projetada para ser confiável, não para ser exatamente uma vez. Se seu servidor estiver lento, estourar tempo, retornar um erro ou ficar brevemente indisponível, o provedor geralmente tentará reenviar o mesmo evento. Você também pode ver dois eventos diferentes que se referem à mesma ação do mundo real (por exemplo, um evento de invoice e um evento de pagamento ligados ao mesmo pagamento). Eventos também podem chegar fora de ordem, especialmente com ações rápidas em sequência, como reembolsos.
Se seu handler não for idempotente, ele pode aplicar o mesmo evento duas vezes, o que vira problemas que clientes e times financeiros percebem imediatamente:
- Uma fatura marcada como paga duas vezes, criando lançamentos contábeis duplicados
- Uma renovação aplicada em duplicidade, estendendo o acesso além do devido
- Direitos (entitlements) concedidos duas vezes (créditos extras, assentos ou recursos)
- Reembolsos ou chargebacks que não reverteram o acesso corretamente
Isso não é apenas “boa prática”. É a diferença entre um faturamento que parece confiável e um faturamento que gera tickets de suporte.
O objetivo deste checklist é simples: trate cada evento recebido como “aplicar no máximo uma vez”. Você vai armazenar um identificador estável para cada evento, lidar com reenvios com segurança e atualizar faturas, assinaturas e direitos de forma controlada. Se você está construindo o backend em uma ferramenta no-code como o AppMaster, as mesmas regras ainda se aplicam: você precisa de um modelo de dados claro e um fluxo de handler repetível que se mantenha correto sob reenvios.
Conceitos básicos de idempotência que você pode aplicar a webhooks
Idempotência significa que processar a mesma entrada mais de uma vez produz o mesmo estado final. Em termos de faturamento: uma fatura termina marcada como paga uma vez, uma assinatura é atualizada uma vez e o acesso é concedido uma vez, mesmo se o webhook for entregue duas vezes.
Os provedores reenviam quando seu endpoint expira, retorna um 5xx ou a rede cai. Esses reenvios repetem o mesmo evento. Isso é diferente de um evento separado que representa uma mudança real, como um reembolso dias depois. Novos eventos têm IDs diferentes.
Para fazer isso funcionar, você precisa de duas coisas: identificadores estáveis e uma pequena “memória” do que você já viu.
Quais IDs importam (e o que armazenar)
A maioria das plataformas de pagamento inclui um event ID que é único para o evento do webhook. Algumas também incluem um request ID, idempotency key ou um ID único do objeto de pagamento (como um charge ou payment intent) dentro do payload.
Armazene o que te ajuda a responder a uma pergunta: “Já apliquei exatamente este evento?”
Um mínimo prático:
- Event ID (chave única)
- Tipo de evento (útil para depuração)
- Timestamp de recebimento
- Status de processamento (processed/failed)
- Referência ao cliente, fatura ou assinatura afetada
A jogada chave é armazenar o event ID em uma tabela com uma restrição única. Então seu handler pode fazer isso com segurança: inserir o event ID primeiro; se já existir, pare e retorne 200.
Por quanto tempo manter registros de dedupe
Mantenha registros de dedupe tempo suficiente para cobrir reenvios tardios e investigações. Uma janela comum é de 30 a 90 dias. Se você lida com chargebacks, disputas ou ciclos de assinatura mais longos, mantenha por mais tempo (6 a 12 meses) e faça purge de linhas antigas para que a tabela continue rápida.
Em um backend gerado como o AppMaster, isso mapeia bem para um modelo simples WebhookEvents com um campo único para o event ID, além de um Business Process que sai cedo quando detecta um duplicado.
Projete um modelo de dados simples para deduplicar eventos
Um bom handler de webhook é em grande parte um problema de dados. Se você conseguir registrar cada evento do provedor exatamente uma vez, tudo que vem depois fica mais seguro.
Comece com uma tabela que funciona como um registro de recibo. No PostgreSQL (incluindo quando modelado no Data Designer do AppMaster), mantenha-a pequena e estrita para que duplicatas falhem rápido.
O mínimo que você precisa
Aqui está uma linha de base prática para uma tabela webhook_events:
provider(texto, como "stripe")provider_event_id(texto, obrigatório)status(texto, como "received", "processed", "failed")processed_at(timestamp, nullable)raw_payload(jsonb ou texto)
Adicione uma restrição única em (provider, provider_event_id). Essa regra única é sua defesa principal contra duplicidade.
Você também vai querer os IDs de negócio que usa para localizar os registros a serem atualizados. Estes são diferentes do event ID do webhook.
Exemplos comuns incluem customer_id, invoice_id e subscription_id. Mantenha-os como texto porque provedores frequentemente usam IDs não numéricos.
Payload bruto vs campos parseados
Armazene o payload bruto para depurar e reprocessar depois. Campos parseados facilitam consultas e relatórios, mas só armazene o que você realmente usa.
Uma abordagem simples:
- Sempre armazene
raw_payload - Também armazene alguns IDs parseados que você consulta com frequência (customer, invoice, subscription)
- Armazene um
event_typenormalizado (texto) para filtragem
Se um evento invoice.paid chegar duas vezes, sua restrição única bloqueia a segunda inserção. Você ainda conserva o payload bruto para auditoria, e o invoice ID parseado facilita localizar a fatura que você atualizou na primeira vez.
Passo a passo: um fluxo seguro de handler de webhook
Um handler seguro é propositalmente simples. Ele se comporta do mesmo jeito sempre, mesmo quando o provedor reenvia o mesmo evento ou entrega eventos fora de ordem.
O fluxo de 5 passos a seguir sempre
-
Verifique a assinatura e faça o parse do payload. Rejeite requisições que falhem na verificação de assinatura, tenham um tipo de evento inesperado ou não possam ser parseadas.
-
Grave o registro do evento antes de tocar nos dados de faturamento. Salve o provider event ID, tipo, hora de criação e o payload bruto (ou um hash). Se o event ID já existir, trate como duplicado e pare.
-
Mapeie o evento para um único registro “dono”. Decida o que você vai atualizar: invoice, subscription ou customer. Armazene IDs externos nos seus registros para que possa buscá-los diretamente.
-
Aplique uma mudança de estado segura. Apenas avance o estado. Não reverta uma fatura para não paga porque chegou um
invoice.updatedtardio. Registre o que foi aplicado (estado antigo, novo estado, timestamp, event ID) para auditoria. -
Responda rapidamente e registre o resultado. Retorne sucesso assim que o evento estiver salvo com segurança e processado ou ignorado. Registre se foi processado, deduplicado ou rejeitado, e por quê.
No AppMaster, isso geralmente se torna uma tabela de banco de dados para eventos de webhook mais um Business Process que checa “event ID visto?” e então executa os passos mínimos de atualização.
Lidando com reenvios, timeouts e entrega fora de ordem
Os provedores reenviam webhooks quando não recebem uma resposta de sucesso rápida. Eles também podem enviar eventos fora de ordem. Seu handler precisa permanecer seguro quando a mesma atualização chega duas vezes, ou quando uma atualização posterior chega primeiro.
Uma regra prática: responda rápido, faça o trabalho depois. Trate a requisição do webhook como um recibo, não como um lugar para executar lógica pesada. Se você chama APIs de terceiros, gera PDFs ou recalcula contas dentro da requisição, aumenta tempos de resposta e dispara mais reenvios.
Fora de ordem: mantenha a verdade mais recente
Entrega fora de ordem é normal. Antes de aplicar qualquer mudança, use duas verificações:
- Compare timestamps: aplique um evento apenas se ele for mais novo que o que você já armazenou para aquele objeto (invoice, subscription, entitlement).
- Use prioridade de status quando timestamps estiverem próximos ou incertos: paid vence open, canceled vence active, refunded vence paid.
Se você já registrou uma fatura como paga e chega um open tardio, ignore-o. Se você recebeu canceled e depois aparece um active mais antigo, mantenha canceled.
Ignorar vs enfileirar
Ignore um evento quando puder provar que ele está obsoleto ou já foi aplicado (mesmo event ID, timestamp mais antigo, prioridade de status menor). Enfileire um evento quando ele depender de dados que você ainda não tem, como uma atualização de subscription chegando antes do registro do cliente existir.
Um padrão prático:
- Armazene o evento imediatamente com um estado de processamento (received, processing, done, failed)
- Se dependências estiverem faltando, marque como waiting e processe novamente em background
- Defina um limite de reintentos e alerte após falhas repetidas
No AppMaster, isso se encaixa bem numa tabela de webhook events mais um Business Process que reconhece a requisição rapidamente e processa eventos enfileirados de forma assíncrona.
Atualizando invoices, subscriptions e entitlements com segurança
Depois de tratar a deduplicação, o próximo risco é estado dividido: a fatura diz paga, mas a assinatura ainda está em atraso, ou o acesso foi concedido duas vezes e nunca revogado. Trate cada webhook como uma transição de estado e aplique-a em uma única atualização atômica.
Faturas: torne as mudanças de status monotônicas
Faturas podem passar por estados como paid, voided e refunded. Você também pode ver pagamentos parciais. Não “alterne” uma fatura com base no último evento que chegou. Armazene o status atual e totais-chave (amount_paid, amount_refunded) e permita apenas transições seguras para frente.
Regras práticas:
- Marque uma fatura como paga apenas uma vez, na primeira vez que vir um evento paid.
- Para reembolsos, aumente
amount_refundedaté o total da fatura; nunca diminua. - Se uma fatura for voided, pare ações de fulfill, mas mantenha o registro para auditoria.
- Para pagamentos parciais, atualize valores sem conceder benefícios de “totalmente paga”.
Subscriptions e entitlements: conceda uma vez, revogue uma vez
Assinaturas incluem renovações, cancelamentos e períodos de carência. Mantenha o status da subscription e os limites de período (current_period_start/end), e então derive janelas de entitlement a partir desses dados. Entitlements devem ser registros explícitos, não apenas um booleano.
Para controle de acesso:
- Uma concessão de entitlement por usuário por produto por período
- Um registro de revogação quando o acesso termina (cancelamento, reembolso, chargeback)
- Uma trilha de auditoria que registre qual evento de webhook causou cada mudança
Use uma transação para evitar estados divididos
Aplique updates de invoice, subscription e entitlement em uma única transação de banco de dados. Leia as linhas atuais, verifique se este evento já foi aplicado e então escreva todas as mudanças juntas. Se algo falhar, faça rollback para não ficar com “fatura paga” mas “sem acesso”, ou o inverso.
No AppMaster, isso frequentemente mapeia para um único fluxo de Business Process que atualiza o PostgreSQL em um caminho controlado e escreve uma entrada de auditoria junto com a mudança de negócio.
Segurança e verificações de integridade para endpoints de webhook
A segurança dos webhooks faz parte da correção. Se um atacante pode atingir seu endpoint, ele pode tentar criar estados de “pago” falsos. Mesmo com deduplicação, você ainda precisa provar que o evento é real e manter os dados do cliente seguros.
Verifique o remetente antes de tocar nos dados de faturamento
Valide a assinatura em toda requisição. Para Stripe, isso normalmente significa checar o cabeçalho Stripe-Signature, usando o corpo bruto da requisição (não um JSON reescrito), e rejeitar eventos com timestamp antigo. Trate cabeçalhos ausentes como falha crítica.
Valide o básico cedo: método HTTP correto, Content-Type e campos obrigatórios (event id, type e o object id que você usará para localizar uma fatura ou assinatura). Se você construir isso no AppMaster, mantenha o segredo de assinatura em variáveis de ambiente ou configuração segura, nunca no banco de dados ou no código cliente.
Um checklist rápido de segurança:
- Rejeitar requisições sem assinatura válida e timestamp recente
- Exigir cabeçalhos e content type esperados
- Usar acesso ao banco com menor privilégio possível para o handler de webhook
- Armazenar segredos fora das tabelas (env/config) e rotacionar quando necessário
- Retornar 2xx apenas depois de persistir o evento com segurança
Mantenha logs úteis sem vazar segredos
Logue o suficiente para depurar reenvios e disputas, mas evite valores sensíveis. Armazene um subconjunto seguro de PII: provider customer ID, ID interno do usuário e talvez um e-mail mascarado (como a***@domain.com). Nunca armazene dados completos de cartão, endereços completos ou cabeçalhos de autorização brutos.
Logue o que ajuda a reconstruir o que aconteceu:
- Provider event id, tipo, hora de criação
- Resultado da verificação (signature ok/failed) sem armazenar a assinatura
- Decisão de dedupe (novo vs já processado)
- IDs internos tocados (invoice/subscription/entitlement)
- Motivo do erro e contagem de reintentos (se enfileirar reenvios)
Adicione proteção básica contra abuso: rate limit por IP e (quando possível) por customer ID, e considere permitir apenas intervalos de IPs conhecidos do provedor se sua infraestrutura suportar.
Erros comuns que causam cobranças em duplicidade ou acesso duplicado
A maioria dos bugs de faturamento não é sobre contas. Acontecem quando você trata a entrega de webhook como uma mensagem única e confiável.
Erros que frequentemente levam a atualizações duplicadas:
- Deduplicar por timestamp ou valor em vez de event ID. Eventos diferentes podem ter o mesmo valor, e reenvios podem chegar minutos depois. Use o event ID único do provedor.
- Atualizar o banco antes de verificar a assinatura. Verifique primeiro, depois parseie e então aja.
- Tratar todo evento como fonte da verdade sem checar o estado atual. Não marque uma fatura como paga cegamente se ela já estiver paga, reembolsada ou voided.
- Criar múltiplos entitlements para a mesma compra. Reenvios podem criar linhas duplicadas. Prefira um upsert como “garantir entitlement existe para subscription_id”, então atualize datas/limites.
- Falhar no webhook porque um serviço de notificação está fora. Email, SMS, Slack ou Telegram não devem bloquear faturamento. Enfileire notificações e ainda retorne sucesso depois que as mudanças centrais do faturamento forem salvas com segurança.
Um exemplo simples: um evento de renovação chega duas vezes. A primeira entrega cria uma linha de entitlement. O reenvio cria uma segunda linha, e sua aplicação vê “dois entitlements ativos” e concede assentos ou créditos extras.
No AppMaster, a correção é em grande parte sobre fluxo: verifique primeiro, insira o registro do evento com uma restrição única, aplique atualizações de faturamento com checagens de estado e empurre efeitos colaterais (emails, recibos) para passos assíncronos para que não provoquem uma tempestade de reenvios.
Exemplo realista: renovação duplicada + reembolso posterior
Esse padrão parece assustador, mas é administrável se seu handler for seguro para executar novamente.
Um cliente está em um plano mensal. Stripe envia um evento de renovação (por exemplo, invoice.paid). Seu servidor recebe, atualiza o banco, mas demora demais para retornar 200 (cold start, banco ocupado). Stripe assume que falhou e reenvia o mesmo evento.
Na primeira entrega, você concede acesso. No reenvio, você detecta que é o mesmo evento e não faz nada. Mais tarde, chega um evento de reembolso (por exemplo, charge.refunded) e você revoga o acesso uma vez.
Aqui está uma maneira simples de modelar estado no banco (tabelas que você pode criar no AppMaster Data Designer):
webhook_events(event_id UNIQUE, type, processed_at, status)invoices(invoice_id UNIQUE, subscription_id, status, paid_at, refunded_at)entitlements(customer_id, product, active, valid_until, source_invoice_id)
Como o banco deve ficar após cada evento
Depois do Evento A (renovação, primeira entrega): webhook_events ganha uma nova linha para event_id=evt_123 com status=processed. invoices é marcada como paga. entitlements.active=true e valid_until avança um período de cobrança.
Depois do Evento A novamente (renovação, reenvio): a inserção em webhook_events falha (unique event_id) ou seu handler vê que já foi processado. Nenhuma mudança em invoices ou entitlements.
Depois do Evento B (reembolso): uma nova linha em webhook_events para event_id=evt_456. invoices.refunded_at é definido e status=refunded. entitlements.active=false (ou valid_until é ajustado para agora) usando source_invoice_id para revogar o acesso correto uma vez.
O detalhe importante é o timing: a checagem de dedupe acontece antes de qualquer gravação de concessão ou revogação.
Checklist rápido pré-lançamento
Antes de ativar webhooks em produção, você quer prova de que um evento do mundo real atualiza registros de faturamento exatamente uma vez, mesmo se o provedor enviá-lo duas vezes (ou dez vezes).
Use este checklist para validar seu setup de ponta a ponta:
- Confirme que todo evento recebido é salvo primeiro (payload bruto, event id, tipo, hora de criação e resultado da verificação de assinatura), mesmo que passos posteriores falhem.
- Verifique que duplicados são detectados cedo (mesmo provider event id) e o handler sai sem alterar invoices, subscriptions ou entitlements.
- Prove que a atualização de negócio ocorre uma única vez: uma mudança de status da fatura, uma mudança de estado da assinatura, uma concessão ou revogação de entitlement.
- Garanta que falhas são registradas com detalhes suficientes para replay seguro (mensagem de erro, passo que falhou, status de retry).
- Teste que seu handler responde rápido: reconheça o recebimento depois de armazenado e evite trabalho lento dentro da requisição.
Você não precisa de uma grande infra de observabilidade para começar, mas precisa de sinais. Acompanhe estes via logs ou dashboards simples:
- Pico em entregas duplicadas (normal, mas grandes saltos podem sinalizar timeouts ou problemas no provedor)
- Alta taxa de erro por tipo de evento (por exemplo, payment failed)
- Crescente backlog de eventos presos em retry
- Checks de inconsistência (fatura paga mas entitlement ausente, assinatura revogada mas acesso ainda ativo)
- Aumento repentino no tempo de processamento
Se você está construindo isso no AppMaster, mantenha o armazenamento de eventos em uma tabela dedicada no Data Designer e faça de “marcar processado” um ponto único e atômico no seu Business Process.
Próximos passos: testar, monitorar e montar em um backend no-code
Testar é onde a idempotência se prova. Não rode apenas o caminho feliz. Reproduza o mesmo evento várias vezes, envie eventos fora de ordem e force timeouts para que o provedor reenvie. A segunda, terceira e décima entrega não devem mudar nada.
Planeje backfilling desde cedo. Mais cedo ou mais tarde você vai querer reprocessar eventos passados após uma correção de bug, mudança de schema ou incidente do provedor. Se seu handler for verdadeiramente idempotente, o reprocessamento vira “replay de eventos pelo mesmo pipeline” sem criar duplicatas.
O suporte também precisa de um pequeno runbook para que problemas não vire guesswork:
- Encontre o event ID e verifique se está registrado como processed.
- Cheque o registro da invoice ou subscription e confirme o estado e timestamps esperados.
- Revise o registro de entitlement (qual acesso foi concedido, quando e por quê).
- Se necessário, reexecute o processamento para aquele único event ID em modo de reprocessamento seguro.
- Se os dados estiverem inconsistentes, aplique uma ação corretiva única e registre-a.
Se quiser implementar isso sem escrever muito boilerplate, AppMaster (appmaster.io) permite modelar as tabelas centrais e construir o fluxo de webhook em um Business Process visual, enquanto ainda gera código-fonte real para o backend.
Tente construir o handler de webhook de ponta a ponta em um backend gerado no-code e garanta que ele se mantenha seguro sob reenvios antes de escalar tráfego e receita.
FAQ
As entregas duplicadas de webhooks são normais porque os provedores priorizam pelo menos uma vez na entrega. Se seu endpoint expira, retorna um 5xx ou a conexão cai brevemente, o provedor reenvia o mesmo evento até receber uma resposta bem-sucedida.
Use o ID de evento único do provedor (o identificador do evento do webhook), não o valor da fatura, a data ou o e-mail do cliente. Armazene esse ID com uma restrição de unicidade para que um reenvio seja detectado imediatamente e ignorado com segurança.
Insira o registro do evento primeiro, antes de atualizar invoices, subscriptions ou entitlements. Se a inserção falhar porque o event ID já existe, pare o processamento e retorne sucesso para que reenvios não criem atualizações duplicadas.
Mantenha-os tempo suficiente para cobrir reenvios tardios e suportar investigações. Um padrão prático é 30–90 dias, e mais longo (por exemplo, 6–12 meses) se você lida com disputas, chargebacks ou ciclos de assinatura longos; depois faça purge de linhas antigas para manter as consultas rápidas.
Verifique a assinatura antes de tocar nos dados de faturamento, depois faça o parse e valide os campos necessários. Se a verificação de assinatura falhar, rejeite a requisição; a deduplicação não protege contra eventos forjados de “pagamento”.
Prefira reconhecer rapidamente após o evento ser armazenado com segurança e mova trabalhos pesados para processamento em background. Handlers lentos aumentam timeouts, o que causa mais reenvios e eleva a chance de atualizações duplicadas se algo não for totalmente idempotente.
Aplique apenas mudanças que avancem o estado e ignore eventos obsoletos. Use timestamps dos eventos quando disponíveis e uma prioridade simples de status (por exemplo, refunded não deve ser sobrescrito por paid, e canceled não deve ser sobrescrito por active).
Não crie uma nova linha de entitlement a cada evento. Use uma regra estilo upsert: “garantir um entitlement por usuário/produto/período (ou por subscription)”, então atualize datas/limites e registre qual event ID causou a mudança para auditoria.
Grave mudanças de invoice, subscription e entitlement em uma única transação de banco de dados para que tudo seja aplicado ou revertido junto. Isso evita estados divididos como “fatura paga” mas “sem acesso concedido”, ou “acesso revogado” sem um registro de reembolso correspondente.
Sim. Crie um modelo WebhookEvents com um event ID único e depois um Business Process que verifica “já visto?” e sai cedo. Modele invoices/subscriptions/entitlements explicitamente no Data Designer para que reenvios e replays não criem linhas duplicadas.


