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:
AFRIROUTE_API_KEY=$AFRIROUTE_API_KEY
AFRIROUTE_API_SECRET=your_api_secret_here
Never commit .env to version control! Add it to .gitignore
Step 3: Send Your First SMS
Create a file named 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:
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:
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:
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:
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
Use environment-specific configs for development and production to avoid accidental SMS sends during testing!