Twilio API: Sending SMS and Voice Messages
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=+15551234567Sending 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 USDSMS 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: queuedMaking 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: queuedPhone 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 NoneCommon 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
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