How to Send Bulk SMS Using Twilio with Python: A Complete Guide

How to Send Bulk SMS Using Twilio with Python: A Complete Guide

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:

  1. A Twilio account (sign up at twilio.com/try-twilio)
  2. Python 3.7 or higher installed
  3. A Twilio phone number with SMS capabilities
  4. 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:

  1. Log into your Twilio Console at console.twilio.com
  2. Find your Account SID and Auth Token on the dashboard
  3. 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:

  1. Log file: sms_log_YYYYMMDD_HHMMSS.txt – Contains detailed execution logs
  2. Results file: sms_results_YYYYMMDD_HHMMSS.csv – Contains delivery status for each number

Running with Your Actual Contact List

Once testing is successful:

  1. Replace the test CSV file with your actual contacts.csv
  2. Update the message template in the script if needed
  3. 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

  1. “Invalid phone number format” error
  • Ensure numbers use E.164 format: +[country code][number]
  • Example: +12345678901 for US numbers
  1. “Authentication failed” error
  • Verify your Account SID and Auth Token
  • Check for extra spaces in credentials
  1. “Not SMS capable” error
  • Confirm your Twilio number has SMS capability
  • Purchase a new number if needed
  1. “Rate limit exceeded” error
  • Increase the delay parameter (try 1.0 second)
  • Implement batch processing with longer pauses
  1. 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

  1. Always Obtain Consent: Only message individuals who have opted in
  2. Include Opt-Out Instructions: Add “Reply STOP to unsubscribe” to messages
  3. Test Thoroughly: Start with small batches before full deployment
  4. Monitor Costs: Twilio charges per message sent
  5. Maintain Records: Keep logs of all communications for compliance
  6. Respect Time Zones: Avoid sending messages during inappropriate hours
  7. Validate Content: Ensure messages comply with carrier regulations

Performance Optimization Tips

  1. 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
  1. 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
   )
  1. Parallel Processing (for advanced users):
  • Use Python’s threading or multiprocessing
  • Implement careful rate limiting across threads
  • Monitor Twilio account usage

Security Considerations

  1. Never Hardcode Credentials:
  • Use environment variables
  • Create a separate configuration file
  • Never commit credentials to version control
  1. Input Validation:
  • Validate all phone numbers before sending
  • Sanitize message content
  • Implement proper error handling
  1. 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.

Posts Carousel

Leave a Comment

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

Latest Posts

Most Commented

Featured Videos