Skip to main content

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 delivered
  • sms.failed - SMS failed
  • payment.completed - Payment successful
  • payment.failed - Payment failed
  • voice.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​

  1. Go to www.afriroute.ai/auth/login/webhooks
  2. Add webhook URL: https://abc123.ngrok.io/webhook
  3. Select events to receive
  4. 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​

Resources​

Pro Tip

Always verify webhook signatures to prevent unauthorized requests!