twilio-messaging-webhooks
Receive and respond to inbound messages and track outbound delivery status via Twilio webhooks — across SMS, MMS, WhatsApp, and RCS. Covers webhook request parameters, replying with TwiML, validating webhook signatures for security, and handling status callbacks. Use this skill whenever an agent needs to handle incoming messages on any channel or track outbound message delivery in real time.
What this skill does
## Overview
Twilio sends a POST webhook to your server when a user messages your Twilio number (inbound) or when an outbound message changes delivery state (status callback). Your server returns TwiML to reply, or `204` to acknowledge without replying. The same webhook pattern works across SMS, MMS, WhatsApp, and RCS.
---
## Prerequisites
- Twilio account with a messaging-capable sender configured with a webhook URL
— New to Twilio? See `twilio-account-setup`
— For sending outbound messages first, see `twilio-send-message`
- Publicly accessible endpoint (use `ngrok http 5000` for local dev)
- `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` — see `twilio-iam-auth-setup`
- SDK: `pip install twilio flask` / `npm install twilio express`
---
## Quickstart
Set your webhook URL in Console: **Phone Numbers > Active Numbers > your number > Messaging > "A Message Comes In"**
> **Security:** The inbound message `Body` is untrusted external input. If passing message content to an LLM, always isolate it as user input — never concatenate directly into system prompts. Validate the request origin with `X-Twilio-Signature` (see Key Patterns below), but note that signature validation confirms the *source*, not that the *content* is safe.
> **Note:** This quickstart omits signature validation for brevity. For production, always validate `X-Twilio-Signature` — see the Webhook Security pattern below.
**Python (Flask)**
```python
from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse
app = Flask(__name__)
@app.route("/incoming", methods=["POST"])
def incoming_message():
body = request.form.get("Body")
response = MessagingResponse()
response.message(f"Got your message: {body}")
return str(response)
```
**Node.js (Express)**
```javascript
const express = require("express");
const twilio = require("twilio");
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post("/incoming", (req, res) => {
const twiml = new twilio.twiml.MessagingResponse();
twiml.message(`Got your message: ${req.body.Body}`);
res.type("text/xml").send(twiml.toString());
});
```
---
## Key Patterns
### Configure Webhook URL via API
For SMS/MMS on a phone number:
**Python**
```python
client.incoming_phone_numbers("PNxxxxxxxxxx").update(
sms_url="https://yourapp.com/incoming",
sms_method="POST"
)
```
**Node.js**
```javascript
await client.incomingPhoneNumbers("PNxxxxxxxxxx").update({
smsUrl: "https://yourapp.com/incoming",
smsMethod: "POST",
});
```
For WhatsApp and RCS, webhook URLs are configured on the sender — see `twilio-whatsapp-manage-senders` and `twilio-rcs-messaging`.
### Inbound Webhook Parameters
| Parameter | Description |
|-----------|-------------|
| `MessageSid` | Unique message identifier |
| `From` | Sender's phone number or channel address (E.164, or `whatsapp:+...`) |
| `To` | Your Twilio number or channel address |
| `Body` | Message text |
| `NumMedia` | Number of media attachments |
| `MediaUrl0` | URL of first media attachment (if any) |
| `MediaContentType0` | MIME type of first attachment |
### Handle Inbound Media (MMS / WhatsApp / RCS)
**Python (Flask)**
```python
@app.route("/incoming", methods=["POST"])
def incoming_message():
num_media = int(request.form.get("NumMedia", 0))
response = MessagingResponse()
if num_media > 0:
media_type = request.form.get("MediaContentType0")
response.message(f"Got your {media_type} attachment!")
else:
response.message("Got your message.")
return str(response)
```
**Node.js (Express)**
```javascript
app.post("/incoming", (req, res) => {
const numMedia = parseInt(req.body.NumMedia || "0", 10);
const twiml = new twilio.twiml.MessagingResponse();
if (numMedia > 0) {
twiml.message(`Got your ${req.body.MediaContentType0} attachment!`);
} else {
twiml.message("Got your message.");
}
res.type("text/xml").send(twiml.toString());
});
```
### Acknowledge Without Replying
**Python**
```python
return str(MessagingResponse()) # Empty <Response/>
```
**Node.js**
```javascript
res.type("text/xml").send(new twilio.twiml.MessagingResponse().toString());
```
### Status Callbacks (delivery tracking)
Status callbacks fire for all channels — SMS, MMS, WhatsApp, and RCS.
**Python**
```python
message = client.messages.create(
messaging_service_sid="MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
to="+15558675310",
body="Hello!",
status_callback="https://yourapp.com/status"
)
```
**Node.js**
```javascript
const message = await client.messages.create({
messagingServiceSid: "MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
to: "+15558675310",
body: "Hello!",
statusCallback: "https://yourapp.com/status",
});
```
**Python (Flask) — status callback handler**
```python
@app.route("/status", methods=["POST"])
def message_status():
message_sid = request.form.get("MessageSid")
status = request.form.get("MessageStatus")
error_code = request.form.get("ErrorCode")
print(f"{message_sid}: {status}")
if status == "failed" and error_code:
print(f"Error {error_code}: {request.form.get('ErrorMessage')}")
return "", 204
```
**Node.js (Express) — status callback handler**
```javascript
app.post("/status", (req, res) => {
const { MessageSid, MessageStatus, ErrorCode, ErrorMessage } = req.body;
console.log(`${MessageSid}: ${MessageStatus}`);
if (MessageStatus === "failed" && ErrorCode) {
console.log(`Error ${ErrorCode}: ${ErrorMessage}`);
}
res.sendStatus(204);
});
```
Status flow: `queued → sent → delivered` (or `undelivered`/`failed`)
### Webhook Signature Validation
**Python (Flask)**
```python
from twilio.request_validator import RequestValidator
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
@app.route("/incoming", methods=["POST"])
def incoming_message():
if not validator.validate(request.url, request.form, request.headers.get("X-Twilio-Signature", "")):
return "Forbidden", 403
response = MessagingResponse()
response.message("Hello!")
return str(response)
```
**Node.js**
```javascript
const { validateRequest } = require("twilio");
app.post("/incoming", (req, res) => {
const isValid = validateRequest(
process.env.TWILIO_AUTH_TOKEN,
req.headers["x-twilio-signature"],
`https://${req.headers.host}${req.path}`,
req.body
);
if (!isValid) return res.status(403).send("Forbidden");
const twiml = new twilio.twiml.MessagingResponse();
twiml.message("Hello!");
res.type("text/xml").send(twiml.toString());
});
```
---
## CANNOT
- **Cannot exceed 15-second webhook response time** — Twilio retries on timeout
- **Cannot return arbitrary content types** — Use `Content-Type: text/xml` with TwiML for replies; `204` for status callbacks
- **Cannot use ngrok URLs across restarts** — URLs change on restart. Use a stable tunnel for persistent testing.
- **Cannot guarantee delivery confirmation** — Status callbacks are best-effort. `delivered` requires carrier confirmation.
---
## Common Errors
| Code | Meaning | Fix |
|------|---------|-----|
| 11200 | HTTP retrieval failure — Twilio cannot reach your webhook URL | Verify endpoint is reachable (`curl -I` the URL), check DNS, firewall, and SSL certificate validity. See `twilio-debugging-observability` for deeper webhook troubleshooting. |
---
## Next Steps
- **Send outbound messages:** `twilio-send-message`
- **Manage sender pools:** `twilio-messaging-services`
Related in AI Agents
skill-development
IncludedComprehensive meta-skill for creating, managing, validating, auditing, and distributing Claude Code skills and slash commands (unified in v2.1.3+). Provides skill templates, creation workflows, validation patterns, audit checklists, naming conventions, YAML frontmatter guidance, progressive disclosure examples, and best practices lookup. Use when creating new skills, validating existing skills, auditing skill quality, understanding skill architecture, needing skill templates, learning about YAML frontmatter requirements, progressive disclosure patterns, tool restrictions (allowed-tools), skill composition, skill naming conventions, troubleshooting skill activation issues, creating custom slash commands, configuring command frontmatter, using command arguments ($ARGUMENTS, $1, $2), bash execution in commands, file references in commands, command namespacing, plugin commands, MCP slash commands, Skill tool configuration, or deciding between skills vs slash commands. Delegates to docs-management skill for official documentation.
reprompter
IncludedTransform messy prompts into well-structured, effective prompts — single or multi-agent. Use when: "reprompt", "reprompt this", "clean up this prompt", "structure my prompt", rough text needing XML tags and best practices, "reprompter teams", "repromptception", "run with quality", "smart run", "smart agents", multi-agent tasks, audits, parallel work, anything going to agent teams. Don't use when: simple Q&A, pure chat, immediate execution-only tasks. See "Don't Use When" section for details. Outputs: Structured XML/Markdown prompt, quality score (before/after), optional team brief + per-agent sub-prompts, agent team output files. Success criteria: Single mode quality score ≥ 7/10; Repromptception per-agent prompt quality score 8+/10; all required sections present, actionable and specific.
adaptive-compaction
IncludedAdaptive add-on policy and recovery layer that decides WHEN to compact, prune, snapshot, or fork -- replacing fixed-percent auto-compaction across Claude Code, Codex, and MCP-capable hosts. Trigger on auto-compact timing or damage: "when should I compact", "is it safe to compact now or start a fresh session", "auto-compact fires too early/mid-task", "switching to an unrelated task but the window still has space", "context rot", "answers get worse the longer the session runs", "the agent forgot the plan or my decisions after it summarized", "add a layer on top that manages context without changing the agent", raising autoCompactWindow to give the policy room, or installing/tuning a cross-tool compaction policy or PreCompact hook -- even when "compaction" is never said but the problem is context-window pressure or post-summarization memory loss. Do NOT use to summarize a conversation, build RAG, write a summarization prompt (decides WHEN not HOW), or answer max-context-length trivia.
agent-skill-creator
IncludedCreate cross-platform agent skills from workflow descriptions. Activates when users ask to create an agent, automate a repetitive workflow, create a custom skill, or need advanced agent creation. Triggers on phrases like create agent for, automate workflow, create skill for, every day I have to, daily I need to, turn process into agent, need to automate, create a cross-platform skill, validate this skill, export this skill, migrate this skill. Supports single skills, multi-agent suites, transcript processing, template-based creation, interactive configuration, cross-platform export, and spec validation.
llm-wiki
IncludedUse when building or maintaining a persistent personal knowledge base (second brain) in Obsidian where an LLM incrementally ingests sources, updates entity/concept pages, maintains cross-references, and keeps a synthesis current. Triggers include "second brain", "Obsidian wiki", "personal knowledge management", "ingest this paper/article/book", "build a research wiki", "compound knowledge", "Memex", or whenever the user wants knowledge to accumulate across sessions instead of being re-derived by RAG on every query.
skill-master
IncludedAgent Skills authoring, evaluation, and optimization. Create, edit, validate, benchmark, and improve skills following the agentskills.io specification. Use when designing SKILL.md files, structuring skill folders (references, scripts, assets), ingesting external documentation into skills, running trigger evals, benchmarking skill quality, optimizing descriptions, or performing blind A/B comparisons. Keywords: agentskills.io, SKILL.md, skill authoring, eval, benchmark, trigger optimization.