Содержание
- События, которые действительно заслуживают прерывания
- Что понадобится
- Шаг 1 — По одному каналу на тип события
- Шаг 2 — Направьте Stripe на канал
- Шаг 3 — Что приходит на самом деле
- Шаг 4 — Фильтруйте условиями, а не силой воли
- Шаг 5 — Держите путь оповещения вне того, что ломается
- Опционально — Сначала проверить подпись
- Чего эта схема не даёт
- Диагностика
- Частые вопросы
- Может ли Stripe позвонить мне при открытии диспута?
- Нужно ли писать код, чтобы связать Stripe с Echobell?
- Безопасно ли давать Stripe сторонний URL вебхука?
- Почему в оповещении 4900 вместо $49.00?
- Как сделать, чтобы события тестового режима меня не будили?
- Может ли сооснователь получать те же оповещения без оплаты места?
- Стоит ли оповещать об успешных платежах?
- Итог
- Похожее
Stripe уже знает, когда открыт диспут, когда не прошло списание по подписке и когда выплата вернулась. И сообщает об этом письмом. Чтобы получилось громче, зарегистрируйте в Stripe второй эндпойнт вебхука, указывающий на URL канала Echobell, подпишите его на несколько типов событий и выберите тип уведомления по сроку, который привязан к каждому из них. У диспутов и ранних предупреждений о мошенничестве часы уже тикают; у неудачного продления — обычно нет.
В этом руководстве: какие события Stripe заслуживают того, чтобы вас прервать, как связать Stripe с Echobell примерно за пять минут, какие поля шаблонов реально существуют у каждого объекта и на какой компромисс вы идёте, пропуская проверку подписи.
События, которые действительно заслуживают прерывания
Оповещения о платежах ломаются всегда одинаково: кто-то подписывается на payment_intent.succeeded, потому что это приятно, телефон вибрирует сорок раз в день, а через шесть недель уведомление о диспуте проскакивает непрочитанным. Начните со срока. Если пропустить событие на восемь часов ничего не стоит, ему не нужно доходить до вас за восемь секунд.
| Событие | Почему это важно | Рекомендуемый тип |
|---|---|---|
charge.dispute.created | На ответ у вас ограниченное окно — обычно от 7 до 21 дня в зависимости от платёжной системы. Пропустили — проиграли автоматически. | Звонок |
radar.early_fraud_warning.created | Банк-эмитент сообщил Stripe, что списание может быть мошенническим. Вернуть деньги до того, как это станет формальным диспутом, — то действие, которое ещё доступно, и окно короткое. | Звонок |
payout.failed | Деньги, собранные Stripe, не доходят до вашего банка. Всё, что ниже по цепочке — зарплаты, расчёт запаса прочности, — теперь неверно. | Звонок |
invoice.payment_failed | Невольный отток. Достаточно часто решается само, так что звонок избыточен, но самые крупные аккаунты стоит посмотреть в тот же день. | Срочное |
customer.subscription.deleted | Добровольный отток. Узнать стоит сегодня, будить не стоит. | Обычное |
payment_intent.succeeded | Ничего не сломалось. Именно это событие приучает вас игнорировать остальные пять. | Ничего |
Уровни соответствуют трём типам уведомлений Echobell: Обычное — это обычный пуш, Срочное пробивается через большинство режимов фокусирования, а Звонок выглядит как входящий вызов и звонит даже в режиме «Не беспокоить». Каждый подписчик выбирает свой уровень для каждого канала, так что сооснователь может получать диспуты звонком, а коллега из поддержки — пушем.
Что понадобится
- Аккаунт Stripe с доступом к вкладке Webhooks в Workbench
- Установленный Echobell (App Store / Google Play)
- Пять минут. Ни сервера, ни деплоя, ни кода — если только вам не нужна проверка подписи, о ней в последнем разделе.
Шаг 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>.
Шаг 2 — Направьте Stripe на канал
В дашборде Stripe откройте вкладку Webhooks и создайте назначение событий:
Нажмите Create an event destination, выберите Your account и оставьте версию API по умолчанию для вашего аккаунта.
Выберите ровно один тип события — для этого канала charge.dispute.created. Сам Stripe советует подписываться только на те события, которые нужны вашей интеграции; здесь это ещё и держит шаблон честным, потому что каждый приходящий payload одной и той же формы.
Выберите тип назначения Webhook endpoint и вставьте URL канала Echobell.
Сохраните, затем нажмите Send test event — или выполните stripe trigger charge.dispute.created из CLI — и убедитесь, что телефон звонит.
Повторите для каждого созданного канала. Stripe разрешает до 16 эндпойнтов вебхуков на аккаунт — с запасом хватит по одному на уровень оповещения.
Шаг 3 — Что приходит на самом деле
Stripe отправляет объект Event в JSON. Echobell читает тело как есть, поэтому любое поле доступно в шаблонах и условиях через точечную нотацию:
{
"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 — Фильтруйте условиями, а не силой воли
Условия канала используют тот же синтаксис выражений, что и шаблоны, но без фигурных скобок, и выполняются до любой доставки.
То, что стоит поставить на каждый канал 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 повторяет попытки до трёх дней с экспоненциальной задержкой и присылает письмо — а письмо о недоставленных вебхуках выглядит точно так же, как любое другое письмо от 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, так что дыру, замеченную за две недели, ещё можно закрыть.
Stripe-Signature никто не проверяет, поэтому утёкший URL — это машина ложных тревог, а не утечка данных. Держите его подальше от репозиториев и скриншотов, используйте Reset Token, если он всё-таки утёк, и прочитайте следующий раздел, если этот компромисс вас смущает.Опционально — Сначала проверить подпись
Если вы хотите, чтобы подпись Stripe действительно проверялась, а суммы выглядели как деньги, поставьте перед каналом небольшой прокси. Этот Cloudflare Worker проверяет событие, сразу отвечает 200, как просит Stripe, и отправляет в Echobell плоский payload:
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 не гарантирует порядок событий и может доставить одно и то же событие несколько раз. Два звонка на один диспут вполне возможны.
- Никакого подтверждения. Ничто не фиксирует, что человек это увидел, и ничто не передаёт второму, если никто не отреагировал.
- Нет оповещения «платежи прекратились». Stripe присылает события, когда что-то происходит, и никогда — когда перестаёт. Если сломается оформление заказа, не сработает ни одно событие. Для этого нужна задача по расписанию на вашей стороне, которая дёргает канал, когда за последний час ноль списаний, — «выключатель мертвеца» на cron.
Диагностика
В 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.
Может ли сооснователь получать те же оповещения без оплаты места?
Да. Поделитесь ссылкой на канал; каждый подписчик выбирает свой тип уведомления. Один получает диспуты звонком, другой — обычным пушем, и никакой платы за подписчика нет.
Стоит ли оповещать об успешных платежах?
Только недолго и только пока бизнес достаточно мал, чтобы каждый платёж ещё был событием. Как только уведомление об успешной оплате становится рутиной, оно начинает подтачивать вашу реакцию на действительно важные — базовый механизм усталости от оповещений.
Итог
Вся схема — это одно назначение событий Stripe на каждый канал Echobell, условие livemode == true и дисциплина: уровень звонка остаётся за событиями с тикающими часами. У диспутов и ранних предупреждений о мошенничестве они есть. У неудачного продления тарифа за $9 — нет, и попытка сделать вид, что есть, — это ровно тот путь, на котором проспишь то самое, с часами.
Скачайте Echobell для iPhone или возьмите в Google Play, создайте сначала канал диспутов и запустите stripe trigger charge.dispute.created, прежде чем доверить этому пути что-то настоящее.