Skip to main content
Guides

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);
});
XRP amounts on the ledger are always expressed in drops (1 XRP = 1,000,000 drops). Always divide by 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 TTL

Failed 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.

Next steps