Nile PayNile Pay

Webhooks

Receive real-time transaction notifications when payments succeed, fail, or require attention.

Overview

Webhooks deliver HTTP POST requests to your configured endpoint when transaction events occur. This enables real-time updates without polling.

Configuration

Set your webhook URL in the merchant dashboard under Settings > Webhooks. The backend sends POST requests to this endpoint for all transaction events.

Payload Format

Each webhook request contains a JSON body with transaction data:

{
  "event": "success",
  "transactionId": "txn_abc123",
  "reference": "ORDER-001",
  "status": "successful",
  "amount": 50000,
  "currency": "UGX",
  "providerReference": "MTN-REF-789",
  "createdAt": "2026-04-01T10:30:00Z",
  "updatedAt": "2026-04-01T10:30:15Z",
  "signature": "sha256=..."
}

Event Types

EventDescription
processingPayment initiated with provider
successPayment completed successfully
failedPayment failed at provider
cancelledPayment cancelled by customer
errorSystem or provider error occurred

Signature Verification

Verify webhook authenticity using the signature header. This prevents unauthorized requests.

import { createHmac } from 'crypto';

export function verifyWebhookSignature(
  payload: string,
  signature: string,
  secretKey: string
): boolean {
  const expectedSignature = createHmac('sha256', secretKey)
    .update(payload)
    .digest('hex');
  
  return `sha256=${expectedSignature}` === signature;
}

Always verify before processing. Reject requests with invalid signatures.

Retry Behavior

Failed deliveries (non-200 response or timeout) trigger exponential backoff:

  • Attempt 1: Immediate
  • Attempt 2: 1 minute delay
  • Attempt 3: 5 minutes delay
  • Attempt 4: 30 minutes delay
  • Attempt 5: 2 hours delay

After 5 failed attempts, the webhook is marked as failed. You can manually retry from the dashboard.

Best Practices

Respond Quickly

Return HTTP 200 immediately. Process the event asynchronously:

app.post('/webhooks/nilepay', async (req, res) => {
  // Acknowledge receipt immediately
  res.status(200).send('OK');
  
  // Process in background
  processQueue.push(req.body);
});

Handle Duplicates

Webhook delivery guarantees "at least once" delivery. Your handler must be idempotent:

async function handleWebhook(event: WebhookPayload) {
  // Check if already processed
  const exists = await db.processedEvents.findOne({
    transactionId: event.transactionId,
    event: event.event,
  });
  
  if (exists) return; // Skip duplicate
  
  // Process the event
  await processPaymentEvent(event);
  
  // Record processed event
  await db.processedEvents.create({
    transactionId: event.transactionId,
    event: event.event,
    processedAt: new Date(),
  });
}

Log Everything

Store raw webhook payloads with timestamps for debugging and audit trails.

Testing Webhooks

Use the sandbox environment to test webhook handling before going live. See the Testing guide for details.

On this page