# Pagou mas continua bloqueado: a noite em que o webhook não chegou

> No Stripe o pagamento estava verde. No banco, o plano ainda era grátis. Esse vão é onde quem lança SaaS sozinho perde uma tarde que não tinha.

O e-mail de suporte chegou às 22h47. Assunto: "Paguei, por que ainda estou no plano grátis?"

Você abriu o Stripe primeiro, porque é lá que o dinheiro mora. O pagamento estava lá. Aprovado. O e-mail batia com o cadastro. Você respirou.

Aí abriu o painel admin. A linha ainda dizia `free`. Sem id de assinatura. Sem flag `active`. O usuário tinha razão, o produto estava errado, e os dois olhavam para verdades diferentes.

Esse vão não é bug do Stripe. É o espaço entre "pagamento capturado" e "seu app ficou sabendo."

## O que quebrou de verdade

Em algum ponto entre o checkout e o banco, o webhook não chegou, ou chegou duas vezes e seu handler travou, ou chegou uma vez com o tipo de evento errado e você só logou e seguiu em frente.

Causas comuns, na ordem em que aparecem para quem lança sozinho:

1. **Túnel local morreu.** Você testou webhook com ngrok na terça. Produção aponta para uma URL que ninguém renovou.
2. **Assinatura falhou em produção.** O segredo na variável de ambiente é de teste. O Stripe assina com o segredo live. Seu handler devolve 400, o Stripe reenvia, você não vê.
3. **Sem idempotência.** O mesmo `invoice.paid` chega três vezes numa tempestade de retry. Seu código tenta inserir três linhas, a segunda estoura, você devolve 500, o Stripe insiste, e o usuário continua no grátis.
4. **Você atualizou o usuário em `checkout.session.completed`, mas o acesso checa `subscription.updated`.** Um evento disparou. O outro não. O painel parece ok até alguém perguntar.

O usuário não liga para qual foi. Ele liga para ter pago e a feature continuar cinza.

## O conserto manual que você não deveria repetir

Provavelmente você fez o que todo fundador faz uma vez: abriu o banco, achou o usuário, copiou o id da assinatura no Stripe, virou um boolean na mão, mandou uma resposta meio sem graça.

Funcionou. Também não ensinou nada durável, porque semana que vem outro webhook vai falhar por outro motivo e você volta às 22h47.

O conserto durável é chato de propósito:

- Uma tabela que guarda eventos de webhook com id único do provedor, para duplicata não fazer mal.
- Um método de serviço que faz upsert do estado da assinatura a partir do payload, não do que você lembra que o Stripe mostrava cinco minutos atrás.
- Um lugar no app que pergunta "esse usuário tem o direito X?" e nunca consulta o Stripe direto durante a requisição.
- Logs que dizem qual id de evento falhou e por quê, não só "erro de webhook."

Isso é uma semana se você começa do zero. É uma tarde se alguém já cometeu os erros por você.

## Por que template pesa mais aqui do que na tela de login

Auth parece difícil até você construir uma vez. Cobrança parece fácil até o primeiro e-mail bravo.

Login é um caminho que todo mundo percorre na mesma ordem. Cobrança é evento fora de ordem, falha parcial, reembolso, renovação que não passou, troca de plano no meio do ciclo, e cliente que fez upgrade no celular enquanto seu cache ainda diz `starter`.

Template SaaS que para em "aqui vai um botão de checkout" vendeu brochura. Um que entrega webhooks assinados, handlers idempotentes, linha de assinatura amarrada a feature flags, e testes que reproduzem um fixture do Stripe duas vezes sem duplicar acesso, vendeu sono.

O CastorStack roteia Stripe e Paddle atrás de uma interface, guarda a assinatura no seu banco e libera feature a partir dessa linha. Webhook não é link de tutorial no README. É código que já rodou no CI antes de você clonar o repositório.

## O que checar antes de culpar a si mesmo

Se você está depurando isso ao vivo, percorra a cadeia nesta ordem:

1. Painel Stripe → Developers → Webhooks → seu endpoint. Entregas recentes estão vermelhas?
2. Logs da API naquele horário. 400 é assinatura ou payload. 500 é exceção no handler.
3. Sua tabela `webhook_events` ou equivalente. O id do evento está lá?
4. A linha de assinatura do usuário. O `status` bate com o que o Stripe mostra para aquele id?

Se o passo 3 está vazio e o 1 está vermelho, o problema é entrada. Se o 3 existe e o 4 está errado, o problema é o mapeamento. Se os dois parecem certos e o usuário continua bloqueado, o problema é cache ou você está checando a chave de direito errada.

Anote esse checklist agora, enquanto está fresco. De madrugada você esquece a ordem.

## A parte que é sua de qualquer jeito

Nenhum template sabe sua história de preço nem qual feature pertence a qual plano. Alguém ainda precisa nomear os direitos e decidir o que "premium" significa no produto.

Mas o cano de "dinheiro moveu" para "linha atualizou" para "botão liberou" não é onde mora sua vantagem competitiva. É onde mora confiança. Quebra uma vez e o cliente se pergunta o que mais está fora de sincronia.

---

O CastorStack já traz webhooks do Stripe e Paddle, sincronização idempotente de assinatura e liberação de features amarrada à linha do banco, não a um pedido no checkout. Pule a edição manual de madrugada e comece no produto que só você consegue construir. [Conheça o CastorStack](/pt/pricing).
