Skip to main content

Overview

The SMS webhook endpoint receives delivery reports and incoming SMS messages from Africa’s Talking. This is essential for tracking message delivery status and handling user responses to contract confirmations.

Endpoint

POST /api/v1/sms/webhook

Webhook Payload

Africa’s Talking sends webhook data as form-encoded data with the following fields:

Incoming SMS Fields

FieldTypeDescription
fromstringPhone number of the sender
tostringYour shortcode or phone number
textstringContent of the SMS message
datestringTimestamp of the message
idstringUnique message ID
linkIdstringLink ID for session-based messages

Delivery Report Fields

FieldTypeDescription
idstringMessage ID
statusstringDelivery status (Success, Failed, etc.)
phoneNumberstringRecipient phone number
networkCodestringMobile network code
retryCountnumberNumber of retry attempts
failureReasonstringReason for failure (if applicable)

Example Payloads

Incoming SMS Message

{
  "from": "+254712345678",
  "to": "40404",
  "text": "YES-AG-2024-001",
  "date": "2024-03-06 10:30:45",
  "id": "SmsId_abc123",
  "linkId": "SmsLinkId_xyz789"
}

Delivery Report

{
  "id": "ATXid_abc123",
  "status": "Success",
  "phoneNumber": "+254712345678",
  "networkCode": "63902",
  "retryCount": 0
}

Implementation

The webhook handler processes incoming messages and identifies contract-related responses:
@router.post("/webhook")
async def sms_webhook(request: Request):
    """Handle SMS delivery reports and responses"""
    try:
        form_data = await request.form()
        webhook_data = dict(form_data)
        
        # Extract key fields
        phone_number = webhook_data.get("from")
        message = webhook_data.get("text", "").upper().strip()
        
        response = {"status": "webhook_received"}
        
        # Handle contract confirmations (YES-{contract_id} or NO-{contract_id})
        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}")
            
            response.update({
                "action": action,
                "contract_id": contract_id,
                "phone_number": phone_number
            })
        
        return response
        
    except Exception as e:
        logger.error(f"SMS webhook error: {e}")
        return {"status": "webhook_error", "error": str(e)}

Contract Response Format

VoicePact uses a specific format for SMS-based contract responses:
  • Confirmation: YES-{contract_id} (e.g., YES-AG-2024-001)
  • Rejection: NO-{contract_id} (e.g., NO-AG-2024-001)
When users send these formatted responses, the webhook:
  1. Parses the action (YES/NO) and contract ID
  2. Logs the response
  3. Updates the contract status in the database
  4. Sends confirmation SMS to both parties

Security Considerations

Webhook Signature Verification

Verify webhook authenticity using HMAC signature validation:
import hmac
import hashlib

def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
    """Verify Africa's Talking webhook signature"""
    expected_signature = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(f"sha256={expected_signature}", signature)

# Usage in webhook handler
@router.post("/webhook")
async def sms_webhook(request: Request):
    # Get signature from header
    signature = request.headers.get("X-AT-Signature")
    
    # Get raw body for verification
    body = await request.body()
    
    # Verify signature
    if not verify_webhook_signature(body.decode(), signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")
    
    # Process webhook...

Best Practices

  1. IP Whitelisting: Restrict webhook endpoint to Africa’s Talking IP addresses
  2. HTTPS Only: Always use HTTPS for webhook URLs
  3. Idempotency: Handle duplicate webhooks gracefully using message IDs
  4. Timeout Handling: Respond quickly (within 10 seconds) to avoid retries
  5. Error Logging: Log all webhook data for debugging and audit trails

Configuring the Webhook URL

Set your webhook URL in the Africa’s Talking dashboard:
  1. Log in to Africa’s Talking Dashboard
  2. Navigate to SMS > Callback URLs
  3. Set Delivery Reports URL: https://your-domain.com/api/v1/sms/webhook
  4. Set Incoming Messages URL: https://your-domain.com/api/v1/sms/webhook
  5. Save and test the configuration

Testing

Local Testing with ngrok

# Start your server
uvicorn app.main:app --reload --port 8000

# In another terminal, expose with ngrok
ngrok http 8000

# Use the ngrok URL in Africa's Talking dashboard
https://abc123.ngrok.io/api/v1/sms/webhook

Test Incoming SMS

Send a test SMS to your shortcode or long number:
# Using curl to simulate webhook
curl -X POST https://your-domain.com/api/v1/sms/webhook \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "from=%2B254712345678" \
  -d "to=40404" \
  -d "text=YES-AG-2024-001" \
  -d "date=2024-03-06+10:30:45" \
  -d "id=SmsId_test123"

Expected Response

{
  "status": "webhook_received",
  "action": "confirm",
  "contract_id": "AG-2024-001",
  "phone_number": "+254712345678"
}

Monitoring and Debugging

Check Webhook Logs

Monitor webhook activity in your application logs:
# View recent webhook logs
tail -f logs/app.log | grep "SMS webhook"

# Filter for specific phone number
tail -f logs/app.log | grep "SMS webhook" | grep "+254712345678"

Africa’s Talking Dashboard

View delivery reports and webhook attempts in the dashboard:
  • SMS > Sent Messages: View delivery status
  • SMS > Received Messages: View incoming messages
  • Logs: Check webhook delivery attempts and responses

Error Handling

ErrorCauseSolution
401 UnauthorizedInvalid signatureVerify webhook secret configuration
400 Bad RequestMalformed payloadCheck payload format and required fields
500 Internal ErrorServer errorCheck application logs and database connection
TimeoutSlow responseOptimize webhook handler (use async processing)

Additional Resources

Build docs developers (and LLMs) love