Building a Payment Notification System
Learn how to receive real-time payment events from the XRP Ledger and deliver push notifications to your users when funds arrive.
Architecture overview
The data flow for a payment notification system is straightforward. XRNotify monitors the XRPL on your behalf and forwards matching events to your server, which then fans out to whichever push notification service your app uses.
XRPL Mainnet → XRNotify → Your Server → Push Notification Service → User
- XRPL Mainnet — the source of truth. Every payment, NFT transfer, and DEX trade is recorded in an immutable ledger that closes roughly every 3–4 seconds.
- XRNotify — listens to the validated ledger stream, detects events matching your webhook configuration, signs and delivers HTTP POST requests to your server with a 10-second timeout and automatic retries.
- Your Server — verifies the webhook signature, updates your database, and dispatches the notification.
- Push Notification Service — any provider such as Firebase Cloud Messaging, Apple Push Notification Service, or a third-party service like OneSignal.
- User — receives a push notification on their device within seconds of the ledger closing.
Step 1: Set up the webhook
Subscribe to payment.xrp and payment.issued for a specific wallet address. Using an account_filters array narrows delivery to only events where your user's address is the sender or receiver, keeping your payload volume low.
curl -X POST https://api.xrnotify.io/v1/webhooks \
-H "X-XRNotify-Key: xrn_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourapp.com/webhooks/payments",
"event_types": ["payment.xrp", "payment.issued"],
"account_filters": ["rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"]
}'The response includes the webhook's secret field — store this securely. You will use it to verify signatures on every incoming delivery.
Step 2: Handle payment.xrp events
Set up a route that accepts raw request bodies (required for HMAC signature verification), verifies the signature, then converts the amount from drops to XRP before dispatching the notification.
app.post('/webhooks/payments', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-xrnotify-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
if (event.event_type === 'payment.xrp') {
const { receiver, amount, destination_tag } = event.payload;
const amountXrp = (parseInt(amount) / 1_000_000).toFixed(6);
// Find user by address
const user = await db.users.findByXrplAddress(receiver);
if (!user) return res.sendStatus(200); // Not our user
// Update balance in DB
await db.balances.increment(user.id, amountXrp);
// Send push notification
await pushService.send({
userId: user.id,
title: 'Payment Received',
body: `You received ${amountXrp} XRP`,
});
}
res.sendStatus(200);
});1_000_000 before displaying or storing as XRP.Step 3: Handle payment.issued events
Issued currency payments carry additional fields — currency, issuer, and value — instead of a raw drops amount.
if (event.event_type === 'payment.issued') {
const { receiver, currency, issuer, value } = event.payload;
const user = await db.users.findByXrplAddress(receiver);
if (!user) return res.sendStatus(200);
// Update token balance; key by (currency, issuer) pair
await db.tokenBalances.increment(user.id, currency, issuer, value);
await pushService.send({
userId: user.id,
title: 'Token Payment Received',
body: `You received ${value} ${currency}`,
});
}Note that the same currency code can be issued by different addresses, so you must always treat the (currency, issuer) pair as the composite key for a token balance.
Step 4: Handle edge cases
Idempotency
XRNotify retries deliveries if your endpoint returns a non-2xx status or times out. Use the X-XRNotify-Delivery-Id header to detect and skip duplicates:
const deliveryId = req.headers['x-xrnotify-delivery-id'];
const alreadyProcessed = await cache.get(`delivery:${deliveryId}`);
if (alreadyProcessed) return res.sendStatus(200);
// ... process event ...
await cache.set(`delivery:${deliveryId}`, 'done', 86400); // 24h TTLFailed deliveries
If your endpoint was unreachable during a window, use the Replay API to re-deliver all missed events in bulk rather than retrying individual deliveries one by one. See the Replay API reference for details.
Destination tags
XRPL destination tags are integers that senders attach to payments to identify the beneficiary on a shared address. Map tags to internal user IDs in your database so you can credit the correct account even when many users share a single deposit address.
Step 5: Monitor delivery health
Keep an eye on your webhook's delivery success rate to catch outages early. You can query delivery stats from the API:
curl "https://api.xrnotify.io/v1/webhooks/wh_abc123/stats" \ -H "X-XRNotify-Key: xrn_live_xxx"
The response includes total_deliveries, successful_deliveries, failed_deliveries, and success_rate. You can also view a visual breakdown on the dashboard.