Alertas de falha de pagamento no Stripe: disputas, recusas e webhooks mortos

O Stripe avisa por e-mail sobre disputas e pagamentos que falham. Como transformar eventos de webhook do Stripe em push, alerta urgente ou ligação.

Atualizado

Sumário

O Stripe já sabe quando uma disputa é aberta, quando a cobrança de uma assinatura falha ou quando um repasse volta. Ele te conta por e-mail. Para conseguir algo mais alto, registre no Stripe um segundo endpoint de webhook apontando para a URL de um canal do Echobell, assine um punhado de tipos de evento e escolha o tipo de notificação de acordo com o prazo que vem colado em cada um. Disputas e avisos antecipados de fraude têm um relógio correndo; uma renovação que falha, normalmente não.

Este guia cobre quais eventos do Stripe merecem te interromper, como apontar o Stripe para o Echobell em cerca de cinco minutos, os campos de template que realmente existem em cada objeto e o trade-off que você aceita ao pular a verificação de assinatura.

Os eventos que realmente merecem uma interrupção

O alerta de pagamentos erra sempre do mesmo jeito: alguém assina payment_intent.succeeded porque é gostoso de ver, o celular vibra quarenta vezes por dia e, seis semanas depois, uma notificação de disputa passa batida sem ser lida. Comece pelo prazo. Se perder o evento por oito horas não custa nada, ele não precisa te alcançar em oito segundos.

EventoPor que importaTipo sugerido
charge.dispute.createdVocê tem uma janela limitada para responder — normalmente de 7 a 21 dias, dependendo da bandeira. Se perder, perde automaticamente.Ligação
radar.early_fraud_warning.createdO emissor do cartão avisou o Stripe de que uma cobrança pode ser fraudulenta. Reembolsar antes que vire uma disputa formal é a ação que ainda resta, e a janela é curta.Ligação
payout.failedO dinheiro que o Stripe recolheu não chega ao seu banco. Tudo o que vem depois — folha de pagamento, cálculo de caixa — está errado agora.Ligação
invoice.payment_failedChurn involuntário. Se resolve sozinho com frequência suficiente para que uma ligação seja exagero, mas as maiores contas merecem um olhar no mesmo dia.Urgente
customer.subscription.deletedChurn voluntário. Vale saber hoje, não vale acordar.Normal
payment_intent.succeededNada está quebrado. É justamente este evento que te treina a ignorar os outros cinco.Nada

Os níveis correspondem aos três tipos de notificação do Echobell: Normal é um push comum, Urgente atravessa a maioria dos modos de foco e Ligação aparece como uma chamada recebida, tocando mesmo com o Não Perturbe. Cada assinante escolhe o próprio nível por canal, então a cofundadora pode receber disputas como ligação enquanto o colega do suporte recebe as mesmas como push.

O que você precisa

  • Uma conta Stripe com acesso à aba Webhooks no Workbench
  • Echobell instalado (App Store / Google Play)
  • Cinco minutos. Sem servidor, sem deploy, sem código — a menos que você queira verificação de assinatura, que está na última seção.

Passo 1 — Um canal por tipo de evento

É tentador criar um único canal "Stripe" e mandar tudo para lá. Não faça isso. O template do corpo e o link do dashboard são diferentes para uma disputa, uma fatura e um repasse, porque cada um carrega um objeto diferente — e o sentido inteiro deste arranjo é que a notificação diga o que aconteceu sem você abrir nada.

Crie um canal com o nome do evento: Stripe Disputes. Escreva os templates de título e corpo para serem lidos de uma vez na tela de bloqueio:

Title: 🔴 Dispute opened — {{data.object.reason}}
Body: Amount: {{data.object.amount}} {{data.object.currency}}
Charge: {{data.object.charge}}
Status: {{data.object.status}}

Defina o template de link nas configurações avançadas para que o registro da notificação abra a página certa:

https://dashboard.stripe.com/disputes/{{data.object.id}}

Depois inscreva-se com o tipo Ligação e copie a URL do webhook na tela de detalhes do canal. Ela tem esta cara: https://hook.echobell.one/t/<channel-token>.

Ative POST Only nas configurações avançadas do canal. O Stripe sempre envia POST, e essa chave faz com que colar a URL em uma janela de chat ou em uma aba do navegador não dispare mais um alerta falso de disputa.

Passo 2 — Aponte o Stripe para o canal

No dashboard do Stripe, abra a aba Webhooks e crie um destino de eventos:

Clique em Create an event destination, selecione Your account e deixe a versão da API no padrão da sua conta.

Selecione exatamente um tipo de evento — charge.dispute.created para este canal. O próprio conselho do Stripe é assinar apenas os eventos de que sua integração precisa; aqui isso também mantém o template honesto, porque todo payload que chega tem o mesmo formato.

Escolha Webhook endpoint como tipo de destino e cole a URL do canal do Echobell.

Salve e use Send test event — ou stripe trigger charge.dispute.created pela CLI — para confirmar que o telefone toca.

Repita para cada canal criado. O Stripe permite até 16 endpoints de webhook por conta, mais que suficiente para um por nível de alerta.

Passo 3 — O que realmente chega

O Stripe envia o objeto Event como JSON. O Echobell lê o corpo como está, então todo campo fica acessível em templates e condições com notação de ponto:

{
  "id": "evt_1P...",
  "type": "charge.dispute.created",
  "livemode": true,
  "created": 1757548800,
  "data": {
    "object": {
      "id": "dp_1P...",
      "amount": 4900,
      "currency": "usd",
      "reason": "fraudulent",
      "status": "needs_response",
      "charge": "ch_3P...",
      "evidence_details": { "due_by": 1759449600 }
    }
  }
}

Três coisas nesse payload costumam surpreender:

Valores são inteiros na menor unidade da moeda. Um amount de 4900 são US$ 49,00. Os templates do Echobell interpolam e comparam valores, mas não fazem aritmética, então {{data.object.amount}} renderiza 4900. Ou você rotula com honestidade (Amount: 4900 (cents)), ou usa o encaminhador da última seção para dividir por 100 antes de enviar.

Timestamps são segundos Unix. {{data.object.evidence_details.due_by}} aparece como 1759449600, não como data. Se o que importa é existir um prazo, e não a hora exata, tire do template — a página da disputa mostra — e deixe o template de link trabalhar.

Os nomes dos campos mudam por objeto. Uma disputa tem amount; uma fatura tem amount_due, customer_email, attempt_count e hosted_invoice_url; um repasse tem failure_message e arrival_date; um aviso antecipado de fraude tem fraud_type, actionable e charge como ID em texto puro. Uma variável ausente renderiza como string vazia em vez de erro, então um template copiado do canal errado falha em silêncio. É esse o motivo prático de ter um canal por tipo de evento.

Passo 4 — Filtre com condições, não com força de vontade

As condições de canal usam a mesma sintaxe de expressões dos templates, sem as chaves, e rodam antes de qualquer entrega.

A que deve estar em todo canal do Stripe:

livemode == true

Tráfego de modo de teste — seus próprios stripe trigger, um colega mexendo num sandbox — não chega mais ao seu celular. Adicione depois de confirmar que a ligação funciona, não antes.

No canal de faturas que falham, um limite mantém as contas pequenas fora da sua noite:

livemode == true && data.object.amount_due > 20000

Lê-se como "acima de US$ 200", em centavos. E se você prefere ver as retentativas realmente travadas em vez de cada primeira falha:

livemode == true && data.object.attempt_count > 1

Se você acabou apontando um endpoint com vários tipos de evento para um canal só, as condições os separam de novo:

type == "charge.dispute.created" || type == "payout.failed"

Passo 5 — Mantenha o caminho do alerta fora daquilo que quebra

Esta parte vale mais que os templates.

Seu endpoint de webhook de produção é onde acontece o fulfillment: ele libera acesso, escreve no banco, manda o recibo. É, portanto, o endpoint que cai quando a sua aplicação cai. Quando isso acontece, o Stripe tenta de novo por até três dias com backoff exponencial e te manda um e-mail — e um e-mail sobre webhooks não entregues é idêntico a qualquer outro e-mail do Stripe, o que explica por que ele é encontrado na segunda-feira.

A morte silenciosa de um webhook tem causas banais. O Stripe trata um redirecionamento 3xx como falha, então um endpoint que passa a redirecionar http para https ou a acrescentar barra final deixa de receber eventos. Ele exige TLS 1.2 ou superior, então um certificado vencido ou mal configurado basta. Um 403 de uma regra de WAF que alguém adicionou semana passada também.

Um segundo endpoint apontado direto para o Echobell não compartilha nada disso. É outra URL, em outro host, com outro certificado, e continua tocando enquanto sua aplicação está no chão. A regra se generaliza: o caminho que te avisa que algo quebrou não deveria passar por aquilo que quebrou.

Você ainda quer que a falha do seu próprio endpoint seja visível. Confira a aba Event deliveries no Workbench quando algo parecer estranho — ela mostra Delivered, Pending e Failed por evento, com o status HTTP de cada tentativa. O Stripe permite reenviar um evento por até 15 dias pelo dashboard, ou 30 dias com stripe events resend na CLI, então um buraco percebido em duas semanas dá para tapar.

A URL de canal do Echobell é uma credencial de portador: quem a tiver consegue disparar o canal. Apontar o Stripe direto para ela significa que ninguém verifica o cabeçalho Stripe-Signature, então uma URL vazada é uma máquina de alertas falsos, não um vazamento de dados. Mantenha fora de repositórios e prints, use Reset Token se ela escapar, e leia a próxima seção se esse trade-off te incomodar.

Opcional — Verifique a assinatura antes

Se você quer que a assinatura do Stripe seja de fato verificada e os valores formatados como dinheiro, coloque um pequeno encaminhador na frente. Este Cloudflare Worker verifica o evento, devolve 200 imediatamente como o Stripe pede e envia ao Echobell um payload plano:

import Stripe from "stripe";

export default {
  async fetch(request, env, ctx) {
    const stripe = new Stripe(env.STRIPE_SECRET_KEY);
    const body = await request.text();

    let event;
    try {
      event = await stripe.webhooks.constructEventAsync(
        body,
        request.headers.get("stripe-signature"),
        env.STRIPE_WEBHOOK_SECRET,
      );
    } catch {
      return new Response("invalid signature", { status: 400 });
    }

    const invoice = event.data.object;
    ctx.waitUntil(
      fetch(env.ECHOBELL_HOOK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          customer: invoice.customer_email || invoice.customer,
          amount: (invoice.amount_due / 100).toFixed(2),
          currency: invoice.currency.toUpperCase(),
          attempt: invoice.attempt_count,
          externalLink: invoice.hosted_invoice_url,
        }),
      }),
    );

    return new Response("ok", { status: 200 });
  },
};

O template do outro lado fica bem mais bonito, porque a formatação aconteceu no código:

Title: 💳 Payment failed — {{currency}} {{amount}}
Body: Customer: {{customer}}
Attempt #{{attempt}}

externalLink é uma variável especial: sem template de link configurado, o Echobell a usa como link do registro da notificação, deixando a página da fatura hospedada a um toque.

Repare no formato do trade-off: isso agora é uma peça de infraestrutura que também pode falhar, exatamente o que o passo 5 alerta. Um meio-termo razoável é verificar assinaturas no canal de alto volume, onde alertas falsos seriam irritantes, e deixar o canal de disputas ligado direto — ali o custo de um toque espúrio é um olhar confuso e o de um toque perdido é o valor disputado.

O que este arranjo não oferece

  • Nada de escala de plantão ou escalonamento. Todos os inscritos num canal de ligação tocam ao mesmo tempo. Isso é qualidade com quatro pessoas e problema com quarenta; com quarenta, você quer uma plataforma de incidentes.
  • Sem deduplicação. O Stripe não garante a ordem dos eventos e pode entregar o mesmo evento mais de uma vez. Dois toques para uma disputa é possível.
  • Sem confirmação. Nada registra que um humano viu, e nada escala para uma segunda pessoa se ninguém responder.
  • Sem alerta de "os pagamentos pararam". O Stripe emite eventos quando as coisas acontecem, nunca quando param. Se o seu checkout quebrar, nenhum evento é disparado. Esse caso exige um job agendado do seu lado que chame um canal quando a contagem de cobranças da última hora for zero — um dead man's switch baseado em cron.

Solução de problemas

O evento de teste mostra 200 no Stripe, mas nenhuma notificação chegou. O Echobell responde 200 com corpo JSON mesmo sem entregar — veja o corpo da resposta na aba Event deliveries. success: false com token de comprimento válido significa token de canal errado. Se success for true, a causa provável é uma condição: livemode == true bloqueia todo evento de teste, por design.

O Stripe reporta 405 Method Not Allowed. O canal está com POST Only ligado e algo enviou um GET. O Stripe sempre usa POST, então isso foi uma prévia de link ou uma aba do navegador, não o Stripe.

A notificação chega com campos em branco. O template está apontando para o objeto errado — {{data.object.amount}} num canal de faturas, onde o campo é amount_due. Envie um evento real, abra no dashboard e leia o JSON.

As entregas passam a falhar depois de semanas funcionando. Verifique o certificado e qualquer redirecionamento na frente da URL. Com a URL de canal usada diretamente isso é raro; com um encaminhador que você mesmo publicou, é o suspeito de sempre.

Perguntas frequentes

O Stripe pode me ligar quando uma disputa é aberta?

Sozinho, não. O Stripe avisa por e-mail, no dashboard, pelo evento charge.dispute.created e por push se você usa o app Stripe Dashboard. Para um toque de verdade, roteie esse evento para um canal cujo tipo de inscrição seja Ligação.

Preciso escrever código para conectar o Stripe ao Echobell?

Não. O Stripe envia JSON para qualquer URL HTTPS pública, e a URL de canal do Echobell é uma delas. Código só é necessário se você quiser verificar o cabeçalho Stripe-Signature ou reformatar os valores.

É seguro dar ao Stripe uma URL de webhook de terceiros?

É um trade-off consciente. O payload que o Stripe envia contém metadados de cliente e pagamento, e o Echobell não armazena permanentemente payloads brutos — a notificação renderizada fica no seu dispositivo. O que você abre mão é da verificação de assinatura: quem descobrir a URL pode te mandar uma falsificação convincente. Trate como chave de API e use o padrão do encaminhador para o que preferir verificar.

Por que meu alerta mostra 4900 em vez de US$ 49,00?

O Stripe envia valores como inteiros na menor unidade da moeda, e os templates do Echobell não fazem aritmética. Indique a unidade no template ou divida por 100 num encaminhador antes de enviar.

Como evito que eventos de modo de teste me acordem?

Adicione a condição livemode == true ao canal. O Stripe marca todo evento de sandbox e de stripe trigger como livemode: false.

Meu cofundador pode receber os mesmos alertas sem pagar um assento?

Pode. Compartilhe o link do canal; cada assinante escolhe o próprio tipo de notificação. Uma pessoa recebe disputas como ligação enquanto outra recebe como push normal, e não há cobrança por assento para assinantes.

Devo alertar sobre pagamentos bem-sucedidos?

Só por um tempo, e só enquanto o negócio for pequeno o bastante para cada um ainda ser um acontecimento. No momento em que a notificação de pagamento aprovado vira rotina, ela começa a corroer sua reação às que importam — o mecanismo central por trás da fadiga de alertas.

Para fechar

O arranjo inteiro é um destino de eventos do Stripe por canal do Echobell, uma condição livemode == true e a disciplina de reservar o nível de ligação para eventos com relógio correndo. Disputas e avisos antecipados de fraude têm um. Uma renovação que falha num plano de US$ 9 não tem — e fingir que tem é exatamente como se acaba dormindo durante a que tinha.

Baixe o Echobell para iPhone ou obtenha no Google Play, crie primeiro o canal de disputas e dispare um stripe trigger charge.dispute.created antes de confiar algo real a esse caminho.


Relacionados