Send Bulk SMS with Python and Flask
Overview
Build a production-ready Flask application that sends SMS messages to multiple recipients using the Telnyx Python SDK. This tutorial demonstrates batch processing with rate limiting, progress tracking, and comprehensive error handling for telecom APIs at scale.
Prerequisites
- Python 3.8 or higher.
- A Telnyx account with an active API key from the Telnyx Portal.
- A Telnyx phone number enabled for outbound SMS.
- pip (Python package manager).
Step 1: Setup
Install the required dependencies:
pip install telnyx flask python-dotenv
Create a project directory and navigate into it:
mkdir telnyx-bulk-sms
cd telnyx-bulk-sms
Step 2: Configuration
Create a .env file in your project root to store credentials securely:
TELNYX_API_KEY=YOUR_API_KEY_HERE
TELNYX_PHONE_NUMBER=+15551234567
Replace YOUR_API_KEY_HERE with your actual API key and +15551234567 with your Telnyx phone number in E.164 format.
Step 3: Implementation
Create app.py and implement the bulk SMS functionality with rate limiting and progress tracking:
import os
import time
from typing import List, Dict, Any
import telnyx
from dotenv import load_dotenv
from flask import Flask, jsonify, request
load_dotenv()
app = Flask(__name__)
# Initialize client with the new SDK pattern
client = telnyx.Telnyx(api_key=os.getenv("TELNYX_API_KEY"))
def send_bulk_sms(recipients: List[Dict[str, str]], message: str) -> Dict[str, Any]:
"""Send SMS to multiple recipients with rate limiting and progress tracking."""
from_number = os.getenv("TELNYX_PHONE_NUMBER")
if not from_number:
raise ValueError("TELNYX_PHONE_NUMBER environment variable not set")
results = {
"total": len(recipients),
"successful": 0,
"failed": 0,
"messages": []
}
for i, recipient in enumerate(recipients):
to_number = recipient.get("phone")
recipient_name = recipient.get("name", "Unknown")
# Validate E.164 format
if not to_number or not to_number.startswith("+"):
results["failed"] += 1
results["messages"].append({
"recipient": recipient_name,
"phone": to_number,
"status": "failed",
"error": "Invalid phone number format"
})
continue
try:
# Send individual message
response = client.messages.create(
from_=from_number,
to=to_number,
text=message,
)
results["successful"] += 1
results["messages"].append({
"recipient": recipient_name,
"phone": to_number,
"message_id": response.data.id,
"status": response.data.to[0].status if response.data.to else "queued"
})
except Exception as e:
results["failed"] += 1
results["messages"].append({
"recipient": recipient_name,
"phone": to_number,
"status": "failed",
"error": str(e)
})
# Rate limiting: 10 messages per second maximum
if i < len(recipients) - 1: # Don't sleep after the last message
time.sleep(0.1)
return results
@app.route("/sms/bulk", methods=["POST"])
def send_bulk_sms_endpoint():
"""HTTP endpoint to send bulk SMS messages."""
data = request.get_json()
if not data:
return jsonify({"error": "Request body required"}), 400
recipients = data.get("recipients", [])
message = data.get("message")
if not recipients or not message:
return jsonify({"error": "Missing required fields: 'recipients' and 'message'"}), 400
if not isinstance(recipients, list):
return jsonify({"error": "'recipients' must be an array"}), 400
if len(recipients) > 100:
return jsonify({"error": "Maximum 100 recipients per batch"}), 400
try:
result = send_bulk_sms(recipients, message)
return jsonify(result), 200
except telnyx.AuthenticationError:
return jsonify({"error": "Invalid API key"}), 401
except telnyx.RateLimitError:
return jsonify({"error": "Rate limit exceeded. Please slow down."}), 429
except telnyx.APIStatusError as e:
return jsonify({"error": str(e), "status_code": e.status_code}), e.status_code
except telnyx.APIConnectionError:
return jsonify({"error": "Network error connecting to Telnyx"}), 503
except ValueError as e:
return jsonify({"error": str(e)}), 400
Step 4: Testing
Add a health check endpoint and start the server:
@app.route("/health", methods=["GET"])
def health_check():
"""Health check endpoint."""
return jsonify({"status": "healthy", "service": "bulk-sms"}), 200
if __name__ == "__main__":
app.run(debug=True, port=5000)
Start the server:
python app.py
Test the bulk SMS endpoint using curl:
curl -X POST http://localhost:5000/sms/bulk \
-H "Content-Type: application/json" \
-d '{
"message": "Hello from Telnyx bulk SMS!",
"recipients": [
{"name": "Alice", "phone": "+15559876543"},
{"name": "Bob", "phone": "+15559876544"},
{"name": "Charlie", "phone": "+15559876545"}
]
}'
Expected response:
{
"total": 3,
"successful": 3,
"failed": 0,
"messages": [
{
"recipient": "Alice",
"phone": "+15559876543",
"message_id": "403171d8-8b00-4b8f-9e9c-1234567890ab",
"status": "queued"
},
{
"recipient": "Bob",
"phone": "+15559876544",
"message_id": "503171d8-8b00-4b8f-9e9c-1234567890ac",
"status": "queued"
},
{
"recipient": "Charlie",
"phone": "+15559876545",
"message_id": "603171d8-8b00-4b8f-9e9c-1234567890ad",
"status": "queued"
}
]
}
Complete Code
#!/usr/bin/env python3
"""Production-ready Flask application for sending bulk SMS via Telnyx."""
import os
import time
from typing import List, Dict, Any
import telnyx
from dotenv import load_dotenv
from flask import Flask, jsonify, request
load_dotenv()
app = Flask(__name__)
# Initialize client with the new SDK pattern
client = telnyx.Telnyx(api_key=os.getenv("TELNYX_API_KEY"))
def send_bulk_sms(recipients: List[Dict[str, str]], message: str) -> Dict[str, Any]:
"""Send SMS to multiple recipients with rate limiting and progress tracking."""
from_number = os.getenv("TELNYX_PHONE_NUMBER")
if not from_number:
raise ValueError("TELNYX_PHONE_NUMBER environment variable not set")
results = {
"total": len(recipients),
"successful": 0,
"failed": 0,
"messages": []
}
for i, recipient in enumerate(recipients):
to_number = recipient.get("phone")
recipient_name = recipient.get("name", "Unknown")
# Validate E.164 format
if not to_number or not to_number.startswith("+"):
results["failed"] += 1
results["messages"].append({
"recipient": recipient_name,
"phone": to_number,
"status": "failed",
"error": "Invalid phone number format"
})
continue
try:
# Send individual message
response = client.messages.create(
from_=from_number,
to=to_number,
text=message,
)
results["successful"] += 1
results["messages"].append({
"recipient": recipient_name,
"phone": to_number,
"message_id": response.data.id,
"status": response.data.to[0].status if response.data.to else "queued"
})
except Exception as e:
results["failed"] += 1
results["messages"].append({
"recipient": recipient_name,
"phone": to_number,
"status": "failed",
"error": str(e)
})
# Rate limiting: 10 messages per second maximum
if i < len(recipients) - 1: # Don't sleep after the last message
time.sleep(0.1)
return results
@app.route("/sms/bulk", methods=["POST"])
def send_bulk_sms_endpoint():
"""HTTP endpoint to send bulk SMS messages."""
data = request.get_json()
if not data:
return jsonify({"error": "Request body required"}), 400
recipients = data.get("recipients", [])
message = data.get("message")
if not recipients or not message:
return jsonify({"error": "Missing required fields: 'recipients' and 'message'"}), 400
if not isinstance(recipients, list):
return jsonify({"error": "'recipients' must be an array"}), 400
if len(recipients) > 100:
return jsonify({"error": "Maximum 100 recipients per batch"}), 400
try:
result = send_bulk_sms(recipients, message)
return jsonify(result), 200
except telnyx.AuthenticationError:
return jsonify({"error": "Invalid API key"}), 401
except telnyx.RateLimitError:
return jsonify({"error": "Rate limit exceeded. Please slow down."}), 429
except telnyx.APIStatusError as e:
return jsonify({"error": str(e), "status_code": e.status_code}), e.status_code
except telnyx.APIConnectionError:
return jsonify({"error": "Network error connecting to Telnyx"}), 503
except ValueError as e:
return jsonify({"error": str(e)}), 400
@app.route("/health", methods=["GET"])
def health_check():
"""Health check endpoint."""
return jsonify({"status": "healthy", "service": "bulk-sms"}), 200
if __name__ == "__main__":
app.run(debug=True, port=5000)
Troubleshooting
| Issue | Problem | Solution |
|---|---|---|
| Rate Limit Exceeded (429) | The endpoint returns {"error": "Rate limit exceeded. Please slow down."} with HTTP 429 during bulk sending. |
The built-in rate limiting (0.1 second delay) may not be sufficient for your account limits. Increase the time.sleep() value to 0.2 or 0.5 seconds. Check your Telnyx account's rate limits in the portal and adjust accordingly. Consider implementing exponential backoff for production use. |
| Partial Batch Failures | Some messages succeed while others fail with various error messages in the response. | Review the individual error messages in the messages array. Common issues include invalid phone number formats (ensure E.164), insufficient account balance, or carrier restrictions. Validate all phone numbers before sending and handle specific error types differently. |
| Memory Issues with Large Batches | The application becomes slow or unresponsive when processing large recipient lists. | The current implementation processes all messages synchronously. For batches larger than 100 recipients, implement background job processing using Celery or similar. Consider chunking large lists into smaller batches and processing them separately to avoid memory issues and improve user experience. |