Receive SMS with Twilio: Inbound Message Webhook Handling
In this tutorial, you will learn about Receive SMS with Twilio: Inbound Message Webhook Handling. We cover key concepts, practical examples, and best practices to help you master this topic.
Twilio captures inbound SMS messages and forwards them to your server via HTTP webhook, enabling auto-replies, command processing, conversational flows, and STOP/HELP keyword handling with TwiML responses.
What You'll Learn
How to configure your Twilio number for inbound SMS, receive messages via webhook, parse message content, send automated TwiML replies, handle STOP commands for opt-out Compliance, and build conversational SMS flows.
Why It Matters
Bidirectional SMS enables customer support, order inquiries, and interactive notifications. DodaTech uses inbound SMS for support requests, order status inquiries, and two-way customer communication.
Real-World Use
A DodaTech customer texts "STATUS" to the support number. Twilio forwards the message to the webhook server, which looks up the customer's order, responds with "Your order ORD-12345 is out for delivery," and logs the interaction.
flowchart LR
A["Customer Text\nSTATUS to Number"] --> B["Twilio\nReceives SMS"]
B --> C["Webhook POST\nto Your Server"]
C --> D["Parse Message\nBody"]
D --> E{"Command\nType"}
E -->|STATUS| F["Look Up\nOrder"]
E -->|HELP| G["Send Help\nMenu"]
E -->|STOP| H["Opt Out\nUser"]
F --> I["Reply:\nOrder Status"]
style A fill:#dbeafe,stroke:#2563eb
style C fill:#f22f46,color:#fff
style I fill:#bbf7d0,stroke:#16a34a
Configuring the SMS Webhook
# In Twilio Console:
# 1. Go to Phone Numbers > Manage > Active Numbers
# 2. Select your number
# 3. Set "A message comes in" webhook URL
# 4. Choose HTTP POST (default)
# Or configure via API:
from twilio.rest import Client
import os
client = Client(
os.environ["TWILIO_ACCOUNT_SID"],
os.environ["TWILIO_AUTH_TOKEN"]
)
# Update phone number webhook
number_sid = "PNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
incoming = client.incoming_phone_numbers(number_sid).update(
sms_url="https://example.com/inbound-sms",
sms_method="POST"
)
print(f"Webhook set: {incoming.sms_url}")
# Expected output:
# Webhook set: https://example.com/inbound-sms
Handling Inbound Messages (Flask)
from flask import Flask, request, Response
from twilio.twiml.messaging_response import MessagingResponse
app = Flask(__name__)
@app.route("/inbound-sms", methods=["POST"])
def inbound_sms():
# Extract message details
from_number = request.form["From"]
to_number = request.form["To"]
body = request.form["Body"].strip().upper()
message_sid = request.form["MessageSid"]
print(f"From: {from_number}")
print(f"Message: {body}")
print(f"SID: {message_sid}")
# Prepare response
resp = MessagingResponse()
if body == "STATUS":
resp.message("Your DodaTech order ORD-12345 is out for delivery.")
elif body == "HELP":
resp.message(
"Available commands:\n"
"STATUS - Check order status\n"
"HELP - Show this menu\n"
"STOP - Unsubscribe from messages"
)
elif body == "STOP":
# Handle opt-out
resp.message("You have been unsubscribed. Reply START to resubscribe.")
else:
resp.message(f"Unknown command: {body}. Reply HELP for options.")
return Response(str(resp), mimetype="text/xml")
# Start: flask run --port 8000
# Expected output when customer texts "STATUS":
# From: +14155551234
# Message: STATUS
# SID: SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TwiML Messaging Response
<!-- TwiML response generated by the code above: -->
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>
Your DodaTech order ORD-12345 is out for delivery.
</Message>
</Response>
Handling Opt-Out (STOP) Commands
import sqlite3
def handle_opt_out(from_number):
"""Store opt-out preference and do not send future messages."""
conn = sqlite3.connect("optouts.db")
cursor = conn.cursor()
cursor.execute("""
INSERT OR REPLACE INTO opt_outs (phone_number, opted_out_at)
VALUES (?, datetime('now'))
""", (from_number,))
conn.commit()
conn.close()
print(f"Opted out: {from_number}")
def is_opted_out(from_number):
conn = sqlite3.connect("optouts.db")
cursor = conn.cursor()
cursor.execute(
"SELECT 1 FROM opt_outs WHERE phone_number = ?",
(from_number,)
)
result = cursor.fetchone()
conn.close()
return result is not None
# Check before sending
def send_sms_if_allowed(to_number, message_body):
if is_opted_out(to_number):
print(f"Skipped {to_number}: opted out")
return None
message = client.messages.create(
body=message_body,
from_=os.environ["TWILIO_PHONE_NUMBER"],
to=to_number
)
print(f"Sent to {to_number}: {message.sid}")
return message
Conversational Flow with State
# Maintain conversation state per number
conversation_state = {}
@app.route("/inbound-sms", methods=["POST"])
def sms_conversation():
from_number = request.form["From"]
body = request.form["Body"].strip().upper()
resp = MessagingResponse()
state = conversation_state.get(from_number, {})
if body == "ORDER":
conversation_state[from_number] = {"step": "awaiting_order_id"}
resp.message("Please enter your order ID (e.g., ORD-XXXXX):")
elif state.get("step") == "awaiting_order_id":
order_id = body
# Look up order
conversation_state[from_number] = {"step": "awaiting_action", "order_id": order_id}
resp.message(
f"Order {order_id} found.\n"
"Reply STATUS for update, CANCEL to cancel, or MENU for options."
)
elif body == "STATUS" and state.get("order_id"):
resp.message(f"Order {state['order_id']} is being processed and will ship tomorrow.")
conversation_state.pop(from_number, None) # End conversation
else:
resp.message("Reply ORDER to start, HELP for commands, or STOP to opt out.")
return Response(str(resp), mimetype="text/xml")
Common Mistakes
1. Returning Non-TwiML Responses
Twilio expects a valid TwiML response. Returning plain text or JSON causes an error. Always use MessagingResponse and set the correct Content-Type: text/xml.
2. Not Handling STOP Commands
US regulations require honoring opt-out requests within 24 hours. Twilio automatically handles STOP and replies for you, but your app must also stop sending messages to opted-out numbers.
3. Webhook Timeout
Twilio waits 15 seconds for your response. If your handler takes longer (e.g., slow database), respond with <Response/> immediately and Process async, or use a queue.
4. Not Validating Incoming Requests
Anyone could POST to your webhook URL. Validate that requests come from Twilio by checking the X-Twilio-Signature header using the RequestValidator.
5. Storing Full Message Body in Logs
SMS bodies may contain sensitive information (passwords, PII). Log message SIDs only, and store message bodies in a secure database with access controls, not in plaintext logs.
Practice Questions
- How does Twilio deliver inbound SMS to your server?
- What is TwiML and why is it used for replies?
- How do you handle SMS opt-outs (STOP commands)?
- How do you validate that a webhook request came from Twilio?
Answers:
- Twilio sends an HTTP POST to the
sms_urlconfigured on your phone number. The POST containsFrom,To,Body,MessageSid, and other metadata. - TwiML (Twilio Markup Language) is an XML format that tells Twilio what to do next — in this case, send an SMS reply. Your server returns TwiML, Twilio executes it.
- When receiving a STOP command, record the opt-out in your database. Before sending any future message, check if the number is opted out. Twilio also auto-handles STOP at the platform level.
- Use
twilio.request_validator.RequestValidatorto validate theX-Twilio-Signatureheader against your Auth Token. This verifies the request came from Twilio, not a third party.
Challenge: Build a complete inbound SMS system: configure the webhook URL, implement a conversational flow (ORDER -> STATUS -> response), handle STOP/HELP commands, validate Twilio signatures, maintain conversation state, and test the full flow using ngrok http 8000 for a public webhook URL.
FAQ
Mini Project
Build an interactive SMS support system: configure a phone number webhook, implement a multi-step order inquiry flow (ORDER -> ID -> STATUS), handle STOP/HELP commands with opt-out tracking, validate request signatures, test with ngrok, and review message logs.
What's Next
Twilio Verify — implement phone verification and two-factor authentication.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro