Перейти к содержимому

Уведомления об оплатах и сбоях платежей

Оплата прошла — приятно и можно тихо порадоваться. А вот неудачный платёж, возврат или диспут (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.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 свои типы событий; принцип тот же — проверить подлинность и разложить:

import ipaddress
from fastapi import FastAPI, Request, HTTPException
from 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}

Если не хочется поднимать эндпойнт, направьте вебхук платёжной системы прямо в NotiflyWebhook 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 часто значит, что что-то отвалилось на вашей стороне.
  • Подпись проверена — фейковые «оплаты» не пролезут.