Skip to content

Payment and payment-failure notifications

A successful payment is pleasant, and you can quietly rejoice. But a failed payment, a refund, or a dispute (chargeback) is something you need to see right away: a dispute can incur a fine, and a run of declines often means the integration is broken. We catch the payment provider’s webhook and sort events by priority.

  • Server-side only. The webhook arrives at your backend; call notify() from there.
  • Verify the webhook signature. Without signature verification, anyone can send you fake “payments”. This is a mandatory step.
  • Priorities by meaning. Success is normal (5), decline/dispute is high (8–9).

We take notify() from the overview page:

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,
)

We verify the signature via stripe.webhooks.constructEvent and sort the events:

const express = require('express');
const Stripe = require('stripe');
const {notify} = require('./notifly');
const stripe = Stripe(process.env.STRIPE_SECRET);
const app = express();
// important: raw body, otherwise the signature won't match
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}`); // fake — discard
}
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});
});

YooKassa has its own event types; the principle is the same — verify authenticity and sort:

import ipaddress
from fastapi import FastAPI, Request, HTTPException
from notifly import notify
app = FastAPI()
# YooKassa sends webhooks only from its own IPs — verify the source
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") # foreign source — discard
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}

If you don’t want to run an endpoint, point the payment provider’s webhook straight at Notifly — the Webhook Router will parse the JSON itself and build a push from a template. Set it as the webhook URL:

https://notifly.example.com/webhook/router?token=APP_TOKEN
&title=Оплата {{data.object.amount}}
&message=Событие: {{type}}
&priority=5

This is handy for “just tell me an event came in”. But signature verification and priority sorting (success/decline/dispute) are more precisely done in your own code — the Router is great for a quick start.

  • Approaching a limit (trial ending, charge coming up soon) — quietly.
  • A run of consecutive declines from one card/client — possibly fraud; priority 8.
  • A large payment above your average order value — a separate pleasant push.
  • A dispute lands in your pocket immediately. A chargeback has tight response deadlines; learning about it a day later from an email means losing money.
  • A broken integration is visible. A sudden run of payment_failed often means something broke on your side.
  • Signature verified — fake “payments” won’t get through.