Skip to main content

Send SMS with Python

Learn how to send your first SMS message using the AfriRoute Python SDK in just 5 minutes.

What You'll Build​

In this tutorial, you'll learn how to:

  • ✅ Install and configure the AfriRoute Python SDK
  • ✅ Send a single SMS message
  • ✅ Send bulk SMS to multiple recipients
  • ✅ Check delivery status
  • ✅ Handle errors and retry logic
  • ✅ Implement best practices for production

Estimated Time: 15 minutes

Prerequisites​

Before you begin, ensure you have:

  • ✅ AfriRoute Account (Sign up for free)
  • ✅ API Credentials from your dashboard
  • ✅ Python 3.8+ installed
  • ✅ pip package manager
  • ✅ Basic Python knowledge

Step 1: Install the AfriRoute Python SDK​

# Create project directory
mkdir sms-tutorial
cd sms-tutorial

# Create virtual environment
python -m venv venv

# Activate virtual environment
# On Windows:
venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate

# Install AfriRoute SDK
pip install afriroute-python

# Install python-dotenv for environment variables
pip install python-dotenv

Step 2: Set Up Your Environment​

Create a .env file in your project root:

.env
AFRIROUTE_API_KEY=$AFRIROUTE_API_KEY
AFRIROUTE_API_SECRET=your_api_secret_here
Security

Never commit .env to version control! Add it to .gitignore

Step 3: Send Your First SMS​

Create a file named send_sms.py:

send_sms.py
import os
from dotenv import load_dotenv
from afriroute import AfriRoute

# Load environment variables
load_dotenv()

# Initialize AfriRoute client
client = AfriRoute(
api_key=os.getenv('AFRIROUTE_API_KEY'),
api_secret=os.getenv('AFRIROUTE_API_SECRET')
)

def send_simple_sms():
"""Send a single SMS message"""
try:
response = client.sms.send(
to="+254700123456", # Replace with recipient's number
message="Hello from AfriRoute! This is your first SMS.",
sender_id="AFRIROUTE"
)

print(f"✅ SMS sent successfully!")
print(f"Message ID: {response['message_id']}")
print(f"Status: {response['status']}")
print(f"Cost: ${response['cost']}")

return response['message_id']

except Exception as e:
print(f"❌ Error sending SMS: {str(e)}")
return None

if __name__ == "__main__":
message_id = send_simple_sms()

Run Your Code​

python send_sms.py

Expected Output:

✅ SMS sent successfully!
Message ID: msg_abc123xyz
Status: accepted
Cost: $0.03

Step 4: Send Bulk SMS​

Send multiple SMS messages efficiently:

bulk_sms.py
from afriroute import AfriRoute
import os
from dotenv import load_dotenv

load_dotenv()

client = AfriRoute(
api_key=os.getenv('AFRIROUTE_API_KEY'),
api_secret=os.getenv('AFRIROUTE_API_SECRET')
)

def send_bulk_sms(recipients, message):
"""Send SMS to multiple recipients"""
results = []

for recipient in recipients:
try:
response = client.sms.send(
to=recipient['phone'],
message=message.format(name=recipient['name']),
sender_id="AFRIROUTE"
)

results.append({
'phone': recipient['phone'],
'name': recipient['name'],
'message_id': response['message_id'],
'status': 'sent',
'cost': response['cost']
})

print(f"✅ Sent to {recipient['name']} ({recipient['phone']})")

except Exception as e:
results.append({
'phone': recipient['phone'],
'name': recipient['name'],
'status': 'failed',
'error': str(e)
})
print(f"❌ Failed for {recipient['name']}: {str(e)}")

return results

if __name__ == "__main__":
# Your recipients list
recipients = [
{"name": "John Doe", "phone": "+254700123456"},
{"name": "Jane Smith", "phone": "+254711234567"},
{"name": "Bob Johnson", "phone": "+254722345678"},
]

# Personalized message
message = "Hi {name}! Welcome to AfriRoute. Reply STOP to unsubscribe."

results = send_bulk_sms(recipients, message)

# Summary
total = len(results)
sent = sum(1 for r in results if r['status'] == 'sent')
failed = total - sent
total_cost = sum(r.get('cost', 0) for r in results if r['status'] == 'sent')

print(f"\n📊 Summary:")
print(f"Total: {total} | Sent: {sent} | Failed: {failed}")
print(f"Total Cost: ${total_cost:.2f}")

Step 5: Check Delivery Status​

Track whether your messages were delivered:

check_status.py
from afriroute import AfriRoute
import os
import time
from dotenv import load_dotenv

load_dotenv()

client = AfriRoute(
api_key=os.getenv('AFRIROUTE_API_KEY'),
api_secret=os.getenv('AFRIROUTE_API_SECRET')
)

def check_delivery_status(message_id):
"""Check the delivery status of an SMS"""
try:
status = client.sms.get_status(message_id)

print(f"Message ID: {message_id}")
print(f"Status: {status['status']}")
print(f"Delivered: {status['delivered']}")
if status.get('delivered_at'):
print(f"Delivered At: {status['delivered_at']}")
if status.get('error_message'):
print(f"Error: {status['error_message']}")

return status

except Exception as e:
print(f"❌ Error checking status: {str(e)}")
return None

def wait_for_delivery(message_id, timeout=60):
"""Wait for message delivery with timeout"""
print(f"⏳ Waiting for delivery... (timeout: {timeout}s)")

start_time = time.time()

while time.time() - start_time < timeout:
status = check_delivery_status(message_id)

if status and status['delivered']:
print("✅ Message delivered successfully!")
return True

if status and status['status'] == 'failed':
print("❌ Message delivery failed!")
return False

print("⏳ Still pending...")
time.sleep(5) # Check every 5 seconds

print("⏰ Timeout reached")
return False

if __name__ == "__main__":
# Replace with your message ID
message_id = "msg_abc123xyz"

wait_for_delivery(message_id)

Step 6: Handle Errors and Retries​

Implement robust error handling:

error_handling.py
from afriroute import AfriRoute
from afriroute.exceptions import (
InsufficientBalanceError,
InvalidPhoneNumberError,
RateLimitError,
APIError
)
import os
import time
from dotenv import load_dotenv

load_dotenv()

client = AfriRoute(
api_key=os.getenv('AFRIROUTE_API_KEY'),
api_secret=os.getenv('AFRIROUTE_API_SECRET')
)

def send_sms_with_retry(to, message, max_retries=3):
"""Send SMS with automatic retry on failure"""

for attempt in range(max_retries):
try:
response = client.sms.send(
to=to,
message=message,
sender_id="AFRIROUTE"
)

print(f"✅ SMS sent successfully on attempt {attempt + 1}")
return response

except RateLimitError as e:
print(f"⏸️ Rate limit reached. Waiting {e.retry_after} seconds...")
time.sleep(e.retry_after)
continue

except InsufficientBalanceError:
print("❌ Insufficient balance. Please top up your account.")
return None

except InvalidPhoneNumberError:
print("❌ Invalid phone number format.")
return None

except APIError as e:
print(f"⚠️ API Error on attempt {attempt + 1}: {str(e)}")
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
print(f"Retrying in {wait_time} seconds...")
time.sleep(wait_time)
else:
print("❌ Max retries reached")
return None

except Exception as e:
print(f"❌ Unexpected error: {str(e)}")
return None

return None

if __name__ == "__main__":
result = send_sms_with_retry(
to="+254700123456",
message="Hello from AfriRoute with retry logic!"
)

if result:
print(f"Final Message ID: {result['message_id']}")

Best Practices​

1. Phone Number Validation​

import re

def validate_phone_number(phone):
"""Validate phone number format"""
# Remove spaces and special characters
phone = re.sub(r'[^0-9+]', '', phone)

# Check format: +[country code][number]
if not phone.startswith('+'):
return False

# Check length (8-15 digits after country code)
if not 9 <= len(phone) <= 16:
return False

return phone

# Usage
phone = validate_phone_number("+254 700 123 456")
if phone:
client.sms.send(to=phone, message="Valid number!")

2. Rate Limiting​

import time
from collections import deque

class RateLimiter:
def __init__(self, max_requests=100, time_window=60):
self.max_requests = max_requests
self.time_window = time_window
self.requests = deque()

def can_send(self):
now = time.time()
# Remove old requests outside time window
while self.requests and self.requests[0] < now - self.time_window:
self.requests.popleft()

if len(self.requests) < self.max_requests:
self.requests.append(now)
return True

return False

def wait_time(self):
if len(self.requests) < self.max_requests:
return 0
oldest = self.requests[0]
return (oldest + self.time_window) - time.time()

# Usage
rate_limiter = RateLimiter(max_requests=10, time_window=60)

for recipient in recipients:
if not rate_limiter.can_send():
wait_time = rate_limiter.wait_time()
print(f"Rate limit reached. Waiting {wait_time:.1f}s...")
time.sleep(wait_time)

client.sms.send(to=recipient, message="Hello!")

3. Cost Calculation​

def calculate_sms_cost(message, recipient_country="KE"):
"""Estimate SMS cost based on message length and destination"""

# SMS segment size (160 characters for GSM, 70 for Unicode)
segment_size = 70 if any(ord(c) > 127 for c in message) else 160

# Calculate number of segments
segments = (len(message) + segment_size - 1) // segment_size

# Pricing per segment by country
pricing = {
"KE": 0.03, # Kenya
"TZ": 0.04, # Tanzania
"UG": 0.035, # Uganda
"RW": 0.04, # Rwanda
"ET": 0.045, # Ethiopia
}

cost_per_segment = pricing.get(recipient_country, 0.05)
total_cost = segments * cost_per_segment

return {
'segments': segments,
'cost_per_segment': cost_per_segment,
'total_cost': total_cost,
'message_length': len(message)
}

# Usage
message = "Your OTP code is 123456. Valid for 5 minutes."
cost_info = calculate_sms_cost(message)
print(f"Message will cost: ${cost_info['total_cost']:.3f}")
print(f"Segments: {cost_info['segments']}")

4. Logging​

import logging
from datetime import datetime

# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('sms_logs.txt'),
logging.StreamHandler()
]
)

logger = logging.getLogger(__name__)

def send_sms_with_logging(to, message):
logger.info(f"Attempting to send SMS to {to}")

try:
response = client.sms.send(to=to, message=message)

logger.info(f"SMS sent successfully. Message ID: {response['message_id']}")
logger.info(f"Cost: ${response['cost']}, Status: {response['status']}")

return response

except Exception as e:
logger.error(f"Failed to send SMS to {to}: {str(e)}")
return None

Complete Example: Production-Ready SMS Sender​

Here's a complete, production-ready example:

production_sms.py
import os
import time
import re
import logging
from typing import List, Dict, Optional
from dataclasses import dataclass
from dotenv import load_dotenv
from afriroute import AfriRoute
from afriroute.exceptions import *

load_dotenv()

# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

@dataclass
class SMSResult:
phone: str
success: bool
message_id: Optional[str] = None
error: Optional[str] = None
cost: Optional[float] = None

class ProductionSMSSender:
def __init__(self):
self.client = AfriRoute(
api_key=os.getenv('AFRIROUTE_API_KEY'),
api_secret=os.getenv('AFRIROUTE_API_SECRET')
)
self.max_retries = 3
self.rate_limit = 10 # messages per minute
self.last_sent_times = []

def validate_phone(self, phone: str) -> Optional[str]:
"""Validate and format phone number"""
phone = re.sub(r'[^0-9+]', '', phone)

if not phone.startswith('+') or not (9 <= len(phone) <= 16):
return None

return phone

def check_rate_limit(self):
"""Ensure we don't exceed rate limits"""
now = time.time()
# Remove entries older than 1 minute
self.last_sent_times = [t for t in self.last_sent_times if now - t < 60]

if len(self.last_sent_times) >= self.rate_limit:
wait_time = 60 - (now - self.last_sent_times[0])
if wait_time > 0:
logger.info(f"Rate limit reached. Waiting {wait_time:.1f}s")
time.sleep(wait_time)
self.last_sent_times = []

self.last_sent_times.append(now)

def send_single(self, to: str, message: str) -> SMSResult:
"""Send single SMS with error handling and retries"""

# Validate phone number
phone = self.validate_phone(to)
if not phone:
logger.error(f"Invalid phone number: {to}")
return SMSResult(phone=to, success=False, error="Invalid phone number")

# Check rate limit
self.check_rate_limit()

# Attempt to send with retries
for attempt in range(self.max_retries):
try:
response = self.client.sms.send(
to=phone,
message=message,
sender_id="AFRIROUTE"
)

logger.info(f"✅ SMS sent to {phone}: {response['message_id']}")

return SMSResult(
phone=phone,
success=True,
message_id=response['message_id'],
cost=response.get('cost', 0)
)

except RateLimitError as e:
logger.warning(f"Rate limit hit. Waiting {e.retry_after}s")
time.sleep(e.retry_after)
continue

except Exception as e:
logger.error(f"Attempt {attempt + 1} failed: {str(e)}")
if attempt < self.max_retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
else:
return SMSResult(phone=phone, success=False, error=str(e))

return SMSResult(phone=phone, success=False, error="Max retries exceeded")

def send_bulk(self, recipients: List[Dict[str, str]], message_template: str) -> Dict:
"""Send bulk SMS with comprehensive reporting"""

results = []

for recipient in recipients:
phone = recipient['phone']
message = message_template.format(**recipient)

result = self.send_single(phone, message)
results.append(result)

# Generate summary
total = len(results)
successful = sum(1 for r in results if r.success)
failed = total - successful
total_cost = sum(r.cost or 0 for r in results if r.success)

summary = {
'total': total,
'successful': successful,
'failed': failed,
'total_cost': total_cost,
'results': results
}

logger.info(f"📊 Bulk SMS Summary: {successful}/{total} sent, Cost: ${total_cost:.2f}")

return summary

if __name__ == "__main__":
sender = ProductionSMSSender()

# Example: Send to multiple recipients
recipients = [
{"phone": "+254700123456", "name": "John"},
{"phone": "+254711234567", "name": "Jane"},
]

message = "Hi {name}! Welcome to AfriRoute SMS service."

summary = sender.send_bulk(recipients, message)
print(f"\nSent: {summary['successful']} | Failed: {summary['failed']}")
print(f"Total Cost: ${summary['total_cost']:.2f}")

Troubleshooting​

Common Issues​

1. "Invalid API credentials" error

# Check your .env file
print(f"API Key: {os.getenv('AFRIROUTE_API_KEY')}")
# Make sure you called load_dotenv()

2. "Invalid phone number" error

# Phone must include country code with +
phone = "+254700123456" # ✅ Correct
phone = "0700123456" # ❌ Wrong

3. "Insufficient balance" error

# Check your balance
balance = client.account.get_balance()
print(f"Current balance: ${balance['amount']}")

4. Message not delivered

# Check message status
status = client.sms.get_status(message_id)
if status['status'] == 'failed':
print(f"Failure reason: {status['error_message']}")

Next Steps​

Now that you can send SMS, explore:

Resources​

Pro Tip

Use environment-specific configs for development and production to avoid accidental SMS sends during testing!