Skip to main content

Overview

The SMS utilities module provides functions to send SMS messages using Africa’s Talking API. It supports both two-way SMS (with shortcode) and bulk SMS sending.

Environment Variables

AT_USERNAME
string
required
Your Africa’s Talking username (e.g., sandbox or your production username)
AT_API_KEY
string
required
Your Africa’s Talking API key
AT_SHORTCODE
string
Your shortcode for two-way SMS (optional, only needed for 2-way messaging)

send_twoway_sms

Send a two-way SMS using a shortcode. Two-way SMS allows recipients to reply, and those replies are sent to your callback URL.
def send_twoway_sms(
    message: str, 
    recipient: str, 
    shortcode: Optional[str] = AT_SHORTCODE
) -> Dict[str, Any]

Parameters

message
string
required
The SMS message to send. Cannot be empty.
recipient
string
required
The recipient’s phone number in international format (e.g., +254712345678)
shortcode
string
The shortcode to use for sending. Defaults to AT_SHORTCODE from environment variables.

Returns

response
Dict[str, Any]
Africa’s Talking API response containing message status and details
{
    "SMSMessageData": {
        "Message": "Sent to 1/1 Total Cost: KES 0.8000",
        "Recipients": [{
            "statusCode": 101,
            "number": "+254712345678",
            "status": "Success",
            "cost": "KES 0.8000",
            "messageId": "ATXid_..."
        }]
    }
}

Usage Example

from utils.sms_utils import send_twoway_sms

# Send a two-way SMS
response = send_twoway_sms(
    message="Hello! Reply to this message.",
    recipient="+254712345678"
)

print(f"Message sent: {response}")
# ✅ SMS sent to +254712345678: Hello! Reply to this message.

# Use custom shortcode
response = send_twoway_sms(
    message="Custom shortcode message",
    recipient="+254712345678",
    shortcode="12345"
)

Error Handling

try:
    response = send_twoway_sms("Test message", "+254712345678")
except ValueError as e:
    print(f"Validation error: {e}")
    # Possible errors:
    # - Invalid recipient format: must start with '+'
    # - Message cannot be empty
except Exception as e:
    print(f"SMS sending failed: {e}")
    # Network errors, API errors, etc.

Route Handler Integration

Example from routes/sms.py:34-52:
@sms_bp.route("/invoke-twoway-sms", methods=["GET"])
def invoke_twoway_sms():
    phone = "+" + request.args.get("phone", "").strip()
    message = request.args.get("message", "Hello from Africa's Talking!").strip()

    if not phone:
        return {"error": "Missing 'phone' query parameter"}, 400

    try:
        response = send_twoway_sms(message, phone)
        return {"message": f"SMS sent to {phone}", "response": response}
    except Exception as e:
        return {"error": str(e)}, 500

Two-Way SMS Callback

Handle replies from recipients (from routes/sms.py:55-77):
@sms_bp.route("/twoway", methods=["POST"])
def twoway_callback():
    linkId = request.values.get("linkId")
    text = request.values.get("text")
    sender = request.values.get("from")
    
    print(f"Received 2-way SMS from {sender}: {text}")
    
    # Respond to the sender
    send_twoway_sms(
        message=f'This is a response to: "{text}"',
        recipient=sender
    )
    
    return "GOOD", 200

send_bulk_sms

Send an SMS to multiple recipients simultaneously. Useful for notifications, marketing, or alerts.
def send_bulk_sms(message: str, recipients: List[str]) -> Dict[str, Any]

Parameters

message
string
required
The SMS message to send to all recipients
recipients
List[string]
required
List of phone numbers in international format (e.g., ["+254712345678", "+254723456789"])

Returns

response
Dict[str, Any]
Africa’s Talking API response containing delivery status for each recipient
{
    "SMSMessageData": {
        "Message": "Sent to 2/2 Total Cost: KES 1.6000",
        "Recipients": [
            {
                "statusCode": 101,
                "number": "+254712345678",
                "status": "Success",
                "cost": "KES 0.8000",
                "messageId": "ATXid_..."
            },
            {
                "statusCode": 101,
                "number": "+254723456789",
                "status": "Success",
                "cost": "KES 0.8000",
                "messageId": "ATXid_..."
            }
        ]
    }
}

Usage Example

from utils.sms_utils import send_bulk_sms

# Send to multiple recipients
recipients = [
    "+254712345678",
    "+254723456789",
    "+254734567890"
]

response = send_bulk_sms(
    message="Important announcement: System maintenance at 2 AM.",
    recipients=recipients
)

print(response)
# 📩 Bulk SMS sent to 3 recipients

# Check individual delivery status
for recipient in response["SMSMessageData"]["Recipients"]:
    print(f"{recipient['number']}: {recipient['status']}")

Route Handler Integration

Example from routes/sms.py:13-31:
@sms_bp.route("/invoke-bulk-sms", methods=["GET"])
def invoke_bulk_sms():
    phone = "+" + request.args.get("phone", "").strip()
    message = request.args.get("message", "Hello from Africa's Talking!").strip()

    if not phone:
        return {"error": "Missing 'phone' query parameter"}, 400

    try:
        # Send to single recipient using bulk SMS
        response = send_bulk_sms(message, [phone])
        return {"message": f"SMS sent to {phone}", "response": response}
    except Exception as e:
        return {"error": str(e)}, 500

Error Handling

try:
    response = send_bulk_sms("Notification", recipients)
except ValueError as e:
    print(f"Validation error: {e}")
    # Error: Recipients list cannot be empty
except Exception as e:
    print(f"Bulk SMS failed: {e}")
    # Network errors, API errors, invalid API keys, etc.

Phone Number Format Validation

Both functions validate phone numbers to ensure they’re in international format:

Valid Formats

# ✅ Valid formats
"+254712345678"  # Kenya
"+234803456789"  # Nigeria
"+233240123456"  # Ghana
"+256701234567"  # Uganda

Invalid Formats

# ❌ Invalid formats (will raise ValueError)
"0712345678"     # Missing country code
"254712345678"   # Missing '+' prefix
"712345678"      # Missing country code and '+'
""

Validation Example

def validate_and_send(message: str, phone: str):
    # Ensure phone starts with '+'
    if not phone.startswith("+"):
        phone = "+" + phone
    
    try:
        return send_twoway_sms(message, phone)
    except ValueError as e:
        print(f"Invalid phone format: {e}")
        return None

Delivery Reports

Monitor SMS delivery status through webhooks (from routes/sms.py:80-100):
@sms_bp.route("/delivery-reports", methods=["POST"])
def sms_delivery_report():
    """
    Handle SMS delivery reports from Africa's Talking.
    
    Payload example:
    {
        "id": "ATXid_...",
        "status": "Success",
        "phoneNumber": "+254712345678",
        "networkCode": "63902",
        "failureReason": "",
        "retryCount": "0"
    }
    """
    payload = {key: request.values.get(key) for key in request.values.keys()}
    
    print("📩 SMS Delivery Report Received:")
    for key, value in payload.items():
        print(f"   {key}: {value}")
    
    # Process delivery status
    if payload.get("status") != "Success":
        print(f"⚠️ Delivery failed: {payload.get('failureReason')}")
    
    return "OK", 200

Status Codes

Common Africa’s Talking SMS status codes:
CodeStatusDescription
101SuccessMessage sent successfully
102QueuedMessage queued for delivery
401RiskHoldMessage flagged for review
402InvalidSenderIdInvalid sender ID
403InvalidPhoneNumberInvalid recipient number
404UnsupportedNumberTypeNumber type not supported
405InsufficientBalanceInsufficient account balance
406UserInBlackListRecipient opted out
407CouldNotRouteUnable to route message
500InternalServerErrorAfrica’s Talking server error

Handling Status Codes

response = send_bulk_sms("Test", ["+254712345678"])

for recipient in response["SMSMessageData"]["Recipients"]:
    status_code = recipient["statusCode"]
    
    if status_code == 101:
        print(f"✅ Success: {recipient['number']}")
    elif status_code == 102:
        print(f"⏳ Queued: {recipient['number']}")
    elif status_code == 405:
        print(f"❌ Insufficient balance")
    else:
        print(f"⚠️ Error {status_code}: {recipient['status']}")

Best Practices

1. Use Bulk SMS for Multiple Recipients

# ❌ Don't do this
for phone in phones:
    send_twoway_sms(message, phone)  # Multiple API calls

# ✅ Do this instead
send_bulk_sms(message, phones)  # Single API call

2. Implement Error Retry Logic

import time

def send_sms_with_retry(message: str, recipients: list, retries: int = 3):
    for attempt in range(retries):
        try:
            return send_bulk_sms(message, recipients)
        except Exception as e:
            if attempt < retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
                continue
            raise

3. Validate Input Before Sending

def safe_send_sms(message: str, recipients: list):
    # Validate message
    if not message or not message.strip():
        raise ValueError("Message cannot be empty")
    
    # Validate and format phone numbers
    valid_recipients = []
    for phone in recipients:
        if not phone.startswith("+"):
            phone = "+" + phone
        if len(phone) >= 10:  # Minimum valid length
            valid_recipients.append(phone)
    
    if not valid_recipients:
        raise ValueError("No valid recipients")
    
    return send_bulk_sms(message, valid_recipients)

4. Monitor Delivery Reports

Always implement delivery report webhooks to track message status:
# Set webhook URL in Africa's Talking dashboard:
# https://your-domain.com/sms/delivery-reports

5. Handle Opt-Outs

Implement opt-out handling (from routes/sms.py:103-119):
@sms_bp.route("/opt-out", methods=["POST"])
def sms_opt_out():
    sender_id = request.values.get("senderId")
    phone_number = request.values.get("phoneNumber")
    
    # Add to opt-out list in your database
    print(f"🚫 User {phone_number} opted out from {sender_id}")
    
    return "OK", 200

Build docs developers (and LLMs) love