关于 twilio-sms
twilio-sms 是面向 OpenClaw 的 Twilio Programmable Messaging 实践指南,供构建短信和多媒体消息系统的工程师使用。内容涵盖发送、Webhook、送达跟踪、退订合规及生产环境故障处理。
主要功能
- 01发送 SMS 和 MMS
- 02接收消息并处理 TwiML 回复
- 03跟踪消息送达状态
- 04处理 STOP 退订及关键词
- 05验证 Webhook 签名并实现幂等
- 06参考 A2P 10DLC 合规要求
适用场景
- 01搭建客户通知消息管道
- 02实现双向短信支持
- 03诊断消息送达失败
- 04降低退订及 Webhook 重试风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ alphaonedev/ openclaw-graph --skill twilio-sms安装后在 Claude Code 中输入/twilio-sms确认已加载。
详细介绍
twilio-sms
用途
让 OpenClaw 能够在生产环境中实现和运营 Twilio Programmable Messaging(SMS/MMS):
- 可靠地发送 SMS/MMS(Messaging Services、地理匹配、固定发送方、媒体限制)。
- 通过 Webhook 接收入站消息,并使用 TwiML 回复。
- 通过状态回调跟踪投递生命周期(queued/sent/delivered/undelivered/failed)。
- 实现退订/STOP 合规和关键词工作流。
- 处理 A2P 10DLC(美国长号码)和免费电话号码验证限制。
- 调试并强化:签名验证、重试、幂等性、速率限制、运营商错误。
此技能适用于构建消息传递管道、客户通知、双向支持和合规敏感消息系统的工程师。
前提条件
账户和 Twilio Console 设置
- 已启用 Programmable Messaging 的 Twilio 账户。
- 至少具备以下其中一项:
- Messaging Service(推荐),并配置发送方池(长号码 / 免费电话号码 / 短码)。
- 一个支持 SMS/MMS 的专用 Phone Number。
- 对于美国 A2P 10DLC:
- 已在 Twilio Console 中完成品牌和 Campaign 注册(Messaging → Regulatory Compliance / A2P 10DLC)。
- 对于免费电话号码:
- 如果向美国/加拿大发送大量消息,已提交/通过免费电话号码验证。
运行时版本(已测试)
- Node.js 20.11.1(LTS)+ npm 10.2.4
- Python 3.11.7
- Twilio SDK:
twilio(Node)4.23.0twilio(Python)9.4.1
- Web 框架示例:
- Express 4.18.3
- FastAPI 0.109.2 + Uvicorn 0.27.1
- 可选工具:
- Twilio CLI 5.16.0
- ngrok 3.13.1(本地 Webhook 隧道)
- Docker 25.0.3 + Compose v2 2.24.6
凭据和身份验证
你需要:
TWILIO_ACCOUNT_SID(以AC...开头)TWILIO_AUTH_TOKEN- 以下其中一项:
TWILIO_MESSAGING_SERVICE_SID(以MG...开头)优先TWILIO_FROM_NUMBER(E.164 格式,例如+14155552671)
请将密钥存储在密钥管理器中(AWS Secrets Manager / GCP Secret Manager / Vault)。本地开发时可以使用 .env。
网络和 Webhook 要求
- 用于入站消息和状态回调的公共 HTTPS 端点。
- 必须接受
application/x-www-form-urlencoded(Twilio 默认格式)和/或 JSON,具体取决于端点。 - 对入站 Webhook 验证 Twilio 签名(
X-Twilio-Signature)。
核心概念
Programmable Messaging 对象
- Message:一条出站或入站 SMS/MMS。通过
MessageSid(SM...)标识。 - Messaging Service:对发送方的抽象,支持:
- 发送方池
- 地理匹配
- 固定发送方
- 智能编码
- 状态回调配置
- Status Callback:在消息状态变化时调用的 Webhook。
- Inbound Webhook:当 Twilio 收到发往你的号码/服务的入站消息时调用的 Webhook。
投递生命周期(实践中)
你可能会看到的典型 MessageStatus 值:
queued→sending→sent→delivered- 失败路径:
undelivered(运营商拒绝 / 无法送达)failed(Twilio 无法发送;配置/身份验证问题)
请将 sent 视为“已交给运营商”,而不是“已送达”。
用于 Messaging 的 TwiML
入站 SMS/MMS Webhook 可以使用 TwiML 回复:
<Response>
<Message>Thanks. We received your message.</Message>
</Response>
使用 TwiML 进行同步回复;使用 REST API 处理异步工作流。
退订合规
- Twilio 会自动处理标准退订关键词(例如
STOP、UNSUBSCRIBE)。 - 用户退订后,Twilio 会阻止该发送方/服务向此接收方发送后续消息,直到用户重新订阅(
START)。 - 你的应用应当:
- 将退订视为一等状态
- 避免对被阻止的接收方反复重试
- 记录并禁止向已退订的号码发送消息
A2P 10DLC / 短码 / 免费电话号码
- 美国 A2P 10DLC:大规模通过美国长号码进行应用到个人消息传递时必须注册。未注册流量可能会被过滤或阻止。
- 短码:吞吐量高、费用昂贵、开通时间长。
- 免费电话号码:适用于美国/加拿大;通过验证可提升送达率和吞吐量。
Webhook 重试与幂等性
Twilio 会在收到非 2xx 响应时重试 webhook。你的 webhook 处理程序必须:
- 具备幂等性(按
MessageSid/SmsSid去重) - 响应迅速(快速响应;将工作加入队列)
- 具备弹性(接受请求后返回 2xx)
安装与设置
官方 Python SDK — Messaging
代码仓库: https://github.com/twilio/twilio-python
PyPI: pip install twilio · 支持: Python 3.7–3.13
from twilio.rest import Client
client = Client() # TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN from env
# Send SMS
msg = client.messages.create(
body="Hello from Python!",
from_="+15017250604",
to="+15558675309"
)
print(msg.sid)
# List recent messages
for m in client.messages.list(limit=20):
print(m.body, m.status)
来源:twilio/twilio-python — messages
Ubuntu 22.04(x86_64 / ARM64)
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jq
通过 NodeSource 安装 Node.js 20:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # v20.11.1 (or later 20.x)
npm -v # 10.2.4 (or later)
Python 3.11:
sudo apt-get install -y python3.11 python3.11-venv python3-pip
python3.11 --version
Twilio CLI 5.16.0:
npm install -g [email protected]
twilio --version
ngrok 3.13.1(可选):
curl -fsSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
| sudo gpg --dearmor -o /usr/share/keyrings/ngrok.gpg
echo "deb [signed-by=/usr/share/keyrings/ngrok.gpg] https://ngrok-agent.s3.amazonaws.com buster main" \
| sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt-get update && sudo apt-get install -y ngrok
ngrok version
Fedora 39(x86_64 / ARM64)
sudo dnf install -y curl jq nodejs python3.11 python3.11-pip
node -v
python3.11 --version
Twilio CLI:
sudo npm install -g [email protected]
twilio --version
macOS(Intel + Apple Silicon)
Homebrew:
brew update
brew install node@20 [email protected] jq
确保 PATH 设置正确:
echo 'export PATH="/opt/homebrew/opt/node@20/bin:/opt/homebrew/opt/[email protected]/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
node -v
python3.11 --version
Twilio CLI:
npm install -g [email protected]
twilio --version
ngrok:
brew install ngrok/ngrok/ngrok
ngrok version
Docker(所有平台)
docker --version
docker compose version
Twilio CLI 身份验证
交互式登录:
twilio login
或者设置环境变量(CI):
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_AUTH_TOKEN="your_auth_token"
验证:
twilio api:core:accounts:fetch --sid "$TWILIO_ACCOUNT_SID"
本地 webhook 隧道(ngrok)
ngrok http 3000
# note the https forwarding URL, e.g. https://f3a1-203-0-113-10.ngrok-free.app
将 Twilio 入站 webhook 配置为:
https://.../twilio/inbound- 状态回调配置为:
https://.../twilio/status
主要功能
发送 SMS/MMS(REST API)
- 生产环境中使用 Messaging Service(
messagingServiceSid)。 - 使用
statusCallback接收送达回执。 - 使用
provideFeedback=true获取运营商反馈(如受支持)。
Node(SMS):
import twilio from "twilio";
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const msg = await client.messages.create({
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
to: "+14155550123",
body: "Build 742 deployed. Reply STOP to opt out.",
statusCallback: "https://api.example.com/twilio/status",
provideFeedback: true,
});
console.log(msg.sid, msg.status);
Python(MMS):
from twilio.rest import Client
import os
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
msg = client.messages.create(
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
to="+14155550123",
body="Here is the incident screenshot.",
media_url=["https://cdn.example.com/incidents/INC-2048.png"],
status_callback="https://api.example.com/twilio/status",
)
print(msg.sid, msg.status)
MMS 的生产环境限制:
- 媒体文件必须可通过 HTTPS 公开访问。
- 内容类型和大小限制因运营商而异;尽可能保持图片较小(< 500KB)。
- 如果文件是私有的,请使用有效期足够长(>= 1 小时)的签名 URL。
接收入站 SMS/MMS(webhook)
入站 webhook 会接收采用表单编码的字段,例如:
From、To、BodyMessageSid(或旧版SmsSid)NumMedia、MediaUrl0、MediaContentType0,……
带签名验证的 Express 处理程序:
import express from "express";
import twilio from "twilio";
const app = express();
// Twilio sends application/x-www-form-urlencoded by default
app.use(express.urlencoded({ extended: false }));
app.post("/twilio/inbound", (req, res) => {
const signature = req.header("X-Twilio-Signature") || "";
const url = "https://api.example.com/twilio/inbound"; // must match public URL exactly
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
if (!isValid) {
return res.status(403).send("Invalid signature");
}
const from = req.body.From;
const body = (req.body.Body || "").trim();
// Fast path: respond immediately; enqueue work elsewhere
const twiml = new twilio.twiml.MessagingResponse();
if (body.toUpperCase() === "HELP") {
twiml.message("Support: https://status.example.com. Reply STOP to opt out.");
} else {
twiml.message("Received. Ticket created.");
}
res.type("text/xml").send(twiml.toString());
});
app.listen(3000);
FastAPI 处理程序:
from fastapi import FastAPI, Request, Response
from twilio.request_validator import RequestValidator
from twilio.twiml.messaging_response import MessagingResponse
import os
app = FastAPI()
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
@app.post("/twilio/inbound")
async def inbound(request: Request):
form = await request.form()
signature = request.headers.get("X-Twilio-Signature", "")
url = "https://api.example.com/twilio/inbound"
if not validator.validate(url, dict(form), signature):
return Response("Invalid signature", status_code=403)
body = (form.get("Body") or "").strip()
resp = MessagingResponse()
resp.message("Received.")
return Response(str(resp), media_type="text/xml")
送达回执(状态回调 webhook)
状态回调会接收:
MessageSidMessageStatus(queued、sent、delivered、undelivered、failed)To、FromErrorCode(例如30003)ErrorMessage(有时会提供)
Express:
app.post("/twilio/status", express.urlencoded({ extended: false }), async (req, res) => {
// Validate signature same as inbound; use exact public URL
const messageSid = req.body.MessageSid;
const status = req.body.MessageStatus;
const errorCode = req.body.ErrorCode ? Number(req.body.ErrorCode) : null;
// Idempotency: upsert by messageSid + status
// Example: write to DB with unique constraint (messageSid, status)
console.log({ messageSid, status, errorCode });
res.status(204).send();
});
运维指南:
- 将回调视为至少投递一次。
- 持久化状态转换;不要假设回调按顺序到达。
- 使用回调来:
- 将通知标记为已送达
- 在状态为
undelivered/failed时触发备用渠道(电子邮件/推送) - 按运营商/地区计算送达率指标
退订 / STOP 处理
Twilio 会自动阻止向已退订的收件人发送消息。你的系统应当:
- 检测入站消息中的退订关键词,并更新自己的联系人偏好设置。
- 禁止向已退订的收件人发送消息,以避免反复出现 21610 错误。
入站关键词处理:
const normalized = body.trim().toUpperCase();
const isStop = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"].includes(normalized);
const isStart = ["START", "YES", "UNSTOP"].includes(normalized);
if (isStop) {
// mark user opted out in your DB
}
if (isStart) {
// mark user opted in
}
发送消息时,先检查数据库中的退订状态。如果仍然被 Twilio 拦截,请处理错误代码 21610。
Messaging Services:发件人池、地理匹配、固定发件人
使用 Messaging Service 来:
- 避免将
From写死 - 安全地轮换发件人
- 启用地理匹配(本地号码)
- 启用固定发件人(对每位收件人使用一致的 From)
创建服务(CLI):
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"
向服务添加电话号码:
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdef
启用固定发件人 / 地理匹配(通过 API;CLI 的支持情况因版本而异):
twilio api:messaging:v1:services:update \
--sid YOUR_MG_SID \
--sticky-sender true \
--area-code-geomatch true
A2P 10DLC 运营检查(美国)
代码中需要实施的检查:
- 如果从美国长号码向美国号码(
+1...)发送消息:- 确保发送方关联了获批的 A2P 活动
- 确保消息内容符合活动用途(避免内容偏离)
- 监控过滤情况:
30003/30005错误和undelivered比率上升- 运营商违规和垃圾信息标记
Twilio Console 是注册状态的权威来源;在 CI/CD 中,将活动 ID 和服务 SID 作为配置处理。
短代码和免费电话号码
- 短代码:吞吐量高,可送达性最佳;申请周期较长。
- 免费电话号码:适用于美国/加拿大;要扩大规模需通过验证。
API 层面的实现完全相同;区别在于资源配置和合规要求。
Webhook 安全:签名验证和 URL 正确性
在以下情况下,签名验证容易出错:
- 使用了错误的 URL 进行验证(必须与 Twilio 使用的公共 URL 一致)
- 代理修改了 scheme/host
- 解析请求体的方式与 Twilio 预期不同
如果位于反向代理(ALB/NGINX)之后,请谨慎使用转发标头重建公共 URL,或者为每个路由硬编码已知的公共 URL。
命令参考
Twilio CLI(5.16.0)
身份验证 / 环境
twilio login
twilio profiles:list
twilio profiles:use <profile>
为单条命令设置环境变量:
TWILIO_ACCOUNT_SID=AC... TWILIO_AUTH_TOKEN=... twilio api:core:accounts:fetch --sid AC...
发送消息(CLI)
Twilio CLI 提供 twilio api:core:messages:create 命令(核心 API)。常用标志:
twilio api:core:messages:create \
--to "+14155550123" \
--from "+14155552671" \
--body "Deploy complete." \
--status-callback "https://api.example.com/twilio/status" \
--provide-feedback true \
--max-price 0.015 \
--application-sid AP0123456789abcdef0123456789abcdef
标志说明:
--to(必填):目标号码,采用 E.164 格式。--from:发送方号码(E.164 格式)。优先使用 Messaging Service。--messaging-service-sid MG...:使用服务;不能与--from同时使用。--body:SMS 文本。--media-url:可重复指定;MMS 媒体 URL。--status-callback:用于状态更新的 webhook。--provide-feedback:请求运营商反馈(并非始终可用)。--max-price:消息价格上限(USD);如果设得过低,可能导致发送失败。--application-sid:用于关联 TwiML 应用(Messaging 中较少使用)。
MMS 示例:
twilio api:core:messages:create \
--to "+14155550123" \
--messaging-service-sid YOUR_MG_SID \
--body "Photo" \
--media-url "https://cdn.example.com/a.png" \
--media-url "https://cdn.example.com/b.jpg"
获取消息:
twilio api:core:messages:fetch --sid SM0123456789abcdef0123456789abcdef
列出消息(筛选条件各不相同;常见条件如下):
twilio api:core:messages:list --limit 50
twilio api:core:messages:list --to "+14155550123" --limit 20
twilio api:core:messages:list --from "+14155552671" --limit 20
删除消息记录(较少使用;主要用于清理/测试):
twilio api:core:messages:remove --sid SM0123456789abcdef0123456789abcdef
Messaging Services
创建:
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"
更新:
twilio api:messaging:v1:services:update \
--sid YOUR_MG_SID \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status" \
--inbound-request-url "https://api.example.com/twilio/inbound" \
--inbound-method POST
列出:
twilio api:messaging:v1:services:list --limit 50
获取:
twilio api:messaging:v1:services:fetch --sid YOUR_MG_SID
服务关联的电话号码:
twilio api:messaging:v1:services:phone-numbers:list \
--service-sid YOUR_MG_SID \
--limit 50
关联号码:
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdef
从服务中移除号码:
twilio api:messaging:v1:services:phone-numbers:remove \
--service-sid YOUR_MG_SID \
--sid PN0123456789abcdef0123456789abcdef
来电电话号码(用于查找 PN SID)
列出号码:
twilio api:core:incoming-phone-numbers:list --limit 50
获取:
twilio api:core:incoming-phone-numbers:fetch --sid PN0123456789abcdef0123456789abcdef
更新号码上的 webhook(未使用 Messaging Service 入站 URL 时):
twilio api:core:incoming-phone-numbers:update \
--sid PN0123456789abcdef0123456789abcdef \
--sms-url "https://api.example.com/twilio/inbound" \
--sms-method POST \
--sms-fallback-url "https://api.example.com/twilio/fallback" \
--sms-fallback-method POST \
--status-callback "https://api.example.com/twilio/status"
配置参考
环境变量
建议使用的变量:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENTWILIO_MESSAGING_SERVICE_SID(首选)TWILIO_FROM_NUMBER(仅在不使用服务时)TWILIO_STATUS_CALLBACK_URLTWILIO_INBOUND_WEBHOOK_PUBLIC_URL(用于签名验证)
Node.js .env
路径:/srv/app/.env(Linux)或本地开发时的项目根目录。
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_AUTH_TOKEN=0123456789abcdef0123456789abcdef
TWILIO_MESSAGING_SERVICE_SID=YOUR_MG_SID
TWILIO_STATUS_CALLBACK_URL=https://api.example.com/twilio/status
TWILIO_INBOUND_WEBHOOK_PUBLIC_URL=https://api.example.com/twilio/inbound
使用 dotenv 加载:
npm install [email protected]
import "dotenv/config";
systemd 单元(生产环境)
路径:/etc/systemd/system/messaging-api.service
[Unit]
Description=Messaging API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=messaging
Group=messaging
WorkingDirectory=/srv/messaging-api
EnvironmentFile=/etc/messaging-api/env
ExecStart=/usr/bin/node /srv/messaging-api/dist/server.js
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/messaging-api /var/log/messaging-api
AmbientCapabilities=
CapabilityBoundingSet=
[Install]
WantedBy=multi-user.target
机密文件路径:/etc/messaging-api/env(chmod 600)
sudo install -m 600 -o root -g root /dev/null /etc/messaging-api/env
/etc/messaging-api/env 示例:
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_AUTH_TOKEN=0123456789abcdef0123456789abcdef
TWILIO_MESSAGING_SERVICE_SID=YOUR_MG_SID
TWILIO_STATUS_CALLBACK_URL=https://api.example.com/twilio/status
TWILIO_INBOUND_WEBHOOK_PUBLIC_URL=https://api.example.com/twilio/inbound
PORT=3000
NGINX 反向代理(webhook 端点)
路径:/etc/nginx/conf.d/messaging-api.conf
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location /twilio/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 10s;
}
}
重要提示:签名验证取决于 Twilio 使用的 URL;请确保应用使用外部 URL,而不是 http://127.0.0.1。
集成模式
模式:根据送达状态触发回退的出站通知
流程:
- 发送带有
statusCallback的 SMS。 - 收到回调时:
- 如果为
delivered:标记为成功。 - 如果为
undelivered/failed:通过 SendGrid 将邮件加入队列,或发送推送通知。
- 如果为
伪架构:
- API 服务:接受“通知用户”请求。
- 队列:存储发送任务。
- Worker:发送 Twilio 消息。
- Webhook 服务:处理状态回调并触发回退。
示例(状态回调 → SQS):
// on /twilio/status
if (status === "undelivered" || status === "failed") {
await sqs.sendMessage({
QueueUrl: process.env.FALLBACK_QUEUE_URL,
MessageBody: JSON.stringify({ messageSid, to: req.body.To, reason: req.body.ErrorCode }),
});
}
模式:支持双向通信并集成工单系统
接收短信:
- 验证签名。
- 规范化发送方(
From),并将其映射到客户。 - 在 Jira/ServiceNow/Zendesk 中创建工单。
- 通过 TwiML 回复工单 ID。
TwiML 回复示例:
<Response>
<Message>Your ticket INC-2048 is open. Reply HELP for options.</Message>
</Response>
模式:基于关键词的工作流(HELP/STOP/START + 自定义关键词)
HELP:返回支持网址和联系方式。STOP:更新内部偏好设置存储(Twilio 也会阻止发送)。- 自定义关键词:
STATUS <id>、ONCALL、ACK <incident>。
确保解析具有健壮性且不区分大小写;记录原始入站载荷以供审计。
模式:按地理区域匹配的多区域发送
- 使用单个 Messaging Service,并配置:
- 覆盖多个区域的发送方池
- 启用地理匹配
- 为满足合规要求,按收件人所在国家/地区路由:
- US/CA:免费电话号码或 A2P 10DLC 长号码
- UK:字母数字发件人 ID(如果支持)或本地号码
- India:DLT 限制(不在此处讨论;作为单独的合规模块处理)
模式:幂等发送 API
如果上游重试,你必须避免重复发送短信。
处理方式:
- 接受调用方传入的
idempotencyKey。 - 存储
idempotencyKey -> MessageSid的映射。 - 如果键已存在,则返回现有的
MessageSid。
数据库约束示例:
- 在
idempotency_key上建立唯一索引。
错误处理与故障排查
在两个层面处理错误:
- REST API 调用错误(同步)
- 状态回调错误(异步投递失败)
以下是常见的 Twilio 错误及其根本原因和修复方法。
1) 21211 — 无效的 To 号码
典型错误文本:
Twilio could not find a Channel with the specified From address
或:
The 'To' number +1415555 is not a valid phone number.
根本原因:
- 格式不是 E.164。
- 包含空格或括号。
- 国家/地区代码无效。
修复方法:
- 发送前规范化为 E.164 格式。
- 使用 libphonenumber 验证。
- 在 API 边界处拒绝请求并返回清晰的错误信息。
2) 20003 — 身份验证错误
错误文本:
Authenticate
或:
Unable to create record: Authenticate
根本原因:
TWILIO_AUTH_TOKEN错误。- 将测试凭据用于线上端点,或反之。
- Account SID 不匹配。
修复方法:
- 验证运行环境中的环境变量。
- 如果令牌泄露,请轮换令牌。
- 在 CI 中确保使用正确的密钥作用域。
3) 20429 — 超出速率限制
错误文本:
Too Many Requests
根本原因:
- 突发发送超出 Twilio 或 Messaging Service 的限制。
- API 轮询过于频繁。
修复方法:
- 实施客户端速率限制和退避策略(带抖动的指数退避)。
- 通过队列工作进程批量发送。
- 使用 Messaging Service / 短代码提高吞吐量。
4) 21610 — 收件人已选择退订
错误文本:
The message From/To pair violates a blacklist rule.
根本原因:
- 收件人回复了 STOP(或被运营商级别拦截)。
- 系统持续重试。
修复方法:
- 在数据库中禁止向已退订的收件人发送消息。
- 提供选择加入流程(START)。
- 不要重试 21610;将其视为终止错误。
5) 30003 — 目标手机无法接通 / 违反运营商规则
常见错误文本:
Unreachable destination handset
根本原因:
- 设备已关机或不在服务范围内。
- 运营商过滤。
- 号码无效或已停用。
修复方法:
- 经过有限次数的尝试后,将其视为不可重试错误。
- 启用备用渠道。
- 按运营商/国家监控错误峰值;检查 A2P 注册情况和内容。
6) 30005 — 目标手机未知
错误文本:
Unknown destination handset
根本原因:
- 号码尚未分配。
- 携号转网问题。
修复方法:
- 多次失败后将号码标记为无效。
- 请用户更新电话号码。
7) 21614 — To 不是有效的移动号码(MMS)
错误文本:
'To' number is not a valid mobile number
根本原因:
- 尝试向不支持 MMS 的号码/运营商发送 MMS。
- 固定电话。
修复方法:
- 检测是否支持 MMS;否则回退为发送带链接的 SMS。
- 如果必须事先检查,可使用 Lookup(Twilio Lookup API;需权衡成本)。
8) 21606 — From 号码不具备发送 SMS 的能力
错误文本:
The From phone number +14155552671 is not a valid, SMS-capable inbound phone number or short code for your account.
根本原因:
- 使用了仅支持语音的号码。
- 该号码不属于你的账户。
- 配置了错误的发送方。
解决方法:
- 使用配置了已验证发送方的 Messaging Service。
- 在 Console 中或通过 IncomingPhoneNumbers API 确认号码功能。
9) Webhook 签名验证失败(你的日志)
典型应用日志:
Invalid signature
根本原因:
- 使用了错误的 URL 进行验证(http 与 https 不一致、主机不匹配或路径不匹配)。
- 代理重写了请求。
- 请求体解析方式不同。
解决方法:
- 使用 Twilio 中配置的准确公网 URL 进行验证。
- 确保对表单负载使用
express.urlencoded()。 - 如果位于代理之后,请设置
app.set('trust proxy', true)并谨慎重构 URL。
10) 未触发状态回调
现象:
- 消息在控制台中显示为已送达,但你的系统始终未收到回调。
根本原因:
- 未在消息或服务上设置
statusCallback。 - 回调 URL 返回了非 2xx 状态码;Twilio 重试后最终会放弃。
- 防火墙屏蔽了 Twilio 的 IP(除非你会维护 Twilio IP 范围,否则不要使用 IP 允许列表)。
解决方法:
- 在 Messaging Service 级别设置状态回调。
- 确保端点快速返回 2xx。
- 记录请求正文和响应代码,并添加指标。
安全加固
密钥处理
- 不要将
TWILIO_AUTH_TOKEN存储在代码仓库中。 - 使用密钥管理器,并在部署时注入短期有效的密钥。
- 发生安全事件时轮换令牌;将其视为高影响凭据。
Webhook 验证(必须)
- 验证入站回调和状态回调中的
X-Twilio-Signature。 - 对无效签名返回 403。
- 仅记录最少的元数据;如果消息正文包含敏感信息,请避免记录完整内容。
TLS 和传输
- 对所有 webhook 端点强制使用 HTTPS。
- 禁用 TLS 1.0/1.1;优先使用 TLS 1.2 及更高版本。
- 如果使用 NGINX,请遵循 CIS NGINX 基准的强加密套件指导(根据组织的基准进行调整)。
请求处理
- 限制请求正文大小(防止滥用):
- Express:
express.urlencoded({ limit: "64kb", extended: false })
- Express:
- 对 webhook 端点应用速率限制(请谨慎处理:Twilio 会重试,不要阻止合法重试)。
- 使用 WAF 规则阻止明显的滥用行为,但不要依赖 IP 允许列表。
数据最小化
- 仅存储必要的数据:
MessageSid、To、From、时间戳、状态、错误代码
- 如果存储消息内容,请对静态数据加密并限制访问权限。
操作系统/服务加固(Linux)
- 使用 systemd 加固标志(参见上面的单元文件)。
- 使用非 root 用户运行。
- 遵循 CIS Linux 基准(Ubuntu 22.04/RHEL/Fedora 对应版本):
- 限制环境文件的文件权限(
chmod 600) - 审计对密钥的访问
- 适当启用自动安全更新
- 限制环境文件的文件权限(
性能调优
降低 webhook 延迟(p95)
目标:以低于 200ms 的 p95 延迟响应 Twilio webhook。
措施:
- 不要在 webhook 处理程序中同步调用外部服务。
- 将任务加入队列(SQS/Kafka/Redis),并立即返回 204/200。
- 快速解析并验证;避免繁重的日志记录。
预期效果:
- 优化前:webhook 处理程序执行数据库操作并创建工单 → p95 为 1–3s,重试会增加负载。
- 优化后:仅将任务加入队列 → p95 低于 100–200ms,重试更少,Twilio webhook 积压更少。
扩展出站发送吞吐量
- 使用带并发控制的工作进程池。
- 按 Messaging Service/发送方类型实现令牌桶速率限制。
- 为了提高吞吐量,优先选择配备合适发送方(短代码/免费电话号码)的 Messaging Service。
预期效果:
- 防止 20429 激增,并减少运营商因突发发送模式而进行的过滤。
成本优化
- 在适用时使用 Messaging Service 地理匹配功能,以降低跨区域成本。
- 如果 SMS 加链接即可满足需求,请避免使用 MMS。
- 谨慎使用
maxPrice;设置过低会增加失败率。
编码和分段
- GSM-7 与 UCS-2 会影响分段数量和成本。
- 如果发送非 GSM 字符(例如表情符号和某些带重音符号的字符),消息可能会切换到 UCS-2,并按每段 70 个字符进行分段。
缓解措施:
- 在可接受的情况下对内容进行规范化。
- 保持消息简短;通过链接提供详细信息。
高级主题
跨重试和部署的幂等性
Twilio REST messages.create 默认不具备幂等性。如果工作进程在超时后重试,可能会重复发送。
缓解措施:
- 使用存储在 DB 中的应用级幂等键。
- 超时后,通过自己的关联 ID 查询(将其存储在
statusCallback查询字符串中,或存储在body中并不安全)。更好的做法是:- 创建成功后存储作业 ID → 消息 SID 的映射
- 如果创建调用超时,通过检查 Twilio 日志来判断消息是否已创建并不可靠;建议谨慎处理“可能已发送”的情况并发出警报。
处理重复状态回调
Twilio 可能会针对同一状态发送多次回调,或者以乱序方式发送。
- 使用单调状态机存储状态转换:
queued<sending<sent<delivered- 终止失败状态:
undelivered/failed
- 如果收到
delivered后又收到undelivered,则保留两者;但如果delivered的时间戳较晚,就将其视为最终状态(由于运营商报告机制的问题,这种情况虽少见但有可能发生)。
多租户系统
如果你运营多个 Twilio 子账户:
- 存储每个租户的 Account SID/Auth Token(或使用 API Keys)。
- 按租户验证 Webhook:
- 签名验证使用该租户的 auth token
- 按
To号码路由,或为每个租户使用专用的 Webhook URL
MMS 的媒体 URL 安全
- Twilio 会从你的 URL 获取媒体文件;该 URL 必须可访问。
- 如果使用签名 URL:
- TTL 必须长于 Twilio 获取文件所需的时间(数分钟到数十分钟;建议留足余量)。
- 不要要求使用 Cookie。
- 如果需要身份验证请求头,Twilio 无法提供;请使用预签名 URL。
合规注意事项
- 不要通过未注册的 A2P 活动发送营销内容。
- 维护 HELP/STOP 回复,并根据你的政策要求,在各渠道遵守退订请求。
- 保留同意记录的审计日志(时间戳、来源,以及适用时的 IP/用户代理)。
使用示例
1) 通过 Messaging Service 发送生产环境外发 SMS 并跟踪状态(Node)
文件:
/srv/messaging-api/src/send.js
import "dotenv/config";
import twilio from "twilio";
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
export async function sendDeployNotification({ to, buildId }) {
const msg = await client.messages.create({
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
to,
body: `Build ${buildId} deployed to prod. Reply HELP for support, STOP to opt out.`,
statusCallback: process.env.TWILIO_STATUS_CALLBACK_URL,
provideFeedback: true,
});
return { sid: msg.sid, status: msg.status };
}
运行:
node -e 'import("./src/send.js").then(m=>m.sendDeployNotification({to:"+14155550123",buildId:"742"}).then(console.log))'
2) 收到 SMS → 创建工单 → 使用 TwiML 回复(FastAPI)
/srv/messaging-api/app.py
from fastapi import FastAPI, Request, Response
from twilio.request_validator import RequestValidator
from twilio.twiml.messaging_response import MessagingResponse
import os
app = FastAPI()
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
def create_ticket(from_number: str, body: str) -> str:
# Replace with real integration
return "INC-2048"
@app.post("/twilio/inbound")
async def inbound(request: Request):
form = await request.form()
signature = request.headers.get("X-Twilio-Signature", "")
url = os.environ["TWILIO_INBOUND_WEBHOOK_PUBLIC_URL"]
if not validator.validate(url, dict(form), signature):
return Response("Invalid signature", status_code=403)
from_number = form.get("From") or ""
body = (form.get("Body") or "").strip()
ticket = create_ticket(from_number, body)
resp = MessagingResponse()
resp.message(f"Created {ticket}. Reply HELP for options. Reply STOP to opt out.")
return Response(str(resp), media_type="text/xml")
运行:
python3.11 -m venv .venv && source .venv/bin/activate
pip install fastapi==0.109.2 uvicorn==0.27.1 twilio==9.4.1 python-multipart==0.0.9
uvicorn app:app --host 0.0.0.0 --port 3000
3) 状态回调 → 指标 + 备用队列入队(Express)
/srv/messaging-api/src/status.js
import express from "express";
import twilio from "twilio";
const app = express();
app.use(express.urlencoded({ extended: false, limit: "64kb" }));
app.post("/twilio/status", async (req, res) => {
const signature = req.header("X-Twilio-Signature") || "";
const url = process.env.TWILIO_STATUS_CALLBACK_PUBLIC_URL;
const ok = twilio.validateRequest(process.env.TWILIO_AUTH_TOKEN, signature, url, req.body);
if (!ok) return res.status(403).send("Invalid signature");
const { MessageSid, MessageStatus, ErrorCode, To } = req.body;
// Example: emit metric
console.log("twilio_status", { MessageSid, MessageStatus, ErrorCode });
if (MessageStatus === "failed" || MessageStatus === "undelivered") {
// enqueue fallback (pseudo)
console.log("enqueue_fallback", { to: To, messageSid: MessageSid, reason: ErrorCode });
}
res.status(204).send();
});
app.listen(3000);
4) 使用签名 URL(S3 预签名)发送 MMS,并在失败时回退为 SMS 链接
伪流程:
- 生成有效期为 2 小时的预签名 URL。
- 使用媒体 URL 发送 MMS。
- 如果状态回调返回
21614或undelivered,则发送包含链接的 SMS。
Python 代码片段:
msg = client.messages.create(
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
to="+14155550123",
body="Incident screenshot attached.",
media_url=[presigned_url], # TTL >= 2h
status_callback="https://api.example.com/twilio/status",
)
备用 SMS 正文:
MMS failed on your carrier. View: https://app.example.com/incidents/INC-2048
5) 支持退订处理的批量发送,并发执行并过滤退订用户
- 加载收件人。
- 从 DB 中筛除已退订用户。
- 以 20 的并发数发送。
- 收到 21610 时,标记用户已退订。
Node(示意):
import pLimit from "p-limit";
import twilio from "twilio";
const limit = pLimit(20);
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
async function sendOne(to) {
try {
return await client.messages.create({
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
to,
body: "Maintenance tonight 01:00-02:00 UTC. Reply STOP to opt out.",
statusCallback: process.env.TWILIO_STATUS_CALLBACK_URL,
});
} catch (e) {
const code = e?.code;
if (code === 21610) {
// mark opted out
return null;
}
throw e;
}
}
await Promise.all(recipients.map((to) => limit(() => sendOne(to))));
安装:
npm install [email protected] [email protected]
快速参考
| 任务 | 命令 / API | 关键标志 / 字段 |
|---|---|---|
| 发送 SMS(CLI) | twilio api:core:messages:create | --to、--from 或 --messaging-service-sid、--body、--status-callback、--provide-feedback、--max-price |
| 发送 MMS(CLI) | twilio api:core:messages:create | --media-url(可重复指定)、--body |
| 获取消息 | twilio api:core:messages:fetch --sid SM... | --sid |
| 列出消息 | twilio api:core:messages:list | --to、--from、--limit |
| 创建 Messaging Service | twilio api:messaging:v1:services:create | --friendly-name、--status-callback |
| 更新服务 Webhook | twilio api:messaging:v1:services:update | --inbound-request-url、--inbound-method、--status-callback |
| 将号码关联到服务 | twilio api:messaging:v1:services:phone-numbers:create | --service-sid、--phone-number-sid |
| 入站 Webhook | HTTP POST /twilio/inbound | 验证 X-Twilio-Signature,解析表单字段 |
| 状态回调 | HTTP POST /twilio/status | MessageSid、MessageStatus、ErrorCode |
图关系
依赖
twilioSDK(Node 4.23.0 / Python 9.4.1)- 公网 HTTPS 入口(NGINX/ALB/API Gateway)
- 用于幂等性和消息状态的持久化存储(Postgres/DynamoDB)
- 建议使用异步处理队列(SQS/Kafka/Redis Streams)
组合
twilio-voice(IVR 可触发 SMS 后续通知;未接来电 → SMS)twilio-verify(通过 SMS 发送 OTP;共享 Webhook 基础设施和签名验证模式)sendgrid-email(未送达时的备用渠道;统一通知服务)observability(用于监控 Webhook 延迟和送达率的指标/日志/追踪)
类似
aws-sns-sms(SMS 发送 + 送达回执,语义不同)messagebird-sms/vonage-sms(运营商路由 + Webhook 模式)firebase-cloud-messaging(送达回调的概念类似,但渠道不同)