Skip to main content

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

  1. Visit ngrok website: https://ngrok.com/download
  2. Create free account (required for authentication)
  3. 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
  4. 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:
  1. Get your authtoken: https://dashboard.ngrok.com/get-started/your-authtoken
  2. 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)

Configure VoicePact

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.

Configure Africa’s Talking

Voice Callback URL

In the Africa’s Talking dashboard:
  1. Navigate to VoiceSettings
  2. Set Callback URL:
    https://abc123.ngrok.io/api/v1/voice/webhook
    
  3. Click Save

SMS Callback URL

  1. Navigate to SMSSettings
  2. Set Delivery Reports Callback URL:
    https://abc123.ngrok.io/api/v1/sms/webhook
    
  3. Set Incoming Messages Callback URL (same URL)
  4. Click Save

USSD Callback URL

  1. Navigate to USSDService Code
  2. Set Callback URL:
    https://abc123.ngrok.io/api/v1/ussd/webhook
    
  3. Click Save

Payment Callback URL

  1. Navigate to PaymentsSettings
  2. Set Validation URL:
    https://abc123.ngrok.io/api/v1/payments/validate
    
  3. Set Confirmation URL:
    https://abc123.ngrok.io/api/v1/payments/confirm
    
  4. Click Save

Testing Webhooks

Monitor Incoming Requests

ngrok provides a web interface to inspect webhook traffic:
  1. Open web interface: http://127.0.0.1:4040
  2. View requests in real-time
  3. Inspect headers, body, and responses
  4. 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:
ngrok start voicepact

Multiple Tunnels

Run multiple services:
tunnels:
  backend:
    proto: http
    addr: 8000
  frontend:
    proto: http
    addr: 3000
Start both:
ngrok start --all

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

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:
  1. Check ngrok is running: curl https://your-url.ngrok.io/health
  2. Verify webhook URL in Africa’s Talking dashboard
  3. Check ngrok web interface (http://127.0.0.1:4040) for requests
  4. Review server logs for errors
  5. Test with manual curl request
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)
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
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

Build docs developers (and LLMs) love