Skip to content

Receive SMS with Twilio: Inbound Message Webhook Handling

DodaTech Updated 2026-06-28 6 min read

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

  1. How does Twilio deliver inbound SMS to your server?
  2. What is TwiML and why is it used for replies?
  3. How do you handle SMS opt-outs (STOP commands)?
  4. How do you validate that a webhook request came from Twilio?

Answers:

  1. Twilio sends an HTTP POST to the sms_url configured on your phone number. The POST contains From, To, Body, MessageSid, and other metadata.
  2. 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.
  3. 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.
  4. Use twilio.request_validator.RequestValidator to validate the X-Twilio-Signature header 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

What happens if my webhook server is down?

Twilio retries the webhook up to 3 times with exponential backoff. If all retries fail, the message is logged in the Console with a failed status.

Can I have multiple webhook URLs?

A phone number can only have one SMS webhook URL. To route to multiple handlers, use a proxy server or a Messaging Service with status callbacks.

What is the difference between sms_url and status_callback?

The sms_url handles incoming messages. The status_callback receives delivery status updates for outgoing messages. They serve different purposes.

How do I test inbound SMS without a real phone?

Use Twilio's test credentials with the sandbox or simulate inbound messages via the Twilio Console > Phone Numbers > Test with a live phone number.

Can I receive MMS messages?

Yes, Twilio forwards MMS media URLs in the webhook. The request includes NumMedia and MediaUrl0, MediaUrl1 etc. for attached images.

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