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.
General rules
Section titled “General rules”- 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).
Base wrapper
Section titled “Base wrapper”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, )Stripe (Node.js + Express)
Section titled “Stripe (Node.js + Express)”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 matchapp.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 (Python + FastAPI)
Section titled “YooKassa (Python + FastAPI)”YooKassa has its own event types; the principle is the same — verify authenticity and sort:
import ipaddressfrom fastapi import FastAPI, Request, HTTPExceptionfrom notifly import notify
app = FastAPI()
# YooKassa sends webhooks only from its own IPs — verify the sourceYOOKASSA_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}Alternative: Webhook Router with no code of your own
Section titled “Alternative: Webhook Router with no code of your own”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=5This 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.
What else to send
Section titled “What else to send”- 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.
Benefits
Section titled “Benefits”- 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_failedoften means something broke on your side. - Signature verified — fake “payments” won’t get through.