📲 How to Automatically Receive SMS Verification Codes with Twilio API (With Complete Code)
A no‑nonsense guide from a full‑stack dev who has burned money on Twilio so you don’t have to. We’ll go from zero to a live webhook that catches OTP codes like a greedy net.
💸 First, Let Me Scare You with the Truth
Before you copy a single line of code, understand two hard gates that filter out most hobbyists:
- You need an upgraded (paid) Twilio account. The free trial lets you send SMS to verified numbers only, but to receive SMS on a Twilio number, you must add funds and upgrade. No way around it.
- You need a Visa/Mastercard that can transact in USD. Twilio bills in US dollars. Some non‑US cards get rejected; virtual cards sometimes work but may be flagged later.
And this isn't free. Let's do quick math: a US phone number costs about $1.00–$2.00 per month (rental), and incoming SMS are typically $0.0075 per message (pay‑as‑you‑go). If you receive 100 verification codes a month, you’re looking at less than $3 total. Cheap, but not zero. For high‑volume usage, costs add up. Plan accordingly.
🏗️ Architecture Overview
Here’s the complete flow we’re going to build:
The key concept: you don’t poll Twilio; Twilio pushes to you. When an SMS hits your Twilio number, Twilio immediately makes an HTTP request to a URL you configure. That URL is your Flask/Python endpoint. You extract the code, and voilà.
🧱 Step-by-Step Implementation
1. Upgrade Your Twilio Account & Get a Phone Number
After signing up at twilio.com, go to Console → Billing → Upgrade and add a minimum of $20. Then install the Python SDK:
pip install twilio flask requests
Now let's search and buy a US number that supports SMS. Create a file buy_number.py:
from twilio.rest import Client
# Your Account SID and Auth Token from console.twilio.com
account_sid = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
auth_token = 'your_auth_token'
client = Client(account_sid, auth_token)
# Search for available US numbers that can receive SMS, under $1.50
available_numbers = client.available_phone_numbers('US').local.list(
sms_enabled=True,
voice_enabled=False,
limit=5
)
for number in available_numbers:
print(f"{number.friendly_name} (locality: {number.locality}, price: {number.price})")
# Purchase the first one (you can add logic to pick by locality)
if available_numbers:
purchased = client.incoming_phone_numbers.create(
phone_number=available_numbers[0].phone_number,
sms_url='' # we'll configure webhook later
)
print(f"✅ Purchased: {purchased.phone_number} SID: {purchased.sid}")
else:
print("❌ No numbers found under price limit")
Run it, and you’ll own a US number. Note the SID and phone number. You’ll need them.
2. Build the SMS Webhook Receiver
We’ll write a Flask server that listens for Twilio’s POST requests. The trick: Twilio sends the SMS body as form data field Body, sender as From, and to number as To.
from flask import Flask, request, jsonify
from twilio.request_validator import RequestValidator
import re, os
app = Flask(__name__)
# Your Twilio auth token (keep secret!)
TWILIO_AUTH_TOKEN = 'your_auth_token'
def validate_twilio_request():
"""Verify that the request genuinely comes from Twilio."""
validator = RequestValidator(TWILIO_AUTH_TOKEN)
url = request.url
# Twilio signs the full URL including params, but for POST we use request.form
params = request.form.to_dict(flat=True)
signature = request.headers.get('X-Twilio-Signature', '')
return validator.validate(url, params, signature)
@app.route('/sms-webhook', methods=['POST'])
def sms_webhook():
# Step 1: Validate signature (reject forged requests)
if not validate_twilio_request():
return 'Bad signature', 403
# Step 2: Extract fields
from_number = request.form.get('From', '')
body = request.form.get('Body', '')
to_number = request.form.get('To', '')
# Step 3: Extract verification code (4-6 consecutive digits)
code_match = re.search(r'\b(\d{4,6})\b', body)
code = code_match.group(1) if code_match else None
# Step 4: Do something with it — here we just print and store in memory
print(f"📩 SMS from {from_number} to {to_number}: {body}")
if code:
print(f"🔑 Verification code: {code}")
# You could put it into a global variable, Redis, or send to Telegram
# For demo, we'll store it in a simple global (not production safe)
latest_code_storage['code'] = code
latest_code_storage['full_body'] = body
# Step 5: Respond with a TwiML (empty is fine, but Twilio expects 200)
return ' '
# Simple in-memory storage for demo; replace with DB in real life
latest_code_storage = {}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Save this as webhook_server.py. The signature validation is crucial. Without it, anyone who discovers your URL can post fake SMS with any content and potentially poison your system.
3. Expose Your Local Server to the Internet (ngrok)
Twilio needs a public HTTPS URL. In development, we use ngrok to tunnel localhost to the internet.
# Install ngrok (once) and run:
ngrok http 5000
You’ll see a forwarding address like https://abc123.ngrok.io. Note this URL. In production, you’d deploy to a real server with HTTPS (Let’s Encrypt).
4. Configure Your Twilio Number to Call the Webhook
Now we link the purchased number to our ngrok URL. You can do this via the Twilio console or with code:
# configure_webhook.py
from twilio.rest import Client
account_sid = 'ACxxxxxxxxxxxx'
auth_token = 'your_auth_token'
client = Client(account_sid, auth_token)
phone_number_sid = 'PNxxxxxxxxxxxx' # the SID from when you bought the number
ngrok_url = 'https://abc123.ngrok.io/sms-webhook'
number = client.incoming_phone_numbers(phone_number_sid).update(
sms_url=ngrok_url,
sms_method='POST'
)
print(f"✅ Webhook updated for {number.phone_number}")
Alternatively, go to Console → Phone Numbers → Manage → Active Numbers, click your number, scroll to Messaging → A message comes in, and set the Webhook URL + HTTP POST.
5. Test the Whole Chain
Start your Flask server:
python webhook_server.py
With ngrok running in another terminal, send an SMS to your Twilio number from any mobile phone. Within seconds, your console should print the extracted verification code. You can also access the global latest_code_storage dictionary from another endpoint to retrieve it:
@app.route('/latest-code', methods=['GET'])
def get_latest_code():
return jsonify(latest_code_storage)
🔥 My Twilio Pitfall Hall of Shame
Pitfall #1: Trial Account Won't Let You Buy Numbers
client.incoming_phone_numbers.create() and get a permission error. You've verified your email, added funds, but something still blocks.
Root Cause: Your account is still in Trial mode. Twilio requires you to explicitly click “Upgrade” in the billing section, even if money is already loaded. After upgrade, you’ll get a fresh ACCOUNT_SID (or keep the old one) and permissions change instantly.
Pitfall #2: Webhook URL Must Be HTTPS (Or You’ll Tear Your Hair Out)
sms_url to http:// and Twilio refuses to deliver, or you see errors in the Twilio console’s error log.
Fix: Twilio enforces HTTPS for production endpoints. If you absolutely must test with HTTP (not recommended), you can uncheck “SSL Certificate Validation” in the console’s webhook settings, but only for testing. ngrok provides HTTPS automatically.
Pitfall #3: Forgetting to Enable “Accept Incoming SMS”
Pitfall #4: US/Canada Numbers Silently Dropping Certain Senders
🔐 Security Note: Signature Validation Saves You from Spoofing
I cannot stress this enough: the validate_twilio_request() function is not optional. The internet is full of bots scanning for open webhook endpoints. If your endpoint blindly trusts POST data, an attacker could inject fake verification codes or flood your log database. Always validate the X-Twilio-Signature header against the full URL and form parameters.
📊 Twilio vs. Hardware Module: When to Use Which?
| Scenario | Twilio | SIM800L / Hardware |
|---|---|---|
| Reliability | Very high, but can't bypass all carrier filters | Works with local SIM, better for specific countries |
| Cost | Monthly number + per-SMS | One‑time hardware + prepaid SIM credit |
| Scalability | Unlimited numbers via API | Limited by USB ports and physical SIMs |
| Setup complexity | Medium (account, webhook, ngrok) | Medium‑High (wiring, AT commands, power) |
| Legal/Contractual | Must agree to Twilio terms, possible KYC | Depends on SIM card carrier |
If you need a quick, globally accessible OTP catcher and don't mind a few dollars a month, Twilio is perfect. If you need a local number in a specific country that Twilio doesn’t serve well, or you want true hardware ownership, go with the SIM800L approach we detailed earlier.
🧠 Final Words & Where to Go Next
With the code above, you have a fully working SMS receiver that can automatically extract verification codes and make them available to your backend services. You can expand it to:
- Push codes to Telegram/Discord via their bot APIs.
- Store codes in a database (Postgres, Redis) with TTL for auto‑cleanup.
- Build a browser dashboard with WebSocket (like we did in our previous real‑time tutorial).
Remember, with great power comes great responsibility — only use this for legitimate purposes like testing your own apps or automating workflows that you have explicit permission to access.
Now go ahead, buy that dollar number, and never miss an OTP again.