Build a Webhook Handler
Create a secure webhook handler to receive real-time notifications from AfriRoute for SMS delivery, payment confirmations, and more.
What You'll Build
- ✅ Secure webhook endpoint with signature verification
- ✅ Handle SMS delivery reports
- ✅ Process payment confirmations
- ✅ Implement retry logic
- ✅ Log and monitor events
Estimated Time: 20 minutes
Prerequisites
- ✅ AfriRoute Account
- ✅ Node.js 18+ or Python 3.8+
- ✅ ngrok for local testing
- ✅ Webhook secret from dashboard
Webhook Events
AfriRoute sends webhooks for:
sms.delivered- SMS deliveredsms.failed- SMS failedpayment.completed- Payment successfulpayment.failed- Payment failedvoice.completed- Voice call ended
Step 1: Setup (Node.js)
npm install express crypto body-parser dotenv
webhook-handler.js
const express = require('express');
const crypto = require('crypto');
require('dotenv').config();
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Verify webhook signature
function verifySignature(signature, payload) {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Main webhook endpoint
app.post('/webhook', (req, res) => {
try {
const signature = req.headers['x-afriroute-signature'];
if (!signature || !verifySignature(signature, req.body)) {
console.log('❌ Invalid signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
console.log(`📨 Webhook received: ${event}`);
// Route to appropriate handler
switch (event) {
case 'sms.delivered':
handleSMSDelivered(data);
break;
case 'sms.failed':
handleSMSFailed(data);
break;
case 'payment.completed':
handlePaymentCompleted(data);
break;
case 'payment.failed':
handlePaymentFailed(data);
break;
default:
console.log(`Unknown event: ${event}`);
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
function handleSMSDelivered(data) {
console.log(`✅ SMS delivered to ${data.phone}`);
// Update database
// Send confirmation email
}
function handleSMSFailed(data) {
console.log(`❌ SMS failed to ${data.phone}: ${data.error}`);
// Retry logic
// Notify admin
}
function handlePaymentCompleted(data) {
console.log(`💰 Payment completed: ${data.transaction_id}`);
// Update order status
// Send receipt
// Fulfill order
}
function handlePaymentFailed(data) {
console.log(`❌ Payment failed: ${data.transaction_id}`);
// Notify customer
// Cancel order
}
app.listen(3000, () => {
console.log('🔔 Webhook handler running on port 3000');
});
Step 2: Python Implementation
webhook_handler.py
import os
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET').encode()
def verify_signature(signature, payload):
"""Verify webhook signature"""
expected = hmac.new(
WEBHOOK_SECRET,
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhook', methods=['POST'])
def webhook():
try:
signature = request.headers.get('X-AfriRoute-Signature')
if not signature or not verify_signature(signature, request.data):
return jsonify({'error': 'Invalid signature'}), 401
data = request.json
event = data.get('event')
payload = data.get('data')
print(f"📨 Webhook: {event}")
# Route to handler
handlers = {
'sms.delivered': handle_sms_delivered,
'sms.failed': handle_sms_failed,
'payment.completed': handle_payment_completed,
'payment.failed': handle_payment_failed
}
handler = handlers.get(event)
if handler:
handler(payload)
else:
print(f"Unknown event: {event}")
return jsonify({'received': True}), 200
except Exception as e:
print(f"Error: {str(e)}")
return jsonify({'error': 'Internal error'}), 500
def handle_sms_delivered(data):
print(f"✅ SMS delivered to {data['phone']}")
# Update database
def handle_sms_failed(data):
print(f"❌ SMS failed: {data['error']}")
# Retry or alert
def handle_payment_completed(data):
print(f"💰 Payment: {data['transaction_id']}")
# Fulfill order
def handle_payment_failed(data):
print(f"❌ Payment failed: {data['transaction_id']}")
# Cancel order
if __name__ == '__main__':
app.run(port=3000, debug=True)
Step 3: Test Locally with ngrok
# Install ngrok
brew install ngrok # macOS
# Or download from https://ngrok.com
# Start your webhook server
node webhook-handler.js
# In another terminal, start ngrok
ngrok http 3000
You'll get a URL like: https://abc123.ngrok.io
Step 4: Configure Webhook in Dashboard
- Go to www.afriroute.ai/auth/login/webhooks
- Add webhook URL:
https://abc123.ngrok.io/webhook - Select events to receive
- Copy webhook secret to
.env
Step 5: Test Webhook
# Send test event from dashboard
# Or use curl:
curl -X POST https://your-url/webhook \
-H "Content-Type: application/json" \
-H "X-AfriRoute-Signature: YOUR_SIGNATURE" \
-d '{
"event": "sms.delivered",
"data": {
"message_id": "msg_123",
"phone": "+254700123456",
"status": "delivered",
"delivered_at": "2025-12-11T10:30:00Z"
}
}'
Best Practices
1. Always Verify Signatures
if (!verifySignature(signature, req.body)) {
return res.status(401).send('Unauthorized');
}
2. Respond Quickly
// Process async, respond immediately
res.status(200).json({ received: true });
processWebhookAsync(data);
3. Implement Idempotency
const processedEvents = new Set();
if (processedEvents.has(data.id)) {
return res.status(200).json({ received: true });
}
processedEvents.add(data.id);
4. Log Everything
console.log({
timestamp: new Date(),
event: event,
data: data,
signature: signature
});
5. Handle Retries
AfriRoute retries failed webhooks (3 attempts with exponential backoff)
Production Deployment
Heroku
heroku create my-webhook-handler
heroku config:set WEBHOOK_SECRET=your_secret
git push heroku main
AWS Lambda
exports.handler = async (event) => {
const body = JSON.parse(event.body);
const signature = event.headers['x-afriroute-signature'];
// Verify and process
return {
statusCode: 200,
body: JSON.stringify({ received: true })
};
};
Troubleshooting
Webhook not received:
- Check URL is publicly accessible
- Verify webhook is configured in dashboard
- Check firewall settings
Signature verification fails:
- Verify webhook secret is correct
- Check payload is not modified
- Use raw body for verification
Timeouts:
- Respond within 5 seconds
- Process async if needed
Next Steps
- 📱 SMS API
- 💰 Payments
- 📦 Bulk Messaging
Resources
Pro Tip
Always verify webhook signatures to prevent unauthorized requests!