Skip to content
Twilio API: Sending SMS and Voice Messages

Twilio API: Sending SMS and Voice Messages

DodaTech Updated Jun 20, 2026 10 min read

Twilio is a cloud communications platform that provides APIs for sending SMS messages, making voice calls, verifying phone numbers, and enabling WhatsApp messaging, all with global reach and carrier-grade reliability.

What You’ll Learn

By the end of this tutorial, you’ll send SMS and WhatsApp messages, make voice calls, implement phone verification, handle incoming messages via webhooks, and manage Twilio error handling.

Why Twilio Matters

Phone communication is deeply personal. SMS open rates exceed 98% (compared to 20% for email), making it critical for time-sensitive notifications. Doda Browser uses Twilio to send 2FA codes, alert users of malware detected on their devices, and verify phone numbers during account recovery.

Twilio Architecture


flowchart LR
    subgraph "Twilio System"
        A[Your App] -- "REST API" --> T[Twilio API]
        T -- "SMS" --> C[Carrier Network]
        T -- "Voice" --> P[PSTN]
        T -- "WhatsApp" --> W[WhatsApp API]
        C -- "Incoming SMS" --> T
        T -- "Webhook POST" --> A
    end
    style T fill:#f22e46,color:#fff

Setup

pip install twilio

# Set environment variables
export TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxx
export TWILIO_AUTH_TOKEN=xxxxxxxxxxxx
export TWILIO_PHONE_NUMBER=+15551234567

Sending SMS

# send_sms.py
# Send an SMS message via Twilio REST API

from twilio.rest import Client
import os

# Initialize client
account_sid = os.environ['TWILIO_ACCOUNT_SID']
auth_token = os.environ['TWILIO_AUTH_TOKEN']
client = Client(account_sid, auth_token)

def send_sms(to_number, message_body):
    """Send an SMS message."""
    message = client.messages.create(
        body=message_body,
        from_=os.environ['TWILIO_PHONE_NUMBER'],
        to=to_number,
    )

    print(f"Message sent!")
    print(f"  SID: {message.sid}")
    print(f"  From: {message.from_}")
    print(f"  To: {message.to}")
    print(f"  Status: {message.status}")
    print(f"  Price: {message.price} {message.price_unit}")

    return message

# Test
send_sms(
    to_number='+15559876543',
    message_body='Your Doda Browser scan found a threat. Check the app for details.'
)

Expected output:

Message sent!
  SID: SMa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
  From: +15551234567
  To: +15559876543
  Status: queued
  Price: None USD

SMS with Status Callback

# send_sms_callback.py
# SMS with delivery status tracking

from twilio.rest import Client
import os

client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])

def send_sms_with_tracking(to_number, message_body):
    """Send SMS and receive delivery status via webhook."""
    message = client.messages.create(
        body=message_body,
        from_=os.environ['TWILIO_PHONE_NUMBER'],
        to=to_number,
        status_callback='https://myapp.com/twilio/status',
        # Twilio POSTs to this URL when status changes
        # Statuses: queued → sent → delivered/failed/undelivered
    )

    print(f"SMS sent: {message.sid}")
    print(f"Initial status: {message.status}")
    print(f"Status callback will POST to: {message.status_callback}")

    return message

send_sms_with_tracking(
    to_number='+15559876543',
    message_body='Your verification code is: 842910'
)

WhatsApp messaging:

# send_whatsapp.py
# Send WhatsApp message via Twilio

from twilio.rest import Client
import os

client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])

def send_whatsapp(to_number, message_body):
    """Send a WhatsApp message. Requires Twilio WhatsApp Sandbox setup."""
    message = client.messages.create(
        body=message_body,
        from_='whatsapp:+14155238886',  # Twilio WhatsApp sandbox number
        to=f'whatsapp:{to_number}',
    )

    print(f"WhatsApp message sent: {message.sid}")
    print(f"Status: {message.status}")

    # For media messages
    if message_body.startswith('http'):
        print(f"Media URL detected — sending as media message")

    return message

# Text message
send_whatsapp(
    to_number='+15559876543',
    message_body='Hello from Doda! Your weekly security report is ready.'
)

# Media message (image)
send_whatsapp(
    to_number='+15559876543',
    message_body='https://dodatech.com/charts/weekly_report.png'
)

Expected output:

WhatsApp message sent: SMa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p7
Status: queued

Making Voice Calls

# make_call.py
# Make a voice call with Twilio

from twilio.rest import Client
import os

client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])

def make_voice_call(to_number, message_text):
    """Make a voice call and speak a message using TwiML."""
    from twilio.twiml.voice_response import VoiceResponse

    # Build TwiML response — Twilio fetches this URL when call connects
    response = VoiceResponse()
    response.say(message_text, voice='alice', language='en-US')
    response.hangup()

    # In production, host this TwiML at a URL
    # For testing, use TwiML Bin or host it on your server
    call = client.calls.create(
        twiml=response,
        to=to_number,
        from_=os.environ['TWILIO_PHONE_NUMBER'],
        status_callback='https://myapp.com/twilio/call-status',
        status_callback_event=['initiated', 'ringing', 'answered', 'completed'],
    )

    print(f"Voice call initiated: {call.sid}")
    print(f"To: {call.to}")
    print(f"Status: {call.status}")

    return call

make_voice_call(
    to_number='+15559876543',
    message_text='Hello! This is Doda Antivirus calling with an important security alert. A threat was detected on your device. Please open the app for details.'
)

Expected output:

Voice call initiated: CAa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p8
To: +15559876543
Status: queued

Phone Verification (Verify API)

# verify.py
# Phone number verification with Twilio Verify

from twilio.rest import Client
import os

client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])

# Create a verification service in Twilio console first
VERIFY_SERVICE_SID = 'VAxxxxxxxxxxxx'

def send_verification_code(phone_number):
    """Send a 6-digit verification code via SMS."""
    verification = client.verify.v2.services(VERIFY_SERVICE_SID) \
        .verifications.create(
            to=phone_number,
            channel='sms',  # Also: 'call', 'email', 'whatsapp'
        )

    print(f"Verification sent to {phone_number}")
    print(f"Status: {verification.status}")
    print(f"Channel: {verification.channel}")
    return verification

def check_verification_code(phone_number, code):
    """Verify the code entered by the user."""
    verification_check = client.verify.v2.services(VERIFY_SERVICE_SID) \
        .verification_checks.create(
            to=phone_number,
            code=code,
        )

    if verification_check.status == 'approved':
        print(f"✅ Phone {phone_number} verified successfully!")
        return True
    else:
        print(f"❌ Invalid code for {phone_number}")
        return False

# Flow: send code → user enters code → verify
send_verification_code('+15559876543')

# User enters the 6-digit code they received
is_verified = check_verification_code('+15559876543', '842910')

Expected output:

Verification sent to +15559876543
Status: pending
Channel: sms
✅ Phone +15559876543 verified successfully!

Webhooks for Incoming Messages

# incoming_webhook.py
# Handle incoming SMS and WhatsApp messages

from flask import Flask, request, twiml
from twilio.twiml.messaging_response import MessagingResponse
import json

app = Flask(__name__)

# Store conversation history (use Redis in production)
conversations = {}

@app.route('/twilio/sms', methods=['POST'])
def handle_incoming_sms():
    """Handle incoming SMS and WhatsApp messages."""
    # Extract message details
    from_number = request.form['From']
    to_number = request.form['To']
    body = request.form['Body']
    message_sid = request.form['MessageSid']
    num_media = int(request.form.get('NumMedia', 0))

    print(f"Incoming message from {from_number}:")
    print(f"  Body: {body}")
    print(f"  Media count: {num_media}")

    # Handle media attachments
    if num_media > 0:
        for i in range(num_media):
            media_url = request.form.get(f'MediaUrl{i}')
            content_type = request.form.get(f'MediaContentType{i}')
            print(f"  Media {i}: {media_url} ({content_type})")

    # Build response
    response = MessagingResponse()

    # Simple auto-reply logic
    body_lower = body.lower()
    if 'help' in body_lower:
        response.message("Available commands:\n- STATUS: Check your account\n- REPORT: Get security report\n- STOP: Unsubscribe from messages")
    elif 'stop' in body_lower or 'unsubscribe' in body_lower:
        response.message("You've been unsubscribed from Doda alerts.")
        # Update user preference in database
    elif 'status' in body_lower:
        response.message("Your Doda Browser subscription is active. No threats detected in the last 24 hours.")
    else:
        response.message("Thanks for your message! A Doda support agent will respond shortly.")

    print(f"Response: {str(response)}")

    return str(response), 200, {'Content-Type': 'text/xml'}

@app.route('/twilio/status', methods=['POST'])
def handle_delivery_status():
    """Receive delivery status updates."""
    message_sid = request.form['MessageSid']
    message_status = request.form['MessageStatus']
    error_code = request.form.get('ErrorCode', 'none')
    to_number = request.form['To']

    print(f"Status update for {message_sid}:")
    print(f"  To: {to_number}")
    print(f"  Status: {message_status}")
    print(f"  Error: {error_code}")

    # Log to database for analytics
    # update_message_status(message_sid, message_status)

    return '', 200

if __name__ == '__main__':
    # twilio phone-numbers:update +15551234567 --sms-url http://localhost:5000/twilio/sms
    app.run(port=5000, debug=True)

Error Handling

# error_handling.py
# Robust Twilio error handling

from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException
import os
import time

client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])

def send_sms_robust(to_number, message_body, max_retries=3):
    """Send SMS with comprehensive error handling."""
    for attempt in range(max_retries):
        try:
            message = client.messages.create(
                body=message_body,
                from_=os.environ['TWILIO_PHONE_NUMBER'],
                to=to_number,
            )
            print(f"SMS sent: {message.sid}")
            return message

        except TwilioRestException as e:
            print(f"Attempt {attempt + 1}/{max_retries} failed:")

            if e.code == 21211:  # Invalid 'To' number
                print(f"  Invalid phone number: {to_number}")
                return None  # Don't retry

            elif e.code == 21610:  # Carrier blocked
                print(f"  Carrier blocked this message type")
                return None  # Don't retry

            elif e.code == 21608:  # Not verified for trial
                print(f"  Trial account: verify {to_number} first")
                return None

            elif e.code == 30001:  # Queue overflow
                print(f"  Queue full, retrying...")
                time.sleep(2 ** attempt)  # Exponential backoff

            elif e.code == 14101:  # Unreachable
                print(f"  Number unreachable at this time")
                time.sleep(60)

            else:
                print(f"  Error {e.code}: {e.msg}")
                time.sleep(2 ** attempt)

        except Exception as e:
            print(f"Unexpected error: {e}")
            time.sleep(2 ** attempt)

    print(f"Failed to send SMS after {max_retries} attempts")
    return None

Common Errors

1. Trial Account Restrictions

Twilio trial accounts can only send messages to verified phone numbers. You must add each recipient number to the Verified Caller IDs list in the console, or upgrade to a paid account.

2. Invalid Phone Number Format

Always use E.164 format: +1 for US, country code + area code + number. A missing + or wrong country code causes the 21211 error.

3. Not Handling Delivery Failures

SMS delivery can fail (carrier issues, phone off, number ported). Use status callbacks with status_callback URL to track delivery and retry failed messages.

4. Hard-Coding Account Credentials

Storing TWILIO_AUTH_TOKEN in source code is a security risk. Use environment variables or a secret manager. Rotate auth tokens periodically.

5. Ignoring Message Segment Limits

SMS messages are limited to 160 characters (GSM-7) or 70 characters (Unicode). Longer messages are split into segments, costing more. Track message.num_segments in production.

6. Not Handling Rate Limits

Twilio has rate limits per second. Sending thousands of messages simultaneously triggers HTTP 429 errors. Implement queuing with exponential backoff.

Practice Questions

1. What format must phone numbers be in for Twilio?

E.164 format: + followed by country code and national number. Example: +15551234567 (US), +442071234567 (UK).

2. How do you track whether an SMS was delivered?

Set a status_callback URL when sending. Twilio POSTs delivery status updates (sent, delivered, failed, undelivered) to that URL.

3. What is TwiML?

TwiML (Twilio Markup Language) is an XML-based language that tells Twilio what to do during a call or SMS session. For example, <Say> speaks text during a voice call.

4. How does Twilio Verify work?

You create a Verification Service, send a code via verifications.create(), then check the user’s input with verification_checks.create(). The service handles code generation, expiry, and resend logic.

5. Challenge: Build an SMS alert system that: (1) sends a notification when a server goes down, (2) tracks delivery status, (3) retries up to 3 times with 1-minute intervals if undelivered, (4) escalates to a voice call if SMS fails, (5) logs all notifications to a database.

Implement with a JobQueue. First send SMS with status callback. If undelivered after 3 retries, trigger a voice call using Twilio’s <Say> verb. Log each attempt with timestamp, status, and channel used.

Mini Project: SMS Keyword Auto-Responder

# auto_responder.py
# SMS keyword auto-responder for common inquiries

from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse
import json

app = Flask(__name__)

# Keyword → response mapping
KEYWORD_RESPONSES = {
    'BALANCE': 'Your current balance is $49.99. Next payment due: July 15, 2026.',
    'THREATS': 'No threats detected in the last 24 hours. Last scan: June 19, 2026 at 3:42 PM.',
    'HELP': 'Available keywords: BALANCE, THREATS, SUBSCRIPTION, RENEW, SUPPORT',
    'SUBSCRIPTION': 'You are on the Pro plan ($9.99/month). Next renewal: July 1, 2026.',
    'RENEW': 'To renew your subscription, visit: https://dodatech.com/account/billing',
    'SUPPORT': 'A support agent will contact you within 1 hour. Your ticket ID is TKT-789.',
    'STOP': 'You have been unsubscribed from SMS alerts. Reply START to resume.',
    'START': 'Welcome back! SMS alerts have been resumed.',
}

@app.route('/sms/auto-respond', methods=['POST'])
def auto_respond():
    """Auto-respond to SMS based on keyword."""
    from_number = request.form['From']
    body = request.form['Body'].strip().upper()

    print(f"Received from {from_number}: {body}")

    response = MessagingResponse()

    # Find matching keyword
    keyword = body.split()[0] if body else ''
    reply = KEYWORD_RESPONSES.get(keyword)

    if reply:
        response.message(reply)
        print(f"Auto-response: {reply[:50]}...")
    else:
        response.message(
            f"Unknown command: '{keyword}'. "
            f"Reply HELP for available keywords."
        )

    return str(response), 200, {'Content-Type': 'text/xml'}

if __name__ == '__main__':
    print("SMS Auto-Responder running on http://localhost:5000")
    print(f"Configured keywords: {', '.join(KEYWORD_RESPONSES.keys())}")
    app.run(port=5000, debug=True)

FAQ

How much does Twilio SMS cost?
US/Canada SMS costs ~$0.0079 per message. International varies ($0.02-$0.10+). WhatsApp messages cost $0.005 per conversation. Check Twilio’s current pricing page.
Can I send SMS to any country?
Twilio supports sending to 180+ countries. Some countries require pre-registration (sending to India requires DLT registration). Check Twilio’s country support docs.
What is the difference between Twilio SMS and WhatsApp?
SMS works on any phone without internet. WhatsApp requires the recipient to have WhatsApp installed. WhatsApp supports rich media (images, documents, buttons). SMS is cheaper but more universal.
How long does SMS delivery take?
Typically 1-10 seconds. In rare cases (international, carrier congestion), up to 30 minutes. Delivery receipts via status_callback give you the actual delivery time.
Can I receive SMS messages with Twilio?
Yes. Buy a phone number from Twilio, configure the SMS webhook URL in the console, and Twilio POSTs incoming messages to your endpoint as XML form data.

Related Concepts

What’s Next

You now know Twilio! Next, learn about SendGrid for transactional email, then explore webhooks for handling incoming Twilio messages at scale.

  • Practice daily — Set up a Twilio trial account and send your first SMS
  • Build a project — Build a 2FA verification system with Twilio Verify and a status dashboard
  • Explore related topics — Check out Twilio SendGrid for email, Twilio Flex for contact centers, and Twilio Segment for customer data

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro