mcpField
T
twilio-sms

twilio-sms

github.com/alphaonedev开发54 次安装
代码仓库

关于 twilio-sms

twilio-sms 是面向 OpenClaw 的 Twilio Programmable Messaging 实践指南,供构建短信和多媒体消息系统的工程师使用。内容涵盖发送、Webhook、送达跟踪、退订合规及生产环境故障处理。

主要功能

  1. 01发送 SMS 和 MMS
  2. 02接收消息并处理 TwiML 回复
  3. 03跟踪消息送达状态
  4. 04处理 STOP 退订及关键词
  5. 05验证 Webhook 签名并实现幂等
  6. 06参考 A2P 10DLC 合规要求

适用场景

  1. 01搭建客户通知消息管道
  2. 02实现双向短信支持
  3. 03诊断消息送达失败
  4. 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.0
    • twilio(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、Body
  • MessageSid(或旧版 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)

状态回调会接收:

  • MessageSid
  • MessageStatus(queued、sent、delivered、undelivered、failed)
  • To、From
  • ErrorCode(例如 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_SID
  • TWILIO_AUTH_TOKEN
  • TWILIO_MESSAGING_SERVICE_SID(首选)
  • TWILIO_FROM_NUMBER(仅在不使用服务时)
  • TWILIO_STATUS_CALLBACK_URL
  • TWILIO_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。


集成模式

模式:根据送达状态触发回退的出站通知

流程:

  1. 发送带有 statusCallback 的 SMS。
  2. 收到回调时:
    • 如果为 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 })
  • 对 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 Servicetwilio api:messaging:v1:services:create--friendly-name、--status-callback
更新服务 Webhooktwilio 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
入站 WebhookHTTP POST /twilio/inbound验证 X-Twilio-Signature,解析表单字段
状态回调HTTP POST /twilio/statusMessageSid、MessageStatus、ErrorCode

图关系

依赖
  • twilio SDK(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(送达回调的概念类似,但渠道不同)

评论

支持 Markdown