Уведомления об оплатах и сбоях платежей
Оплата прошла — приятно и можно тихо порадоваться. А вот неудачный платёж, возврат или диспут (chargeback) — это то, что нужно увидеть сразу: за диспут можно получить штраф, а серия отказов часто означает поломку интеграции. Ловим вебхук платёжной системы и раскладываем события по приоритетам.
Общие правила
Заголовок раздела «Общие правила»- Только на сервере. Вебхук приходит на ваш бэкенд,
notify()вызываем оттуда. - Проверяйте подпись вебхука. Без проверки подписи кто угодно сможет слать вам фейковые «оплаты». Это обязательный шаг.
- Приоритеты по смыслу. Успех — обычный (5), отказ/диспут — высокий (8–9).
Базовая обёртка
Заголовок раздела «Базовая обёртка»Берём notify() из обзорной страницы:
import os, requests
def notify(title, message, priority=5): requests.post( f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5, )Stripe (Node.js + Express)
Заголовок раздела «Stripe (Node.js + Express)»Проверяем подпись через stripe.webhooks.constructEvent и раскладываем события:
const express = require('express');const Stripe = require('stripe');const {notify} = require('./notifly');
const stripe = Stripe(process.env.STRIPE_SECRET);const app = express();
// важно: raw body, иначе подпись не сойдётсяapp.post('/webhook/stripe', express.raw({type: 'application/json'}), (req, res) => { let event; try { event = stripe.webhooks.constructEvent( req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.status(400).send(`Bad signature: ${err.message}`); // фейк — отбрасываем }
const obj = event.data.object; switch (event.type) { case 'payment_intent.succeeded': notify('💰 Оплата', `${obj.amount / 100} ${obj.currency.toUpperCase()}`, 5); break; case 'payment_intent.payment_failed': notify('⚠️ Платёж не прошёл', `${obj.amount / 100} ${obj.currency.toUpperCase()}: ${obj.last_payment_error?.message}`, 8); break; case 'charge.dispute.created': notify('🚨 Диспут (chargeback)', `${obj.amount / 100} ${obj.currency.toUpperCase()}, причина: ${obj.reason}`, 9); break; } res.json({received: true});});ЮKassa (Python + FastAPI)
Заголовок раздела «ЮKassa (Python + FastAPI)»У ЮKassa свои типы событий; принцип тот же — проверить подлинность и разложить:
import ipaddressfrom fastapi import FastAPI, Request, HTTPExceptionfrom notifly import notify
app = FastAPI()
# ЮKassa шлёт вебхуки только со своих IP — проверяем источникYOOKASSA_NETS = [ipaddress.ip_network(n) for n in ( "185.71.76.0/27", "185.71.77.0/27", "77.75.153.0/25", "77.75.156.11/32",)]
def from_yookassa(ip: str) -> bool: addr = ipaddress.ip_address(ip) return any(addr in net for net in YOOKASSA_NETS)
@app.post("/webhook/yookassa")async def yookassa(request: Request): if not from_yookassa(request.client.host): raise HTTPException(403, "not from YooKassa") # чужой источник — отбрасываем
body = await request.json() obj = body.get("object", {}) amount = obj.get("amount", {}) label = f"{amount.get('value')} {amount.get('currency')}"
if body.get("event") == "payment.succeeded": notify("💰 Оплата", f"{label}, {obj.get('description', '')}", 5) elif body.get("event") == "payment.canceled": reason = obj.get("cancellation_details", {}).get("reason", "?") notify("⚠️ Платёж отменён", f"{label}, причина: {reason}", 8) elif body.get("event") == "refund.succeeded": notify("↩️ Возврат", label, 7) return {"ok": True}Альтернатива: Webhook Router без своего кода
Заголовок раздела «Альтернатива: Webhook Router без своего кода»Если не хочется поднимать эндпойнт, направьте вебхук платёжной системы прямо в Notifly — Webhook Router сам разберёт JSON и соберёт пуш по шаблону. Укажите как webhook URL:
https://notifly.example.com/webhook/router?token=APP_TOKEN &title=Оплата {{data.object.amount}} &message=Событие: {{type}} &priority=5Это удобно для «просто сообщи, что пришло событие». Но проверку подписи и раскладку по приоритетам (успех/отказ/диспут) точнее делать своим кодом — Router хорош для быстрого старта.
Что ещё присылать
Заголовок раздела «Что ещё присылать»- Приближение к лимиту (подходит окончание триала, скоро списание) — тихо.
- Серия отказов подряд от одной карты/клиента — возможно, фрод; приоритет 8.
- Крупная оплата выше вашего среднего чека — отдельный приятный пуш.
- Диспут — сразу в кармане. У chargeback жёсткие сроки на ответ; узнать через сутки из письма — потерять деньги.
- Поломка интеграции видна. Внезапная серия
payment_failedчасто значит, что что-то отвалилось на вашей стороне. - Подпись проверена — фейковые «оплаты» не пролезут.