---
title: "Alertas de falha de pagamento no Stripe: disputas, recusas e webhooks mortos"
description: "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."
date: 2026-09-11
author: Nooc
authorAvatarLink: /images/avatars/nooc.webp
authorLink: https://nooc.me
tags:
  - Stripe
  - pagamentos
  - webhooks
  - disputas
  - SaaS
  - alertas
---

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

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.

| Evento | Por que importa | Tipo sugerido |
| --- | --- | --- |
| `charge.dispute.created` | Você tem uma janela limitada para responder — [normalmente de 7 a 21 dias, dependendo da bandeira](https://docs.stripe.com/disputes/responding). Se perder, perde automaticamente. | Ligação |
| `radar.early_fraud_warning.created` | O 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.failed` | O 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_failed` | Churn 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.deleted` | Churn voluntário. Vale saber hoje, não vale acordar. | Normal |
| `payment_intent.succeeded` | Nada 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](/pt/docs/notification) 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](https://dashboard.stripe.com/webhooks)
- Echobell instalado ([App Store](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-stripe-payment-failure-alerts-pt&mt=8) / [Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid))
- 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>`.

<Callout type="info">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.</Callout>

## Passo 2 — Aponte o Stripe para o canal

No dashboard do Stripe, abra a [aba Webhooks](https://dashboard.stripe.com/webhooks) e crie um destino de eventos:

<Steps>

<Step>

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

</Step>

<Step>

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](https://docs.stripe.com/webhooks); aqui isso também mantém o template honesto, porque todo payload que chega tem o mesmo formato.

</Step>

<Step>

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

</Step>

<Step>

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

</Step>

</Steps>

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](https://docs.stripe.com/api/events/object) 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:

```json
{
  "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](/pt/docs/conditions) 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](https://docs.stripe.com/webhooks) 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.

<Callout type="warn">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.</Callout>

## 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:

```js
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](https://docs.stripe.com/webhooks) 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](/pt/blog/cron-job-failure-alerts).

## 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](/pt/blog/fix-alert-fatigue-developer-guide).

## 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](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-stripe-payment-failure-alerts-pt&mt=8) ou [obtenha no Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid), crie primeiro o canal de disputas e dispare um `stripe trigger charge.dispute.created` antes de confiar algo real a esse caminho.

---

## Relacionados

- [Documentação de integração por webhook](/pt/docs/webhook)
- [Referência de condições de canal](/pt/docs/conditions)
- [Guia do desenvolvedor para vencer a fadiga de alertas](/pt/blog/fix-alert-fatigue-developer-guide)
- [Alertas de falha em cron jobs](/pt/blog/cron-job-failure-alerts)
- [Notificações de webhook do Zapier no celular](/pt/blog/zapier-webhook-notifications-to-phone)
