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
| Event | Description |
|---|---|
processing | Payment initiated with provider |
success | Payment completed successfully |
failed | Payment failed at provider |
cancelled | Payment cancelled by customer |
error | System 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.