Bulk SMS messaging enables businesses to communicate effectively with customers through promotional campaigns, appointment reminders, and important updates. This guide provides a complete solution for automating bulk SMS using Python, Twilio’s API, and CSV files containing phone numbers.
Benefits of This Approach
Using Python with Twilio offers:
- Full Automation: Process thousands of phone numbers efficiently
- Error Management: Track delivery failures and implement retries
- Message Personalization: Customize messages for each recipient
- Comprehensive Logging: Maintain records of all communications
Prerequisites
Before starting, ensure you have:
- A Twilio account (sign up at twilio.com/try-twilio)
- Python 3.7 or higher installed
- A Twilio phone number with SMS capabilities
- A CSV file containing phone numbers
Step-by-Step Implementation
Step 1: Install Required Packages
Open your terminal or command prompt and run:
pip install twilio pandas
Step 2: Prepare Your CSV File
Create a file named contacts.csv with the following structure:
name,phone_number,country_code John Doe,+12345678901,US Jane Smith,+441234567890,UK Bob Wilson,+61412345678,AU
Important Note: Phone numbers must use E.164 format (e.g., +12345678901).
Step 3: Create the Python Script
Create a file named bulk_sms_sender.py and add the following code:
import pandas as pd
from twilio.rest import Client
import time
import logging
from datetime import datetime
import os
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(f'sms_log_{datetime.now().strftime("%Y%m%d_%H%M%S")}.txt'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class BulkSMSSender:
def __init__(self, account_sid, auth_token, twilio_number):
"""
Initialize the SMS sender with Twilio credentials
Args:
account_sid: Your Twilio Account SID
auth_token: Your Twilio Auth Token
twilio_number: Your Twilio phone number in E.164 format
"""
self.client = Client(account_sid, auth_token)
self.twilio_number = twilio_number
logger.info("Twilio client initialized successfully")
def validate_phone_number(self, phone_number):
"""
Basic phone number validation
Args:
phone_number: Phone number to validate
Returns:
bool: True if valid, False otherwise
"""
if not phone_number:
return False
# Check for E.164 format: + followed by 10-15 digits
if not phone_number.startswith('+'):
logger.warning(f"Phone number {phone_number} missing '+' prefix")
return False
# Remove + and check if all remaining are digits
digits = phone_number[1:]
if not digits.isdigit() or len(digits) < 10 or len(digits) > 15:
logger.warning(f"Phone number {phone_number} has invalid format")
return False
return True
def send_single_sms(self, to_number, message_body, max_retries=3):
"""
Send a single SMS with retry logic
Args:
to_number: Recipient phone number
message_body: SMS content
max_retries: Number of retry attempts on failure
Returns:
dict: Result containing status and message details
"""
# Validate phone number
if not self.validate_phone_number(to_number):
return {
'to': to_number,
'status': 'failed',
'error': 'Invalid phone number format',
'message_sid': None
}
for attempt in range(max_retries):
try:
# Send the SMS
message = self.client.messages.create(
body=message_body,
from_=self.twilio_number,
to=to_number
)
logger.info(f"SMS sent to {to_number} | SID: {message.sid}")
return {
'to': to_number,
'status': 'sent',
'message_sid': message.sid,
'error': None,
'price': message.price,
'status_update': message.status
}
except Exception as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
logger.warning(f"Attempt {attempt + 1} failed for {to_number}. "
f"Retrying in {wait_time} seconds. Error: {str(e)}")
time.sleep(wait_time)
else:
logger.error(f"Failed to send SMS to {to_number} after {max_retries} attempts. Error: {str(e)}")
return {
'to': to_number,
'status': 'failed',
'error': str(e),
'message_sid': None
}
def send_bulk_sms(self, csv_file_path, message_template, delay=1,
personalized_columns=None, batch_size=None):
"""
Send bulk SMS from a CSV file
Args:
csv_file_path: Path to the CSV file
message_template: SMS message template (use {column_name} for variables)
delay: Seconds to wait between sends (avoid rate limits)
personalized_columns: List of column names to use for personalization
batch_size: Number of SMS to send in a batch (None for all at once)
Returns:
pd.DataFrame: Results with status for each number
"""
try:
# Read CSV file
df = pd.read_csv(csv_file_path)
logger.info(f"Loaded {len(df)} contacts from {csv_file_path}")
# Validate required columns
if 'phone_number' not in df.columns:
raise ValueError("CSV must contain 'phone_number' column")
results = []
sent_count = 0
failed_count = 0
# Process each contact
for index, row in df.iterrows():
# Prepare personalized message
if personalized_columns:
# Replace placeholders in template with actual values
message = message_template
for col in personalized_columns:
if col in row:
placeholder = f"{{{col}}}"
if placeholder in message:
message = message.replace(placeholder, str(row[col]))
else:
message = message_template
# Send SMS
result = self.send_single_sms(row['phone_number'], message)
result['contact_name'] = row.get('name', 'Unknown')
results.append(result)
# Update counters
if result['status'] == 'sent':
sent_count += 1
else:
failed_count += 1
# Delay between messages to avoid rate limiting
if delay > 0 and index < len(df) - 1:
time.sleep(delay)
# Optional: Process in batches
if batch_size and (index + 1) % batch_size == 0:
logger.info(f"Processed {index + 1}/{len(df)} contacts...")
# Create results DataFrame
results_df = pd.DataFrame(results)
# Save results to CSV
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
results_file = f"sms_results_{timestamp}.csv"
results_df.to_csv(results_file, index=False)
logger.info(f"Bulk SMS completed")
logger.info(f" Total contacts: {len(df)}")
logger.info(f" Successfully sent: {sent_count}")
logger.info(f" Failed: {failed_count}")
logger.info(f" Results saved to: {results_file}")
return results_df
except FileNotFoundError:
logger.error(f"CSV file not found: {csv_file_path}")
raise
except Exception as e:
logger.error(f"Error processing CSV: {str(e)}")
raise
def main():
"""
Main execution function
"""
# Configuration - REPLACE THESE WITH YOUR VALUES
ACCOUNT_SID = 'YOUR_TWILIO_ACCOUNT_SID'
AUTH_TOKEN = 'YOUR_TWILIO_AUTH_TOKEN'
TWILIO_NUMBER = '+12345678901' # Your Twilio phone number
# File paths
CSV_FILE = 'contacts.csv'
# Message configuration
MESSAGE_TEMPLATE = """Hello {name},
This is a reminder about your appointment tomorrow at 3:00 PM.
Please arrive 15 minutes early.
Best regards,
Your Company Team"""
# Create sender instance
sender = BulkSMSSender(ACCOUNT_SID, AUTH_TOKEN, TWILIO_NUMBER)
# Send bulk SMS with personalization
results = sender.send_bulk_sms(
csv_file_path=CSV_FILE,
message_template=MESSAGE_TEMPLATE,
delay=0.5, # 0.5 seconds between messages
personalized_columns=['name'], # Columns to use for personalization
batch_size=100 # Log progress every 100 messages
)
# Print summary
print("\n" + "="*50)
print("SEND SUMMARY")
print("="*50)
print(f"Total Contacts: {len(results)}")
print(f"Successfully Sent: {len(results[results['status'] == 'sent'])}")
print(f"Failed: {len(results[results['status'] == 'failed'])}")
if len(results[results['status'] == 'failed']) > 0:
print("\nFailed Numbers:")
failed = results[results['status'] == 'failed']
for _, row in failed.iterrows():
print(f" {row['to']}: {row['error']}")
if __name__ == "__main__":
main()
Step 4: Configure the Script with Your Twilio Credentials
To configure the script:
- Log into your Twilio Console at console.twilio.com
- Find your Account SID and Auth Token on the dashboard
- Note your Twilio phone number from the “Phone Numbers” section
Update these lines in the script:
# Configuration - REPLACE THESE WITH YOUR VALUES ACCOUNT_SID = 'YOUR_TWILIO_ACCOUNT_SID' # Replace with your actual Account SID AUTH_TOKEN = 'YOUR_TWILIO_AUTH_TOKEN' # Replace with your actual Auth Token TWILIO_NUMBER = '+12345678901' # Replace with your Twilio phone number
Step 5: Create a Test CSV File
Create a file named test_contacts.csv for initial testing:
name,phone_number Test User,+15005550006 # Twilio test number Test User 2,+15005550007 # Twilio test number
Note: +15005550006 and +15005550007 are Twilio’s test numbers that always succeed. Use these for testing before sending to real numbers.
Step 6: Run the Script
Execute the script with this command:
python bulk_sms_sender.py
Step 7: Monitor the Output
The script will display progress in the terminal. When complete, you’ll see:
2024-01-15 10:30:00,123 - INFO - Loaded 2 contacts from contacts.csv 2024-01-15 10:30:00,456 - INFO - Twilio client initialized successfully 2024-01-15 10:30:00,789 - INFO - SMS sent to +15005550006 | SID: SM1234567890abcdef 2024-01-15 10:30:01,234 - INFO - SMS sent to +15005550007 | SID: SMabcdef1234567890 2024-01-15 10:30:01,567 - INFO - Bulk SMS completed 2024-01-15 10:30:01,567 - INFO - Total contacts: 2 2024-01-15 10:30:01,567 - INFO - Successfully sent: 2 2024-01-15 10:30:01,567 - INFO - Failed: 0 2024-01-15 10:30:01,567 - INFO - Results saved to: sms_results_20240115_103001.csv ================================================== SEND SUMMARY ================================================== Total Contacts: 2 Successfully Sent: 2 Failed: 0
Step 8: Review Results
The script creates two output files:
- Log file:
sms_log_YYYYMMDD_HHMMSS.txt– Contains detailed execution logs - Results file:
sms_results_YYYYMMDD_HHMMSS.csv– Contains delivery status for each number
Running with Your Actual Contact List
Once testing is successful:
- Replace the test CSV file with your actual
contacts.csv - Update the message template in the script if needed
- Run the script again:
python bulk_sms_sender.py
Command Line Version (Optional)
For more flexibility, create a command-line version named send_sms_cli.py:
import argparse
def main():
parser = argparse.ArgumentParser(description='Send bulk SMS using Twilio')
parser.add_argument('--csv', required=True, help='Path to CSV file')
parser.add_argument('--message', required=True, help='SMS message template')
parser.add_argument('--delay', type=float, default=0.5, help='Delay between messages')
parser.add_argument('--account-sid', required=True, help='Twilio Account SID')
parser.add_argument('--auth-token', required=True, help='Twilio Auth Token')
parser.add_argument('--from-number', required=True, help='Twilio phone number')
args = parser.parse_args()
sender = BulkSMSSender(args.account_sid, args.auth_token, args.from_number)
results = sender.send_bulk_sms(
csv_file_path=args.csv,
message_template=args.message,
delay=args.delay
)
print(f"Processed {len(results)} contacts")
if __name__ == "__main__":
main()
Run it with:
python send_sms_cli.py --csv contacts.csv --message "Your message here" --account-sid YOUR_SID --auth-token YOUR_TOKEN --from-number +12345678901
Troubleshooting Guide
Common Issues and Solutions
- “Invalid phone number format” error
- Ensure numbers use E.164 format: +[country code][number]
- Example: +12345678901 for US numbers
- “Authentication failed” error
- Verify your Account SID and Auth Token
- Check for extra spaces in credentials
- “Not SMS capable” error
- Confirm your Twilio number has SMS capability
- Purchase a new number if needed
- “Rate limit exceeded” error
- Increase the delay parameter (try 1.0 second)
- Implement batch processing with longer pauses
- CSV file not found
- Verify the file path
- Use absolute path if needed:
/full/path/to/contacts.csv
Testing with Twilio Test Numbers
For initial testing without charges, use these test numbers:
- +15005550006 (always succeeds)
- +15005550007 (always succeeds)
- +15005550008 (always fails to test error handling)
Best Practices for Production Use
- Always Obtain Consent: Only message individuals who have opted in
- Include Opt-Out Instructions: Add “Reply STOP to unsubscribe” to messages
- Test Thoroughly: Start with small batches before full deployment
- Monitor Costs: Twilio charges per message sent
- Maintain Records: Keep logs of all communications for compliance
- Respect Time Zones: Avoid sending messages during inappropriate hours
- Validate Content: Ensure messages comply with carrier regulations
Performance Optimization Tips
- Adjust Delay Settings:
- Start with 0.5 seconds between messages
- Reduce to 0.1 seconds for high-volume sending
- Increase if you encounter rate limits
- Use Batch Processing:
# Process in batches of 500 with 5-second pauses
results = sender.send_bulk_sms(
csv_file_path=CSV_FILE,
message_template=MESSAGE_TEMPLATE,
delay=0.1,
batch_size=500
)
- Parallel Processing (for advanced users):
- Use Python’s threading or multiprocessing
- Implement careful rate limiting across threads
- Monitor Twilio account usage
Security Considerations
- Never Hardcode Credentials:
- Use environment variables
- Create a separate configuration file
- Never commit credentials to version control
- Input Validation:
- Validate all phone numbers before sending
- Sanitize message content
- Implement proper error handling
- Access Control:
- Restrict who can run the script
- Implement audit logging
- Secure the CSV files containing phone numbers
Conclusion
This solution provides a robust, scalable method for sending bulk SMS using Python and Twilio. By following the step-by-step instructions, you can implement a reliable system that handles personalization, error management, and comprehensive logging.
Remember to start with small test batches, verify delivery rates, and gradually scale your operations. Always prioritize obtaining proper consent and following telecommunications regulations in your region.
The complete code provided offers a production-ready foundation that you can extend with additional features like scheduling, web interfaces, or advanced analytics as your needs evolve.

























Leave a Comment
Your email address will not be published. Required fields are marked with *