Documentation Index
Fetch the complete documentation index at: https://mintlify.com/mutuiris/voicepact/llms.txt
Use this file to discover all available pages before exploring further.
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
Webhook Payload
Africa’s Talking sends webhook data as form-encoded data with the following fields:
Incoming SMS Fields
| Field | Type | Description |
|---|
from | string | Phone number of the sender |
to | string | Your shortcode or phone number |
text | string | Content of the SMS message |
date | string | Timestamp of the message |
id | string | Unique message ID |
linkId | string | Link ID for session-based messages |
Delivery Report Fields
| Field | Type | Description |
|---|
id | string | Message ID |
status | string | Delivery status (Success, Failed, etc.) |
phoneNumber | string | Recipient phone number |
networkCode | string | Mobile network code |
retryCount | number | Number of retry attempts |
failureReason | string | Reason 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)}
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:
- Parses the action (YES/NO) and contract ID
- Logs the response
- Updates the contract status in the database
- 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
- IP Whitelisting: Restrict webhook endpoint to Africa’s Talking IP addresses
- HTTPS Only: Always use HTTPS for webhook URLs
- Idempotency: Handle duplicate webhooks gracefully using message IDs
- Timeout Handling: Respond quickly (within 10 seconds) to avoid retries
- 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:
- Log in to Africa’s Talking Dashboard
- Navigate to SMS > Callback URLs
- Set Delivery Reports URL:
https://your-domain.com/api/v1/sms/webhook
- Set Incoming Messages URL:
https://your-domain.com/api/v1/sms/webhook
- 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
| Error | Cause | Solution |
|---|
| 401 Unauthorized | Invalid signature | Verify webhook secret configuration |
| 400 Bad Request | Malformed payload | Check payload format and required fields |
| 500 Internal Error | Server error | Check application logs and database connection |
| Timeout | Slow response | Optimize webhook handler (use async processing) |
Additional Resources