Why ngrok?
Africa’s Talking services (Voice, SMS, USSD, Payments) use webhooks to communicate with your application. During local development, your application runs on localhost, which is not accessible from the internet.
ngrok creates a secure tunnel from a public URL to your local server, allowing Africa’s Talking to send webhook events to your development environment.
Webhook Flow
Prerequisites
Working VoicePact development setup (see guide )
FastAPI server running on port 8000
Africa’s Talking account (sandbox or production)
Installation
Option 1: Download Binary (Recommended)
Visit ngrok website : https://ngrok.com/download
Create free account (required for authentication)
Download for your platform:
Linux: ngrok-v3-stable-linux-amd64.tgz
macOS: ngrok-v3-stable-darwin-amd64.zip
Windows: ngrok-v3-stable-windows-amd64.zip
Extract and move to PATH :
# Linux/macOS
unzip ngrok-v3-stable- * .zip
sudo mv ngrok /usr/local/bin/
# Verify installation
ngrok version
Option 2: Package Manager
# macOS (Homebrew)
brew install ngrok/ngrok/ngrok
# Linux (Snap)
sudo snap install ngrok
# Windows (Chocolatey)
choco install ngrok
Authentication
After installation, authenticate with your ngrok account:
Get your authtoken : https://dashboard.ngrok.com/get-started/your-authtoken
Configure ngrok :
ngrok config add-authtoken YOUR_AUTHTOKEN_HERE
The authtoken is stored in ~/.ngrok2/ngrok.yml and only needs to be set once.
Basic Usage
Start ngrok Tunnel
With your FastAPI server running on port 8000:
# Terminal 1: Start your server
cd server
source venv/bin/activate
uvicorn main:app --reload --port 8000
# Terminal 2: Start ngrok
ngrok http 8000
ngrok Dashboard Output
ngrok
Session Status online
Account [email protected] (Plan: Free)
Version 3.x.x
Region United States (us)
Forwarding https://abc123.ngrok.io -> http://localhost:8000
Web Interface http://127.0.0.1:4040
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Important : Copy the HTTPS forwarding URL (e.g., https://abc123.ngrok.io)
Update Environment Variables
Update .env with your ngrok URL:
# server/.env
# Before
WEBHOOK_BASE_URL = https://your-ngrok-url.ngrok.io
# After (example)
WEBHOOK_BASE_URL = https://abc123.ngrok.io
ngrok URLs change every time you restart ngrok (on free plan). Update .env each time.
Restart Server
For changes to take effect:
# Stop server (Ctrl+C)
# Restart
uvicorn main:app --reload --port 8000
Verify Configuration
Check webhook URL is set correctly:
curl http://localhost:8000/info | jq
Or visit http://localhost:8000/docs and check the info endpoint.
Voice Callback URL
In the Africa’s Talking dashboard :
Navigate to Voice → Settings
Set Callback URL :
https://abc123.ngrok.io/api/v1/voice/webhook
Click Save
SMS Callback URL
Navigate to SMS → Settings
Set Delivery Reports Callback URL :
https://abc123.ngrok.io/api/v1/sms/webhook
Set Incoming Messages Callback URL (same URL)
Click Save
USSD Callback URL
Navigate to USSD → Service Code
Set Callback URL :
https://abc123.ngrok.io/api/v1/ussd/webhook
Click Save
Payment Callback URL
Navigate to Payments → Settings
Set Validation URL :
https://abc123.ngrok.io/api/v1/payments/validate
Set Confirmation URL :
https://abc123.ngrok.io/api/v1/payments/confirm
Click Save
Testing Webhooks
Monitor Incoming Requests
ngrok provides a web interface to inspect webhook traffic:
Open web interface : http://127.0.0.1:4040
View requests in real-time
Inspect headers, body, and responses
Replay requests for debugging
Test Voice Webhook
From server/app/api/v1/endpoints/voice.py:223:
@router.post ( "/webhook" )
async def voice_webhook (
request : Request,
at_client : AfricasTalkingClient = Depends(get_africastalking_client),
db : AsyncSession = Depends(get_db)
):
try :
body = await request.body()
form_data = await request.form()
session_id = form_data.get( "sessionId" )
phone_number = form_data.get( "phoneNumber" )
recording_url = form_data.get( "recordingUrl" )
duration = form_data.get( "duration" )
status = form_data.get( "status" , "completed" )
logger.info( f "Voice webhook received: { session_id } " )
# Process webhook...
return { "status" : "webhook_processed" , "session_id" : session_id}
except Exception as e:
logger.error( f "Voice webhook processing failed: { e } " )
return { "status" : "webhook_error" , "error" : str (e)}
Trigger a voice call and watch the webhook arrive:
# Watch server logs
tail -f server/logs/app.log
# Or check ngrok web interface
open http://127.0.0.1:4040
Test SMS Webhook
From server/app/api/v1/endpoints/sms.py:293:
@router.post ( "/webhook" )
async def sms_webhook ( request : Request):
try :
form_data = await request.form()
webhook_data = dict (form_data)
logger.info( f "SMS webhook received: { webhook_data } " )
phone_number = webhook_data.get( "from" )
message = webhook_data.get( "text" , "" ).upper().strip()
# Handle contract confirmations
if message.startswith( "YES-" ) or message.startswith( "NO-" ):
contract_id = message.split( "-" , 1 )[ 1 ] if "-" in message else "unknown"
action = "confirm" if message.startswith( "YES-" ) else "reject"
logger.info( f "Contract { action } : { contract_id } from { phone_number } " )
return {
"action" : action,
"contract_id" : contract_id,
"phone_number" : phone_number
}
return { "status" : "webhook_received" }
except Exception as e:
logger.error( f "SMS webhook error: { e } " )
return { "status" : "webhook_error" , "error" : str (e)}
Send SMS to test:
# Run SMS demo
python tests / test_sms_demo.py
# Or send via API
curl - X POST http: // localhost: 8000 / api / v1 / sms / send \
- H "Content-Type: application/json" \
- d '{
"phoneNumber" : "+254712345678" ,
"message" : "Test message"
} '
Manual Webhook Testing
Test webhooks without Africa’s Talking:
# Test voice webhook
curl -X POST https://abc123.ngrok.io/api/v1/voice/webhook \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "sessionId=test-session" \
-d "phoneNumber=+254712345678" \
-d "recordingUrl=https://example.com/recording.mp3" \
-d "duration=120" \
-d "status=completed"
# Test SMS webhook
curl -X POST https://abc123.ngrok.io/api/v1/sms/webhook \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "from=+254712345678" \
-d "text=YES-AG-123456" \
-d "id=message-id" \
-d "date=2024-03-06T12:00:00Z"
Advanced Configuration
Custom Subdomain (Paid Plan)
ngrok paid plans allow custom subdomains:
ngrok http 8000 --subdomain=voicepact-dev
# Results in: https://voicepact-dev.ngrok.io
Benefits:
Stable URL (doesn’t change on restart)
No need to update .env every time
Professional appearance
Configuration File
Create ~/.ngrok2/ngrok.yml:
version : "2"
authtoken : YOUR_AUTHTOKEN_HERE
tunnels :
voicepact :
proto : http
addr : 8000
subdomain : voicepact-dev # Paid plan only
inspect : true
Start with:
Multiple Tunnels
Run multiple services:
tunnels :
backend :
proto : http
addr : 8000
frontend :
proto : http
addr : 3000
Start both:
Regional Endpoints
Choose ngrok region for lower latency:
# US (default)
ngrok http 8000 --region us
# Europe
ngrok http 8000 --region eu
# Asia Pacific
ngrok http 8000 --region ap
# Australia
ngrok http 8000 --region au
Troubleshooting
ngrok tunnel not starting
Symptom : ERR_NGROK_108 or connection failedSolution :# Check if port 8000 is in use
lsof -i :8000
# Verify server is running
curl http://localhost:8000/health
# Try different port
uvicorn main:app --reload --port 8001
ngrok http 8001
Symptom : No webhook events receivedSolution :
Check ngrok is running: curl https://your-url.ngrok.io/health
Verify webhook URL in Africa’s Talking dashboard
Check ngrok web interface (http://127.0.0.1:4040 ) for requests
Review server logs for errors
Test with manual curl request
Webhook URL keeps changing
Symptom : ngrok URL changes on every restartSolution :
Free plan : This is expected. Update .env each time.
Paid plan : Use custom subdomain (see Advanced Configuration)
Alternative : Use a development deployment (e.g., Railway, Render)
ERR_NGROK_302 Account limit reached
Symptom : Cannot start tunnel, limit reachedSolution :
Free plan: 1 tunnel at a time. Stop other tunnels.
Check for zombie ngrok processes: pkill ngrok
Upgrade to paid plan for multiple tunnels
502 Bad Gateway from ngrok
Symptom : ngrok returns 502 errorSolution :# Server is not running - start it
cd server
source venv/bin/activate
uvicorn main:app --reload --port 8000
# Verify server is accessible
curl http://localhost:8000/health
Development Workflow
Daily Setup Routine
# 1. Start Redis
docker run -d -p 6379:6379 redis:alpine
# 2. Start FastAPI server
cd server
source venv/bin/activate
uvicorn main:app --reload --port 8000
# 3. Start ngrok (new terminal)
ngrok http 8000
# 4. Copy ngrok URL
# https://abc123.ngrok.io
# 5. Update .env
# WEBHOOK_BASE_URL=https://abc123.ngrok.io
# 6. Restart server (Ctrl+C, then rerun uvicorn)
# 7. Update Africa's Talking dashboard callbacks
# (Only if testing voice/SMS/USSD)
Quick Test Script
Create dev-start.sh:
#!/bin/bash
echo "Starting VoicePact development environment..."
# Start Redis
echo "Starting Redis..."
docker run -d -p 6379:6379 redis:alpine
# Start server in background
echo "Starting FastAPI server..."
cd server
source venv/bin/activate
uvicorn main:app --reload --port 8000 &
SERVER_PID = $!
sleep 2
# Start ngrok
echo "Starting ngrok..."
ngrok http 8000 &
NGROK_PID = $!
echo ""
echo "✅ Development environment started!"
echo "Server: http://localhost:8000"
echo "Docs: http://localhost:8000/docs"
echo "ngrok Inspector: http://127.0.0.1:4040"
echo ""
echo "Copy the ngrok HTTPS URL and update WEBHOOK_BASE_URL in .env"
echo ""
echo "Press Ctrl+C to stop all services"
# Wait for Ctrl+C
trap "kill $SERVER_PID $NGROK_PID ; exit" INT
wait
Make executable and run:
chmod +x dev-start.sh
./dev-start.sh
Alternatives to ngrok
LocalTunnel
Free alternative with stable URLs:
# Install
npm install -g localtunnel
# Start tunnel
lt --port 8000 --subdomain voicepact
# https://voicepact.loca.lt
Cloudflare Tunnel
Free with Cloudflare account:
# Install
brew install cloudflared
# Start tunnel
cloudflared tunnel --url http://localhost:8000
Tailscale Funnel
For team development:
# Install Tailscale
# https://tailscale.com/download
# Expose service
tailscale funnel 8000
Production Considerations
Never use ngrok in production. It’s designed for development only.
For production:
Deploy to a cloud provider (AWS, GCP, Azure, DigitalOcean)
Use proper domain with SSL certificate
Configure webhook URLs to production domain
Implement webhook signature verification
Set up monitoring and alerting
See deployment guides for production setup.
Next Steps
Local Development Return to development setup guide
Testing Guide Learn how to test webhooks
Voice API Explore Voice API endpoints
SMS API Explore SMS API endpoints