---
title: "Оповещения о сбоях платежей в Stripe: диспуты, отказы и мёртвые вебхуки"
description: "Stripe сообщает о диспутах и неудачных платежах по email. Как превратить события вебхуков Stripe в пуш, срочное уведомление или звонок."
date: 2026-09-11
author: Nooc
authorAvatarLink: /images/avatars/nooc.webp
authorLink: https://nooc.me
tags:
  - Stripe
  - платежи
  - вебхуки
  - диспуты
  - SaaS
  - оповещения
---

# Оповещения о сбоях платежей в Stripe: диспуты, отказы и мёртвые вебхуки

Stripe уже знает, когда открыт диспут, когда не прошло списание по подписке и когда выплата вернулась. И сообщает об этом письмом. Чтобы получилось громче, зарегистрируйте в Stripe второй эндпойнт вебхука, указывающий на URL канала Echobell, подпишите его на несколько типов событий и выберите тип уведомления по сроку, который привязан к каждому из них. У диспутов и ранних предупреждений о мошенничестве часы уже тикают; у неудачного продления — обычно нет.

В этом руководстве: какие события Stripe заслуживают того, чтобы вас прервать, как связать Stripe с Echobell примерно за пять минут, какие поля шаблонов реально существуют у каждого объекта и на какой компромисс вы идёте, пропуская проверку подписи.

## События, которые действительно заслуживают прерывания

Оповещения о платежах ломаются всегда одинаково: кто-то подписывается на `payment_intent.succeeded`, потому что это приятно, телефон вибрирует сорок раз в день, а через шесть недель уведомление о диспуте проскакивает непрочитанным. Начните со срока. Если пропустить событие на восемь часов ничего не стоит, ему не нужно доходить до вас за восемь секунд.

| Событие | Почему это важно | Рекомендуемый тип |
| --- | --- | --- |
| `charge.dispute.created` | На ответ у вас ограниченное окно — [обычно от 7 до 21 дня в зависимости от платёжной системы](https://docs.stripe.com/disputes/responding). Пропустили — проиграли автоматически. | Звонок |
| `radar.early_fraud_warning.created` | Банк-эмитент сообщил Stripe, что списание может быть мошенническим. Вернуть деньги до того, как это станет формальным диспутом, — то действие, которое ещё доступно, и окно короткое. | Звонок |
| `payout.failed` | Деньги, собранные Stripe, не доходят до вашего банка. Всё, что ниже по цепочке — зарплаты, расчёт запаса прочности, — теперь неверно. | Звонок |
| `invoice.payment_failed` | Невольный отток. Достаточно часто решается само, так что звонок избыточен, но самые крупные аккаунты стоит посмотреть в тот же день. | Срочное |
| `customer.subscription.deleted` | Добровольный отток. Узнать стоит сегодня, будить не стоит. | Обычное |
| `payment_intent.succeeded` | Ничего не сломалось. Именно это событие приучает вас игнорировать остальные пять. | Ничего |

Уровни соответствуют трём [типам уведомлений](/ru/docs/notification) Echobell: **Обычное** — это обычный пуш, **Срочное** пробивается через большинство режимов фокусирования, а **Звонок** выглядит как входящий вызов и звонит даже в режиме «Не беспокоить». Каждый подписчик выбирает свой уровень для каждого канала, так что сооснователь может получать диспуты звонком, а коллега из поддержки — пушем.

## Что понадобится

- Аккаунт Stripe с доступом к [вкладке Webhooks в Workbench](https://dashboard.stripe.com/webhooks)
- Установленный Echobell ([App Store](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-stripe-payment-failure-alerts-ru&mt=8) / [Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid))
- Пять минут. Ни сервера, ни деплоя, ни кода — если только вам не нужна проверка подписи, о ней в последнем разделе.

## Шаг 1 — По одному каналу на тип события

Хочется сделать один канал «Stripe» и слать туда всё. Не надо. Шаблон тела и ссылка в дашборд различаются для диспута, счёта и выплаты, потому что каждый несёт свой объект, — а весь смысл этой схемы в том, чтобы уведомление само говорило, что произошло, без открытия чего бы то ни было.

Создайте канал, названный по событию: `Stripe Disputes`. Напишите шаблоны заголовка и тела так, чтобы они читались с экрана блокировки:

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

В расширенных настройках задайте шаблон ссылки, чтобы запись уведомления открывала нужную страницу:

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

Затем подпишитесь сами с типом **Звонок** и скопируйте URL вебхука из карточки канала. Он выглядит так: `https://hook.echobell.one/t/<channel-token>`.

<Callout type="info">Включите **POST Only** в расширенных настройках канала. Stripe всегда отправляет POST, а этот переключатель означает, что вставка URL в окно чата или адресную строку браузера больше не сможет поднять ложную тревогу о диспуте.</Callout>

## Шаг 2 — Направьте Stripe на канал

В дашборде Stripe откройте [вкладку Webhooks](https://dashboard.stripe.com/webhooks) и создайте назначение событий:

<Steps>

<Step>

Нажмите **Create an event destination**, выберите **Your account** и оставьте версию API по умолчанию для вашего аккаунта.

</Step>

<Step>

Выберите ровно один тип события — для этого канала `charge.dispute.created`. Сам Stripe советует [подписываться только на те события, которые нужны вашей интеграции](https://docs.stripe.com/webhooks); здесь это ещё и держит шаблон честным, потому что каждый приходящий payload одной и той же формы.

</Step>

<Step>

Выберите тип назначения **Webhook endpoint** и вставьте URL канала Echobell.

</Step>

<Step>

Сохраните, затем нажмите **Send test event** — или выполните `stripe trigger charge.dispute.created` из CLI — и убедитесь, что телефон звонит.

</Step>

</Steps>

Повторите для каждого созданного канала. Stripe разрешает до 16 эндпойнтов вебхуков на аккаунт — с запасом хватит по одному на уровень оповещения.

## Шаг 3 — Что приходит на самом деле

Stripe отправляет [объект Event](https://docs.stripe.com/api/events/object) в JSON. Echobell читает тело как есть, поэтому любое поле доступно в шаблонах и условиях через точечную нотацию:

```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 }
    }
  }
}
```

Три вещи в этом payload удивляют чаще всего:

**Суммы — целые числа в минимальной единице валюты.** `amount` равный `4900` — это $49.00. Шаблоны Echobell подставляют и сравнивают значения, но не выполняют арифметику, поэтому `{{data.object.amount}}` выведет `4900`. Либо честно подпишите единицу (`Amount: 4900 (cents)`), либо используйте прокси из последнего раздела, чтобы поделить на 100 перед отправкой.

**Метки времени — это Unix-секунды.** `{{data.object.evidence_details.due_by}}` выведет `1759449600`, а не дату. Если важнее сам факт срока, чем точный час, уберите его из шаблона — страница диспута его показывает — и пусть работает шаблон ссылки.

**Имена полей различаются по объектам.** У диспута есть `amount`; у счёта — `amount_due`, `customer_email`, `attempt_count` и `hosted_invoice_url`; у выплаты — `failure_message` и `arrival_date`; у раннего предупреждения о мошенничестве — `fraud_type`, `actionable` и `charge` в виде обычной строки-идентификатора. Отсутствующая переменная выводится пустой строкой, а не ошибкой, поэтому шаблон, скопированный не из того канала, ломается молча. Это и есть практическая причина держать по каналу на тип события.

## Шаг 4 — Фильтруйте условиями, а не силой воли

[Условия](/ru/docs/conditions) канала используют тот же синтаксис выражений, что и шаблоны, но без фигурных скобок, и выполняются до любой доставки.

То, что стоит поставить на каждый канал Stripe:

```
livemode == true
```

Трафик тестового режима — ваши же запуски `stripe trigger`, коллега, ковыряющийся в песочнице, — больше не доходит до телефона. Добавляйте это после того, как убедитесь, что связка работает, а не до.

Для канала неудачных счетов порог убирает мелкие аккаунты из вашего вечера:

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

Читается как «больше $200», в центах. А если вы хотите видеть действительно застрявшие повторы, а не каждую первую попытку:

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

Если вы всё же направили один эндпойнт с несколькими типами событий в один канал, условия разведут их обратно:

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

## Шаг 5 — Держите путь оповещения вне того, что ломается

Вот часть, которая стоит дороже шаблонов.

Ваш продакшн-эндпойнт вебхука — это место, где происходит выдача: он открывает доступ, пишет в базу, отправляет чек. А значит, это и тот эндпойнт, который падает вместе с вашим приложением. Когда это случается, Stripe [повторяет попытки до трёх дней с экспоненциальной задержкой](https://docs.stripe.com/webhooks) и присылает письмо — а письмо о недоставленных вебхуках выглядит точно так же, как любое другое письмо от Stripe, поэтому его находят в понедельник.

Причины тихой смерти вебхука скучны. Stripe считает редирект `3xx` неудачей, поэтому эндпойнт, который вдруг начал перенаправлять `http` на `https` или дописывать слеш, перестаёт получать события. Требуется TLS 1.2 или выше — достаточно просроченного или неверно настроенного сертификата. `403` от правила WAF, которое кто-то добавил на прошлой неделе, тоже подойдёт.

Второй эндпойнт, направленный прямо на Echobell, не разделяет ничего из этого. Это другой URL на другом хосте с другим сертификатом, и он продолжает звонить, пока ваше приложение лежит. Правило обобщается: *путь, который сообщает, что что-то сломалось, не должен проходить через то, что сломалось.*

Отказ собственного эндпойнта всё равно должен быть заметен. Загляните во вкладку **Event deliveries** в Workbench, когда что-то кажется неправильным, — она показывает `Delivered`, `Pending` и `Failed` по каждому событию и HTTP-статус каждой попытки. Stripe позволяет переотправить событие в течение 15 дней из дашборда или 30 дней через `stripe events resend` в CLI, так что дыру, замеченную за две недели, ещё можно закрыть.

<Callout type="warn">URL канала Echobell — это bearer-учётные данные: кто им владеет, тот может запустить канал. Направив Stripe прямо туда, вы соглашаетесь, что заголовок `Stripe-Signature` никто не проверяет, поэтому утёкший URL — это машина ложных тревог, а не утечка данных. Держите его подальше от репозиториев и скриншотов, используйте **Reset Token**, если он всё-таки утёк, и прочитайте следующий раздел, если этот компромисс вас смущает.</Callout>

## Опционально — Сначала проверить подпись

Если вы хотите, чтобы подпись Stripe действительно проверялась, а суммы выглядели как деньги, поставьте перед каналом небольшой прокси. Этот Cloudflare Worker проверяет событие, сразу отвечает `200`, как просит Stripe, и отправляет в Echobell плоский payload:

```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 });
  },
};
```

Шаблон на другой стороне становится куда приятнее, потому что вся подготовка произошла в коде:

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

`externalLink` — специальная переменная: если шаблон ссылки не задан, Echobell использует её как ссылку записи уведомления, и размещённая страница счёта оказывается в одно касание.

Обратите внимание на форму компромисса: теперь это ещё один элемент инфраструктуры, который сам может упасть, — ровно то, о чём предупреждает шаг 5. Разумная середина: проверять подписи на канале с большим потоком, где ложные тревоги раздражали бы, и оставить канал диспутов подключённым напрямую — там цена ложного звонка — недоумённый взгляд, а цена пропущенного — сумма диспута.

## Чего эта схема не даёт

- **Ни дежурств, ни эскалации.** Все подписчики канала со звонком звонят одновременно. При четверых это плюс, при сорока — проблема; при сорока нужна платформа управления инцидентами.
- **Никакой дедупликации.** Stripe [не гарантирует порядок событий](https://docs.stripe.com/webhooks) и может доставить одно и то же событие несколько раз. Два звонка на один диспут вполне возможны.
- **Никакого подтверждения.** Ничто не фиксирует, что человек это увидел, и ничто не передаёт второму, если никто не отреагировал.
- **Нет оповещения «платежи прекратились».** Stripe присылает события, когда что-то происходит, и никогда — когда перестаёт. Если сломается оформление заказа, не сработает ни одно событие. Для этого нужна задача по расписанию на вашей стороне, которая дёргает канал, когда за последний час ноль списаний, — [«выключатель мертвеца» на cron](/ru/blog/cron-job-failure-alerts).

## Диагностика

**В Stripe тестовое событие показывает `200`, но уведомление не пришло.** Echobell отвечает `200` с телом JSON даже тогда, когда ничего не доставляет, — посмотрите тело ответа во вкладке Event deliveries. `success: false` при токене правильной длины означает неверный токен канала. Если `success` равен `true`, вероятная причина — условие: `livemode == true` по замыслу блокирует все тестовые события.

**Stripe сообщает `405 Method Not Allowed`.** У канала включён POST Only, а что-то отправило GET. Сам Stripe всегда шлёт POST, так что это превью ссылки или вкладка браузера, а не Stripe.

**Уведомление приходит с пустыми полями.** Шаблон обращается не к тому объекту — `{{data.object.amount}}` в канале счетов, где поле называется `amount_due`. Отправьте одно настоящее событие, откройте его в дашборде и прочитайте JSON.

**Доставки начали падать после недель работы.** Проверьте сертификат и любой редирект перед URL. Для канала, используемого напрямую, это редкость; для развёрнутого вами прокси — обычный подозреваемый.

## Частые вопросы

### Может ли Stripe позвонить мне при открытии диспута?

Сам по себе — нет. Stripe уведомляет письмом, в дашборде, событием `charge.dispute.created` и пушем, если вы пользуетесь приложением Stripe Dashboard. Чтобы телефон реально зазвонил, направьте это событие в канал с типом подписки **Звонок**.

### Нужно ли писать код, чтобы связать Stripe с Echobell?

Нет. Stripe отправляет JSON на любой публичный HTTPS-URL, а URL канала Echobell — как раз такой. Код нужен только если вы хотите проверять заголовок `Stripe-Signature` или переформатировать суммы.

### Безопасно ли давать Stripe сторонний URL вебхука?

Это осознанный компромисс. Payload, который отправляет Stripe, содержит метаданные клиента и платежа, а Echobell не хранит сырые payload постоянно — отрисованное уведомление живёт на вашем устройстве. Вы отказываетесь от проверки подписи: тот, кто узнает URL, может прислать вам убедительную подделку. Относитесь к нему как к API-ключу и используйте схему с прокси для всего, что хотите проверять.

### Почему в оповещении 4900 вместо $49.00?

Stripe отправляет суммы целыми числами в минимальной единице валюты, а шаблоны Echobell не считают. Укажите единицу в шаблоне или поделите на 100 в прокси перед отправкой.

### Как сделать, чтобы события тестового режима меня не будили?

Добавьте в канал условие `livemode == true`. Stripe помечает каждое событие песочницы и `stripe trigger` как `livemode: false`.

### Может ли сооснователь получать те же оповещения без оплаты места?

Да. Поделитесь ссылкой на канал; каждый подписчик выбирает свой тип уведомления. Один получает диспуты звонком, другой — обычным пушем, и никакой платы за подписчика нет.

### Стоит ли оповещать об успешных платежах?

Только недолго и только пока бизнес достаточно мал, чтобы каждый платёж ещё был событием. Как только уведомление об успешной оплате становится рутиной, оно начинает подтачивать вашу реакцию на действительно важные — базовый механизм [усталости от оповещений](/ru/blog/fix-alert-fatigue-developer-guide).

## Итог

Вся схема — это одно назначение событий Stripe на каждый канал Echobell, условие `livemode == true` и дисциплина: уровень звонка остаётся за событиями с тикающими часами. У диспутов и ранних предупреждений о мошенничестве они есть. У неудачного продления тарифа за $9 — нет, и попытка сделать вид, что есть, — это ровно тот путь, на котором проспишь то самое, с часами.

[Скачайте Echobell для iPhone](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-stripe-payment-failure-alerts-ru&mt=8) или [возьмите в Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid), создайте сначала канал диспутов и запустите `stripe trigger charge.dispute.created`, прежде чем доверить этому пути что-то настоящее.

---

## Похожее

- [Документация по интеграции вебхуков](/ru/docs/webhook)
- [Справочник по условиям канала](/ru/docs/conditions)
- [Руководство разработчика по борьбе с усталостью от оповещений](/ru/blog/fix-alert-fatigue-developer-guide)
- [Оповещения о сбоях cron-задач](/ru/blog/cron-job-failure-alerts)
- [Уведомления вебхуков Zapier на телефон](/ru/blog/zapier-webhook-notifications-to-phone)
