Eventos de webhook do Stripe: quais seu app precisa tratar

Quais eventos de webhook do Stripe seu app precisa tratar: checkout, assinatura, reembolso e disputa, e como lidar com duplicatas e entrega fora de ordem.

Envelopes flying into labeled mail slots, one orange-sealed event caught while duplicate stripe webhook events fade behind
Nesta página · 7 seções
  1. Quais eventos de webhook do Stripe um checkout simples precisa?
  2. Quais eventos importam para assinaturas e renovações que falham?
  3. E os reembolsos e disputas?
  4. Por que os eventos chegam fora de ordem ou mais de uma vez?
  5. Como torno meu handler de eventos idempotente?
  6. Quais eventos devo ignorar?
  7. Quer uma segunda opinião sobre seu handler de webhooks?

Principais pontos

  • Comece com uma lista curta de eventos e adicione outros só quando uma funcionalidade precisar.
  • Libere acesso a partir dos webhooks, não da página de sucesso para onde o cliente é redirecionado.
  • O Stripe pode enviar o mesmo evento duas vezes e em qualquer ordem, então guarde os IDs dos eventos e torne os handlers idempotentes.
  • Assinaturas precisam de eventos de falha de pagamento e de cancelamento, não só do primeiro pagamento.
  • Reembolsos e disputas precisam atualizar seus próprios registros, senão sua contabilidade e o controle de acesso se desencontram.
  • Ignore os eventos sobre os quais você não age, mas ainda assim retorne 200 para eles.

Os eventos de webhook do Stripe que seu app precisa tratar dependem do que você vende, mas o conjunto essencial é pequeno: checkout.session.completed (ou payment_intent.succeeded) para pagamentos avulsos, invoice.paid e invoice.payment_failed para assinaturas, customer.subscription.updated e customer.subscription.deleted para controle de acesso, e charge.refunded mais charge.dispute.created para o dinheiro que volta.

O resto é opcional. O trabalho de verdade não é assinar muitos eventos, é tratar corretamente os poucos de que você precisa: verificá-los, tolerar duplicatas e entrega fora de ordem, e nunca confiar no navegador para dizer que um pagamento deu certo. Essa é a lógica que scanners de segurança genéricos não pegam, e apps construídos com IA costumam errar.

Quais eventos de webhook do Stripe um checkout simples precisa?

Para uma compra avulsa pelo Stripe Checkout, o evento principal é o checkout.session.completed. Ele avisa que o cliente concluiu a sessão. Se você usa meios de pagamento com confirmação posterior, como débito bancário, escute também checkout.session.async_payment_succeeded e checkout.session.async_payment_failed, porque concluído nem sempre significa pago. Confira o campo payment_status da sessão antes de entregar o pedido.

Se você monta seu próprio fluxo com Payment Intents, use payment_intent.succeeded e payment_intent.payment_failed.

  • checkout.session.completed: crie o pedido, libere o acesso, envie o recibo.
  • checkout.session.async_payment_succeeded: entregue os pedidos que estavam pendentes.
  • checkout.session.async_payment_failed: cancele o pedido pendente e avise o usuário.
  • checkout.session.expired: libere o estoque reservado, se você reservar algum.

Um erro comum que vejo em código gerado por IA é entregar o pedido no redirecionamento para a URL de sucesso. Um usuário pode chegar a essa URL sem pagar, ou fechar a aba antes de chegar nela. O webhook é a fonte da verdade.

Quais eventos importam para assinaturas e renovações que falham?

É nas assinaturas que mora a maioria dos bugs, porque o primeiro pagamento é só o começo. As renovações acontecem semanas depois, sem nenhum usuário presente, e seu app só fica sabendo por meio dos eventos.

O conjunto mínimo para assinaturas

  • checkout.session.completed: vincule os IDs de cliente e de assinatura do Stripe ao seu usuário.
  • invoice.paid: o pagamento funcionou; estenda o acesso pelo novo período.
  • invoice.payment_failed: a renovação falhou; avise o usuário e marque a conta como inadimplente.
  • customer.subscription.updated: mudanças de plano, cancelamento agendado, mudanças de status como past_due ou unpaid.
  • customer.subscription.deleted: a assinatura terminou; remova o acesso.

Decida o que significa uma falha

O Stripe tenta de novo os pagamentos que falharam de acordo com as configurações de retentativa no dashboard. Seu app precisa de uma política: o usuário perde o acesso na primeira falha ou depois que as retentativas se esgotam? Escreva isso e depois mapeie para o status da assinatura. Prefiro derivar o acesso do campo de status da assinatura em vez de uma pilha de flags atualizadas por eventos individuais, porque é mais fácil de entender e de consertar.

Considere também o customer.subscription.trial_will_end se você oferece período de teste. Ele dispara alguns dias antes do fim do teste, um bom momento para mandar um email ao usuário.

Linha do tempo de eventos de pagamento fluindo de um provedor de pagamentos para um servidor de aplicação
Cada evento é uma mensagem sobre algo que já aconteceu no Stripe, não um comando.

E os reembolsos e disputas?

O dinheiro pode sair depois da venda, e seu app deveria perceber. Dois grupos de eventos cobrem isso.

  • charge.refunded: foi emitido um reembolso total ou parcial, muitas vezes pelo dashboard, por alguém do seu time. Atualize o pedido e decida se vai revogar acesso ou créditos.
  • charge.dispute.created: um cliente contestou a cobrança junto ao banco. Sinalize a conta, reúna evidências e considere pausar a entrega.
  • charge.dispute.closed: o resultado saiu (ganha ou perdida). Atualize seus registros de acordo.

Talvez você nunca automatize isso por completo, e tudo bem. Mesmo um handler simples que grava o evento numa tabela e te manda um email é melhor que o silêncio. Sem ele, um cliente reembolsado continua com um plano pago, ou você só descobre uma disputa quando o prazo do Stripe para enviar evidências já está quase no fim. Os prazos de disputa são reais, então confira a documentação atual do Stripe sobre os prazos.

Por que os eventos chegam fora de ordem ou mais de uma vez?

A documentação do Stripe afirma que ele não garante a ordem de entrega dos eventos, e que os endpoints podem receber o mesmo evento mais de uma vez. As retentativas acontecem quando seu servidor estoura o tempo limite ou retorna um status diferente de 2xx. Então as premissas que muitos handlers assumem estão erradas:

  • Você pode receber customer.subscription.updated antes de checkout.session.completed.
  • Você pode receber invoice.paid antes de seu banco de dados saber que a assinatura existe.
  • Você pode receber o mesmo checkout.session.completed duas vezes, criando dois pedidos.

Trabalhando no time de carrinho e checkout da Total Wine & More, um varejista americano com 190 milhões de visitas por ano no site, aprendi a tratar mensagens duplicadas e atrasadas como uma condição normal, não um caso extremo. Nessa escala, tudo que pode acontecer duas vezes vai acontecer. Apps pequenos também passam por isso, só que com menos frequência, e costuma aparecer como um pedido duplicado justo na semana em que chega o primeiro tráfego de verdade.

Defesas práticas

  • Retorne um 200 rápido e faça o trabalho pesado num job em segundo plano, para o Stripe não tentar de novo porque você demorou.
  • Quando um evento referencia algo que você ainda não tem, busque o objeto na API do Stripe ou falhe com um status que permita nova tentativa.
  • Não confie no payload como a verdade mais recente. Para decisões críticas, recupere o objeto atual no Stripe.
Envelopes duplicados chegando a uma caixa de correio, com um deles carimbado como já processado
O mesmo evento pode aparecer duas vezes, então o handler precisa reconhecer o que já fez.

Como torno meu handler de eventos idempotente?

Idempotente significa que rodar o handler duas vezes tem o mesmo resultado que rodá-lo uma vez. O padrão confiável mais simples usa o ID do evento, que é único por evento.

  1. Verifique a assinatura primeiro, usando o corpo bruto da requisição e o secret do seu endpoint. Rejeite tudo que falhar.
  2. Insira o ID do evento numa tabela com uma restrição de unicidade, na mesma transação de banco de dados das suas alterações.
  3. Se o insert falhar porque o ID já existe, retorne 200 e não faça nada.
  4. Caso contrário, aplique a alteração e faça o commit.

A restrição de unicidade importa mais do que um if no código. Duas entregas que chegam no mesmo instante podem passar as duas por uma checagem de leitura, mas só uma consegue vencer o insert.

Adicione uma segunda camada nos objetos de negócio: guarde o ID da sessão ou do payment intent do Stripe no pedido, com uma restrição de unicidade, para que nem um bug no controle de eventos consiga criar dois pedidos para um único pagamento. Escreva os handlers como mudanças de estado (definir o status como pago) em vez de incrementos (somar 30 créditos) sempre que possível; se precisar conceder créditos, amarre a concessão ao ID do evento ou da fatura.

Se seu app roda em edge functions do Supabase ou numa estrutura parecida, as mesmas regras valem. O banco de dados é o lugar certo para garantir unicidade, e a função deve ser tratada como algo que pode rodar duas vezes. Para um exemplo real de risco no nível da plataforma, leia sobre a segurança do Base44 e a descoberta da Wiz.

Quais eventos devo ignorar?

O Stripe emite dezenas de tipos de evento. No dashboard você escolhe quais vão para cada endpoint, então assine apenas os que você trata. Uma lista menor significa menos ruído, menos falhas e menos coisa para testar.

Geralmente desnecessários para um app básico:

  • customer.created, customer.updated e a maioria dos eventos de payment_method, a não ser que você os sincronize de propósito.
  • charge.succeeded e payment_intent.created quando você já age sobre os eventos de checkout ou de fatura. Tratar os dois leva a entregas em dobro.
  • invoice.created, invoice.finalized e similares, a não ser que você personalize as faturas.
  • Eventos de produtos e preços, se você os gerencia só pelo dashboard.

Se um tipo inesperado chegar ao seu endpoint mesmo assim, registre no log e retorne 200. Retornar erro para eventos desconhecidos provoca retentativas e pode levar o Stripe a desativar o endpoint depois de falhas repetidas.

Um cuidado: ignorar um evento é uma decisão, não um acidente. Mantenha no repositório uma lista curta de quais eventos você trata e por quê, para que o próximo desenvolvedor, ou o próximo prompt de IA, não mude o comportamento sem ninguém perceber.

Quer uma segunda opinião sobre seu handler de webhooks?

Se quiser um segundo par de olhos na sua configuração do Stripe, mande um email para me@filipeeduardo.dev com uma descrição ou o código do handler, e eu te digo o que eu verificaria primeiro.

Perguntas frequentes

Preciso de webhooks se uso o Stripe Checkout?

Sim. O redirecionamento para sua página de sucesso não é prova de pagamento. Os webhooks são a forma confiável de saber que um pagamento foi concluído, falhou ou foi reembolsado.

Qual é o evento de webhook do Stripe mais importante?

Para vendas avulsas, é o checkout.session.completed, com uma checagem do status do pagamento. Para assinaturas, é o invoice.paid, porque ele conduz as renovações, com o invoice.payment_failed logo atrás.

Como evito pedidos duplicados vindos de webhooks?

Guarde cada ID de evento com uma restrição de unicidade e processe-o na mesma transação da sua alteração. Como segunda proteção, adicione uma restrição de unicidade no ID do pagamento ou da sessão do Stripe no pedido.

O que meu endpoint deve retornar?

Um status 2xx assim que o evento for verificado e registrado com segurança. Retorne erro só quando quiser que o Stripe tente de novo, como numa falha temporária do banco de dados. Confira a documentação do Stripe para o comportamento atual das retentativas.

Um construtor de apps com IA consegue configurar isso corretamente?

Ele consegue gerar um primeiro rascunho que funciona, mas trate como rascunho. Revise a verificação de assinatura, a idempotência e as regras de acesso antes que os pagamentos reais cheguem.

stripe webhook events

Filipe Eduardo

Engenheiro de Software Sênior. Sete anos construindo produtos web e mobile de ponta a ponta e liderando os times que os entregam.

Acompanhe

Quer conversar
sobre isso?

Trabalho com problemas assim todos os dias. Me conte o seu.

Fale comigo