Nylon PayNylon Pay

Webhooks

Receive real-time payment notifications on your server

Webhooks are HTTP POST requests Nylon Pay sends to your server when a transaction status changes. Use them to update your database, fulfill orders, or reconcile ledgers without polling.

Quick Start

  1. Set your webhook URL in Dashboard > Settings > Webhooks
  2. Your endpoint receives POST requests for transaction events
  3. Return HTTP 200 within 5 seconds to acknowledge delivery
  4. Verify the signature before processing the event

Event Types

EventWhen it happensWhat to do
transaction.successfulTransaction completed successfullyFulfill order, send receipt, credit account
transaction.failedTransaction was rejected or timed outNotify customer, offer retry
transaction.processingTransaction is being processedUpdate status in your system
transaction.cancelledTransaction was cancelledUpdate order state

Payload Format

Each webhook is a POST request with a JSON body. The signature is in the x-nylon-signature request header, not in the body.

The payload shape is identical for every event. Your handler reads the same fields regardless of which status triggered it.

{
  "delivery_id": "d7f3a1b2-...",
  "event": "transaction.successful",
  "payload": {
    "transactionId": "8e4f2c1a-...",
    "reference": "550e8400-e29b-41d4-a716-446655440000",
    "amount": "50000",
    "currency": "UGX",
    "status": "successful",
    "previousStatus": "processing",
    "type": "charge",
    "method": "mobileMoney",
    "mode": "live",
    "failureReason": null,
    "operatorTid": "MPS20240530123456"
  },
  "timestamp": "2026-05-30T10:30:15.000Z"
}
FieldDescription
delivery_idUnique ID for this delivery attempt. Use it for idempotency.
eventEvent name. See Event Types.
payload.transactionIdInternal transaction ID
payload.referenceYour reference UUID passed when creating the transaction
payload.amountTransaction amount as a string
payload.currencyThree-letter currency code
payload.statusNew transaction status
payload.previousStatusStatus before this transition
payload.typecharge for collections, payout for disbursements
payload.methodPayment method, for example mobileMoney
payload.modelive or test
payload.failureReasonHuman-readable reason for failed or cancelled events, otherwise null
payload.operatorTidMobile operator or bank transaction ID, or null if unavailable

Signature Verification

Every webhook includes an x-nylon-signature header with a signature of the raw request body. Verify it before processing the event using the SDK's built-in verifyWebhookSignature method.

verifyWebhookSignature returns true only when the signature is authentic and the webhook is fresh. See Replay protection below.

import { createNylonPay } from '@nile-squad/nylonpay-ts'

const nylonpay = createNylonPay({
  apiKey: 'npk_test_your_key',
  apiSecret: 'nps_test_your_secret',
})

Express Example

import express from 'express'

const app = express()

// Capture raw body. Verification must run against the raw bytes, not parsed JSON.
app.use(
  express.json({
    verify: (req, _res, buf) => {
      req.rawBody = buf.toString()
    },
  })
)

app.post('/webhooks', (req, res) => {
  const webhookSecret = process.env.NYLONPAY_WEBHOOK_SECRET
  const signature = req.headers['x-nylon-signature'] as string

  const isValid = nylonpay.verifyWebhookSignature({
    payload: req.rawBody,
    signature,
    secret: webhookSecret,
  })

  if (!isValid) {
    return res.status(401).send('Invalid signature')
  }

  // All events carry the same payload shape, so one handler covers every status.
  const { event, payload } = req.body

  switch (event) {
    case 'transaction.successful':
      fulfillOrder(payload.reference)
      break
    case 'transaction.failed':
    case 'transaction.cancelled':
      notifyCustomer(payload.reference, payload.failureReason)
      break
    case 'transaction.processing':
      updateOrderStatus(payload.reference, 'processing')
      break
  }

  res.status(200).send('OK')
})

Replay protection

A signature proves who sent a webhook, not when. Without an expiry, anyone who captures a valid delivery could replay it forever to re-trigger fulfilment. verifyWebhookSignature prevents this: after the signature checks out, it confirms the timestamp inside the signed body is recent, within 5 minutes by default. A stale or replayed delivery fails verification.

This never rejects legitimate traffic. Every delivery, including retries hours later, is re-stamped and re-signed by Nylon Pay, so a retry always looks fresh. Only a replay of an older captured payload is rejected.

Tune or disable the window with toleranceSeconds:

// Widen to 15 minutes for a slow or queued consumer
nylonpay.verifyWebhookSignature({
  payload: req.rawBody,
  signature: req.headers['x-nylon-signature'],
  secret: webhookSecret,
  toleranceSeconds: 900,
})

// Disable freshness entirely (not recommended)
nylonpay.verifyWebhookSignature({ /* ... */ toleranceSeconds: 0 })

Replay protection is defence in depth. Still apply your own idempotency, for example by deduplicating on delivery_id or the transaction reference, before acting on an event.

Delivery Guarantees

Nylon Pay delivers webhooks on an at-least-once basis. A single event may be sent more than once. Make your handler idempotent by tracking processed delivery IDs or transaction references.

async function handleWebhook(event, payload, deliveryId) {
  const processed = await db.get(`processed:${deliveryId}`)
  if (processed) return

  await processEvent(event, payload)
  await db.set(`processed:${deliveryId}`, true)
}

Retry Behavior

If your endpoint does not return HTTP 200, Nylon Pay retries with increasing delays.

AttemptDelay
1Immediate
2About 1 minute
3About 5 minutes
4About 30 minutes
5About 2 hours

After 5 failures the delivery is marked as failed. You can retry from the dashboard.

Best Practices

  • Return HTTP 200 immediately, process asynchronously
  • Use delivery_id or the transaction reference for idempotency
  • Read the signature from the x-nylon-signature header, not the body
  • Log raw payloads for debugging
  • Store your webhook secret in environment variables, not in code
  • Rotate the webhook secret periodically from the dashboard

Testing Webhooks

In sandbox mode, webhooks work the same as in production. Use a tool like ngrok to expose your local server:

ngrok http 3000

Set your webhook URL to https://your-ngrok-url.ngrok.io/webhook in the dashboard.

On this page