twilio-sms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesetwilio-sms
Twilio SMS
Purpose
用途
Enable OpenClaw to implement and operate Twilio Programmable Messaging (SMS/MMS) in production:
- Send SMS/MMS reliably (Messaging Services, geo-matching, sticky sender, media constraints).
- Receive inbound messages via webhooks and respond with TwiML.
- Track delivery lifecycle via status callbacks (queued/sent/delivered/undelivered/failed).
- Implement opt-out/STOP compliance and keyword workflows.
- Operate A2P 10DLC (US long code) and toll-free verification constraints.
- Debug and harden: signature validation, retries, idempotency, rate limits, carrier errors.
This skill is for engineers building messaging pipelines, customer notifications, 2-way support, and compliance-sensitive messaging.
帮助OpenClaw在生产环境中实现并运行Twilio可编程消息服务(SMS/MMS):
- 可靠发送SMS/MMS(消息服务、地理匹配、固定发送方、媒体限制)。
- 通过Webhook接收入站消息并使用TwiML回复。
- 通过状态回调跟踪送达生命周期(排队/发送/已送达/未送达/失败)。
- 实现退订/STOP合规性和关键词工作流。
- 遵循A2P 10DLC(美国长码)和免费电话验证限制。
- 调试与加固:签名验证、重试、幂等性、速率限制、运营商错误处理。
本技能适用于构建消息管道、客户通知、双向支持和合规敏感型消息的工程师。
Prerequisites
前置条件
Accounts & Twilio Console setup
账户与Twilio控制台设置
- Twilio account with Programmable Messaging enabled.
- At least one of:
- Messaging Service (recommended) with sender pool (long codes / toll-free / short code).
- A dedicated Phone Number capable of SMS/MMS.
- For US A2P 10DLC:
- Brand + Campaign registration completed in Twilio Console (Messaging → Regulatory Compliance / A2P 10DLC).
- For toll-free:
- Toll-free verification submitted/approved if sending high volume to US/CA.
- 已启用可编程消息服务的Twilio账户。
- 至少具备以下其中一项:
- 消息服务(推荐),包含发送方池(长码/免费电话/短码)。
- 支持SMS/MMS的专用电话号码。
- 针对美国A2P 10DLC:
- 已在Twilio控制台完成品牌+活动注册(消息→合规监管/A2P 10DLC)。
- 针对免费电话:
- 若向美国/加拿大发送高流量消息,需提交并通过免费电话验证。
Runtime versions (tested)
已测试的运行时版本
- Node.js 20.11.1 (LTS) + npm 10.2.4
- Python 3.11.7
- Twilio SDKs:
- (Node) 4.23.0
twilio - (Python) 9.4.1
twilio
- Web framework examples:
- Express 4.18.3
- FastAPI 0.109.2 + Uvicorn 0.27.1
- Optional tooling:
- Twilio CLI 5.16.0
- ngrok 3.13.1 (local webhook tunneling)
- Docker 25.0.3 + Compose v2 2.24.6
- Node.js 20.11.1(LTS)+ npm 10.2.4
- Python 3.11.7
- Twilio SDK:
- Node版4.23.0
twilio - Python版9.4.1
twilio
- Node版
- 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
Credentials & auth
凭证与认证
You need:
- (starts with
TWILIO_ACCOUNT_SID)AC... TWILIO_AUTH_TOKEN- One of:
- (starts with
TWILIO_MESSAGING_SERVICE_SID) preferredMG... - (E.164, e.g.
TWILIO_FROM_NUMBER)+14155552671
Store secrets in a secret manager (AWS Secrets Manager / GCP Secret Manager / Vault). For local dev, is acceptable.
.env你需要:
- (以
TWILIO_ACCOUNT_SID开头)AC... TWILIO_AUTH_TOKEN- 以下其中一项:
- (以
TWILIO_MESSAGING_SERVICE_SID开头)优先推荐MG... - (E.164格式,例如
TWILIO_FROM_NUMBER)+14155552671
将密钥存储在密钥管理器中(AWS Secrets Manager / GCP Secret Manager / Vault)。本地开发时,可使用文件。
.envNetwork & webhook requirements
网络与Webhook要求
- Public HTTPS endpoint for inbound and status callbacks.
- Must accept (Twilio default) and/or JSON depending on endpoint.
application/x-www-form-urlencoded - Validate Twilio signatures () on inbound webhooks.
X-Twilio-Signature
- 用于入站和状态回调的公共HTTPS端点。
- 必须支持(Twilio默认格式),或根据端点需求支持JSON。
application/x-www-form-urlencoded - 验证入站Webhook的Twilio签名()。
X-Twilio-Signature
Core Concepts
核心概念
Programmable Messaging objects
可编程消息对象
- Message: a single outbound or inbound SMS/MMS. Identified by (
MessageSid).SM... - Messaging Service: abstraction over senders; supports:
- sender pool
- geo-matching
- sticky sender
- smart encoding
- status callback configuration
- Status Callback: webhook invoked as message state changes.
- Inbound Webhook: webhook invoked when Twilio receives an inbound message to your number/service.
- Message:单条出站或入站SMS/MMS,由(
MessageSid)标识。SM... - Messaging Service:发送方的抽象层,支持:
- 发送方池
- 地理匹配
- 固定发送方
- 智能编码
- 状态回调配置
- Status Callback:消息状态变更时触发的Webhook。
- Inbound Webhook:Twilio收到发送至你的号码/服务的入站消息时触发的Webhook。
Delivery lifecycle (practical)
送达生命周期(实际场景)
Typical values you will see:
MessageStatus- →
queued→sending→sentdelivered - Failure paths:
- (carrier rejected / unreachable)
undelivered - (Twilio could not send; configuration/auth issues)
failed
Treat as “handed to carrier”, not “delivered”.
sent你会遇到的典型值:
MessageStatus- →
queued→sending→sentdelivered - 失败路径:
- (运营商拒绝/无法到达)
undelivered - (Twilio无法发送;配置/认证问题)
failed
注意:仅表示“已提交给运营商”,而非“已送达”。
sentTwiML for Messaging
消息用TwiML
Inbound SMS/MMS webhooks can respond with TwiML:
xml
<Response>
<Message>Thanks. We received your message.</Message>
</Response>Use TwiML for synchronous replies; use REST API for async workflows.
入站SMS/MMS Webhook可通过TwiML回复:
xml
<Response>
<Message>Thanks. We received your message.</Message>
</Response>同步回复使用TwiML;异步工作流使用REST API。
Opt-out compliance
退订合规性
- Twilio automatically handles standard opt-out keywords (e.g., ,
STOP).UNSUBSCRIBE - When a user opts out, Twilio blocks further messages from that sender/service to that recipient until they opt back in ().
START - Your app should:
- treat opt-out as a first-class state
- avoid retry storms on blocked recipients
- log and suppress sends to opted-out numbers
- Twilio自动处理标准退订关键词(如、
STOP)。UNSUBSCRIBE - 用户退订后,Twilio会阻止该发送方/服务向该收件人发送进一步消息,直到用户重新订阅()。
START - 你的应用应:
- 将退订作为核心状态处理
- 避免向被阻止的收件人重复发送
- 记录并抑制向已退订号码发送消息
A2P 10DLC / short codes / toll-free
A2P 10DLC / 短码 / 免费电话
- US A2P 10DLC: required for application-to-person messaging over US long codes at scale. Unregistered traffic may be filtered or blocked.
- Short codes: high throughput, expensive, long provisioning.
- Toll-free: good for US/CA; verification improves deliverability and throughput.
- 美国A2P 10DLC:通过美国长码大规模发送应用到用户消息的必填项。未注册流量可能被过滤或阻止。
- 短码:高吞吐量、成本高、配置周期长。
- 免费电话:适用于美国/加拿大;验证可提升送达率和吞吐量。
Webhook retries and idempotency
Webhook重试与幂等性
Twilio retries webhooks on non-2xx responses. Your webhook handlers must be:
- idempotent (dedupe by /
MessageSid)SmsSid - fast (respond quickly; enqueue work)
- resilient (return 2xx once accepted)
Twilio会对非2xx响应的Webhook进行重试。你的Webhook处理程序必须:
- 幂等(通过/
MessageSid去重)SmsSid - 快速(快速响应;将工作加入队列)
- resilient(接收后立即返回2xx)
Installation & Setup
安装与设置
Official Python SDK — Messaging
官方Python SDK — 消息服务
Repository: https://github.com/twilio/twilio-python
PyPI: · Supported: Python 3.7–3.13
PyPI:
pip install twiliopython
from twilio.rest import Client
client = Client() # TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN from envpython
from twilio.rest import Client
client = Client() # 从环境变量读取TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKENSend SMS
发送SMS
msg = client.messages.create(
body="Hello from Python!",
from_="+15017250604",
to="+15558675309"
)
print(msg.sid)
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)
Source: [twilio/twilio-python — messages](https://github.com/twilio/twilio-python/blob/main/twilio/rest/api/v2010/account/message/__init__.py)for m in client.messages.list(limit=20):
print(m.body, m.status)
来源:[twilio/twilio-python — messages](https://github.com/twilio/twilio-python/blob/main/twilio/rest/api/v2010/account/message/__init__.py)Ubuntu 22.04 (x86_64 / ARM64)
Ubuntu 22.04(x86_64 / ARM64)
bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jqNode.js 20 via NodeSource:
bash
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:
bash
sudo apt-get install -y python3.11 python3.11-venv python3-pip
python3.11 --versionTwilio CLI 5.16.0:
bash
npm install -g twilio-cli@5.16.0
twilio --versionngrok 3.13.1 (optional):
bash
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 versionbash
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jq通过NodeSource安装Node.js 20:
bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # v20.11.1(或更高20.x版本)
npm -v # 10.2.4(或更高版本)安装Python 3.11:
bash
sudo apt-get install -y python3.11 python3.11-venv python3-pip
python3.11 --version安装Twilio CLI 5.16.0:
bash
npm install -g twilio-cli@5.16.0
twilio --version安装ngrok 3.13.1(可选):
bash
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 versionFedora 39 (x86_64 / ARM64)
Fedora 39(x86_64 / ARM64)
bash
sudo dnf install -y curl jq nodejs python3.11 python3.11-pip
node -v
python3.11 --versionTwilio CLI:
bash
sudo npm install -g twilio-cli@5.16.0
twilio --versionbash
sudo dnf install -y curl jq nodejs python3.11 python3.11-pip
node -v
python3.11 --version安装Twilio CLI:
bash
sudo npm install -g twilio-cli@5.16.0
twilio --versionmacOS (Intel + Apple Silicon)
macOS(Intel + Apple Silicon)
Homebrew:
bash
brew update
brew install node@20 python@3.11 jqEnsure PATH:
bash
echo 'export PATH="/opt/homebrew/opt/node@20/bin:/opt/homebrew/opt/python@3.11/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
node -v
python3.11 --versionTwilio CLI:
bash
npm install -g twilio-cli@5.16.0
twilio --versionngrok:
bash
brew install ngrok/ngrok/ngrok
ngrok version通过Homebrew安装:
bash
brew update
brew install node@20 python@3.11 jq配置PATH:
bash
echo 'export PATH="/opt/homebrew/opt/node@20/bin:/opt/homebrew/opt/python@3.11/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
node -v
python3.11 --version安装Twilio CLI:
bash
npm install -g twilio-cli@5.16.0
twilio --version安装ngrok:
bash
brew install ngrok/ngrok/ngrok
ngrok versionDocker (all platforms)
Docker(全平台)
bash
docker --version
docker compose versionbash
docker --version
docker compose versionTwilio CLI authentication
Twilio CLI认证
Interactive login:
bash
twilio loginOr set env vars (CI):
bash
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_AUTH_TOKEN="your_auth_token"Verify:
bash
twilio api:core:accounts:fetch --sid "$TWILIO_ACCOUNT_SID"交互式登录:
bash
twilio login或设置环境变量(CI场景):
bash
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_AUTH_TOKEN="your_auth_token"验证:
bash
twilio api:core:accounts:fetch --sid "$TWILIO_ACCOUNT_SID"Local webhook tunneling (ngrok)
本地Webhook隧道(ngrok)
bash
ngrok http 3000bash
ngrok http 3000note the https forwarding URL, e.g. https://f3a1-203-0-113-10.ngrok-free.app
记录HTTPS转发URL,例如https://f3a1-203-0-113-10.ngrok-free.app
Configure Twilio inbound webhook to:
- `https://.../twilio/inbound`
- Status callback to:
- `https://.../twilio/status`
---
配置Twilio入站Webhook为:
- `https://.../twilio/inbound`
- 状态回调为:
- `https://.../twilio/status`
---Key Capabilities
核心功能
Send SMS/MMS (REST API)
发送SMS/MMS(REST API)
- Use Messaging Service () for production.
messagingServiceSid - Use for delivery receipts.
statusCallback - Use for carrier feedback (where supported).
provideFeedback=true
Node (SMS):
javascript
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):
python
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)Production constraints for MMS:
- Media must be publicly reachable via HTTPS.
- Content-type and size limits vary by carrier; keep images small (< 500KB) when possible.
- Use signed URLs with sufficient TTL (>= 1 hour) if private.
- 生产环境使用消息服务()。
messagingServiceSid - 使用获取送达回执。
statusCallback - (支持的场景)使用获取运营商反馈。
provideFeedback=true
Node.js(SMS示例):
javascript
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示例):
python
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。
Receive inbound SMS/MMS (webhook)
接收入站SMS/MMS(Webhook)
Inbound webhook receives form-encoded fields like:
- ,
From,ToBody - (or
MessageSidlegacy)SmsSid - ,
NumMedia,MediaUrl0, ...MediaContentType0
Express handler with signature validation:
javascript
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 handler:
python
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会接收表单编码字段,例如:
- ,
From,ToBody - (或旧版
MessageSid)SmsSid - ,
NumMedia,MediaUrl0, ...MediaContentType0
带签名验证的Express处理程序:
javascript
import express from "express";
import twilio from "twilio";
const app = express();
// Twilio默认发送application/x-www-form-urlencoded格式
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"; // 必须与公开URL完全匹配
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();
// 快速路径:立即回复;将其他工作加入队列
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处理程序:
python
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")Delivery receipts (status callback webhook)
送达回执(状态回调Webhook)
Status callback receives:
MessageSid- (
MessageStatus,queued,sent,delivered,undelivered)failed - ,
ToFrom - (e.g.,
ErrorCode)30003 - (sometimes present)
ErrorMessage
Express:
javascript
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();
});Operational guidance:
- Treat callbacks as at-least-once delivery.
- Persist state transitions; do not assume ordering.
- Use callbacks to:
- mark notifications delivered
- trigger fallback channels (email/push) on /
undeliveredfailed - compute deliverability metrics by carrier/region
状态回调会接收:
MessageSid- (
MessageStatus,queued,sent,delivered,undelivered)failed - ,
ToFrom - (例如
ErrorCode)30003 - (有时存在)
ErrorMessage
Express示例:
javascript
app.post("/twilio/status", express.urlencoded({ extended: false }), async (req, res) => {
// 签名验证方式与入站Webhook相同;使用精确的公开URL
const messageSid = req.body.MessageSid;
const status = req.body.MessageStatus;
const errorCode = req.body.ErrorCode ? Number(req.body.ErrorCode) : null;
// 幂等性:通过messageSid + status进行更新插入
// 示例:写入带唯一约束(messageSid, status)的数据库
console.log({ messageSid, status, errorCode });
res.status(204).send();
});操作指南:
- 回调至少会触发一次。
- 持久化状态转换;不要假设顺序。
- 使用回调:
- 标记通知已送达
- 收到/
undelivered时触发备用渠道(邮件/推送)failed - 按运营商/地区计算送达率指标
Opt-out / STOP handling
退订/STOP处理
Twilio blocks messages to opted-out recipients automatically. Your system should:
- Detect opt-out keywords on inbound messages and update your own contact preferences.
- Suppress sends to opted-out recipients to avoid repeated 21610 errors.
Inbound keyword handling:
javascript
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
}When sending, pre-check your DB opt-out state. If you still hit Twilio block, handle error code .
21610Twilio会自动阻止向已退订收件人发送消息。你的系统应:
- 检测入站消息中的退订关键词并更新内部联系人偏好。
- 抑制向已退订收件人发送消息,避免重复出现21610错误。
入站关键词处理示例:
javascript
const normalized = body.trim().toUpperCase();
const isStop = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"].includes(normalized);
const isStart = ["START", "YES", "UNSTOP"].includes(normalized);
if (isStop) {
// 在数据库中标记用户已退订
}
if (isStart) {
// 在数据库中标记用户已重新订阅
}发送消息前,预先检查数据库中的退订状态。如果仍触发Twilio阻止,处理错误码。
21610Messaging Services: sender pools, geo-matching, sticky sender
消息服务:发送方池、地理匹配、固定发送方
Use a Messaging Service to:
- avoid hardcoding
From - rotate senders safely
- enable geo-matching (local presence)
- enable sticky sender (consistent From per recipient)
Create a service (CLI):
bash
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"Add a phone number to the service:
bash
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdefEnable sticky sender / geo-match (via API; CLI coverage varies by version):
bash
twilio api:messaging:v1:services:update \
--sid YOUR_MG_SID \
--sticky-sender true \
--area-code-geomatch true使用消息服务可:
- 避免硬编码号码
From - 安全轮换发送方
- 启用地理匹配(本地号码适配)
- 启用固定发送方(对同一收件人使用一致的发送方)
创建服务(CLI方式):
bash
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"向服务添加电话号码:
bash
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdef启用固定发送方/地理匹配(通过API;CLI支持因版本而异):
bash
twilio api:messaging:v1:services:update \
--sid YOUR_MG_SID \
--sticky-sender true \
--area-code-geomatch trueA2P 10DLC operational checks (US)
A2P 10DLC操作检查(美国)
What to enforce in code:
- If sending to US numbers () from US long codes:
+1...- ensure the sender is associated with an approved A2P campaign
- ensure message content matches campaign use case (avoid content drift)
- Monitor filtering:
- rising /
30003and30005ratesundelivered - carrier violations and spam flags
- rising
Twilio Console is the source of truth for registration state; in CI/CD, treat campaign IDs and service SIDs as config.
代码中需强制执行的规则:
- 若向美国号码()发送消息且使用美国长码:
+1...- 确保发送方关联已批准的A2P活动
- 确保消息内容与活动用例匹配(避免内容偏离)
- 监控过滤情况:
- /
30003错误和30005率上升undelivered - 运营商违规和垃圾邮件标记
Twilio控制台是注册状态的权威来源;在CI/CD中,将活动ID和服务SID视为配置项。
Short codes and toll-free
短码与免费电话
- Short code: high throughput, best deliverability; long lead time.
- Toll-free: good for US/CA; verification required for scale.
Implementation is identical at API level; difference is provisioning and compliance.
- 短码:高吞吐量、最佳送达率;配置周期长。
- 免费电话:适用于美国/加拿大;大规模发送需验证。
API层面实现方式相同;差异在于配置和合规要求。
Webhook security: signature validation and URL correctness
Webhook安全:签名验证与URL正确性
Signature validation is brittle if:
- you validate against the wrong URL (must match the public URL Twilio used)
- you have proxies altering scheme/host
- you parse body differently than Twilio expects
If behind a reverse proxy (ALB/NGINX), reconstruct the public URL using forwarded headers carefully, or hardcode the known public URL per route.
以下情况会导致签名验证失败:
- 验证使用错误的URL(必须与Twilio使用的公开URL完全一致)
- 代理修改了协议/主机
- 解析请求体的方式与Twilio不同
若在反向代理(ALB/NGINX)后,需通过转发头小心重构公开URL,或为每个路由硬编码已知的公开URL。
Command Reference
命令参考
Twilio CLI (5.16.0)
Twilio CLI(5.16.0)
Authentication / environment
认证/环境配置
bash
twilio login
twilio profiles:list
twilio profiles:use <profile>Set env vars for a single command:
bash
TWILIO_ACCOUNT_SID=AC... TWILIO_AUTH_TOKEN=... twilio api:core:accounts:fetch --sid AC...bash
twilio login
twilio profiles:list
twilio profiles:use <profile>为单个命令设置环境变量:
bash
TWILIO_ACCOUNT_SID=AC... TWILIO_AUTH_TOKEN=... twilio api:core:accounts:fetch --sid AC...Send a message (CLI)
发送消息(CLI)
Twilio CLI has a command (core API). Common flags:
twilio api:core:messages:createbash
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 AP0123456789abcdef0123456789abcdefNotes on flags:
- (required): destination E.164.
--to - : sender number (E.164). Prefer Messaging Service instead.
--from - : use service; mutually exclusive with
--messaging-service-sid MG....--from - : SMS text.
--body - : repeatable; MMS media URL(s).
--media-url - : webhook for status updates.
--status-callback - : request carrier feedback (not always available).
--provide-feedback - : cap price (USD) for message; may cause failures if too low.
--max-price - : for TwiML app association (rare for Messaging).
--application-sid
MMS example:
bash
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"Fetch message:
bash
twilio api:core:messages:fetch --sid SM0123456789abcdef0123456789abcdefList messages (filters vary; core ones):
bash
twilio api:core:messages:list --limit 50
twilio api:core:messages:list --to "+14155550123" --limit 20
twilio api:core:messages:list --from "+14155552671" --limit 20Delete message record (rare; mostly for cleanup/testing):
bash
twilio api:core:messages:remove --sid SM0123456789abcdef0123456789abcdefTwilio CLI提供命令(核心API)。常用参数:
twilio api:core:messages:createbash
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参数说明:
- (必填):目标号码,E.164格式。
--to - :发送方号码(E.164格式)。优先使用消息服务。
--from - :使用消息服务;与
--messaging-service-sid MG...互斥。--from - :SMS文本内容。
--body - :可重复使用;MMS媒体文件URL。
--media-url - :状态更新Webhook。
--status-callback - :请求运营商反馈(并非所有场景支持)。
--provide-feedback - :消息价格上限(美元);设置过低可能导致发送失败。
--max-price - :关联TwiML应用(消息服务场景中很少使用)。
--application-sid
MMS示例:
bash
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"获取消息详情:
bash
twilio api:core:messages:fetch --sid SM0123456789abcdef0123456789abcdef列出消息(过滤条件多样;核心条件):
bash
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删除消息记录(罕见;主要用于清理/测试):
bash
twilio api:core:messages:remove --sid SM0123456789abcdef0123456789abcdefMessaging Services
消息服务
Create:
bash
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"Update:
bash
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 POSTList:
bash
twilio api:messaging:v1:services:list --limit 50Fetch:
bash
twilio api:messaging:v1:services:fetch --sid YOUR_MG_SIDPhone numbers attached to a service:
bash
twilio api:messaging:v1:services:phone-numbers:list \
--service-sid YOUR_MG_SID \
--limit 50Attach a number:
bash
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdefRemove a number from service:
bash
twilio api:messaging:v1:services:phone-numbers:remove \
--service-sid YOUR_MG_SID \
--sid PN0123456789abcdef0123456789abcdef创建服务:
bash
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"更新服务:
bash
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列出服务:
bash
twilio api:messaging:v1:services:list --limit 50获取服务详情:
bash
twilio api:messaging:v1:services:fetch --sid YOUR_MG_SID查看服务关联的电话号码:
bash
twilio api:messaging:v1:services:phone-numbers:list \
--service-sid YOUR_MG_SID \
--limit 50关联电话号码:
bash
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdef从服务中移除电话号码:
bash
twilio api:messaging:v1:services:phone-numbers:remove \
--service-sid YOUR_MG_SID \
--sid PN0123456789abcdef0123456789abcdefIncoming phone numbers (to find PN SIDs)
呼入电话号码(查找PN SID)
List numbers:
bash
twilio api:core:incoming-phone-numbers:list --limit 50Fetch:
bash
twilio api:core:incoming-phone-numbers:fetch --sid PN0123456789abcdef0123456789abcdefUpdate webhook on a number (if not using Messaging Service inbound URL):
bash
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"列出号码:
bash
twilio api:core:incoming-phone-numbers:list --limit 50获取号码详情:
bash
twilio api:core:incoming-phone-numbers:fetch --sid PN0123456789abcdef0123456789abcdef更新号码的Webhook(若未使用消息服务入站URL):
bash
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"Configuration Reference
配置参考
Environment variables
环境变量
Recommended variables:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN- (preferred)
TWILIO_MESSAGING_SERVICE_SID - (only if not using service)
TWILIO_FROM_NUMBER TWILIO_STATUS_CALLBACK_URL- (for signature validation)
TWILIO_INBOUND_WEBHOOK_PUBLIC_URL
推荐配置的变量:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN- (优先推荐)
TWILIO_MESSAGING_SERVICE_SID - (仅当不使用消息服务时配置)
TWILIO_FROM_NUMBER TWILIO_STATUS_CALLBACK_URL- (用于签名验证)
TWILIO_INBOUND_WEBHOOK_PUBLIC_URL
Node.js .env
.envNode.js .env
文件
.envPath: (Linux) or project root for local dev.
/srv/app/.envdotenv
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/inboundLoad with :
dotenvbash
npm install dotenv@16.4.5javascript
import "dotenv/config";路径:Linux为,本地开发为项目根目录。
/srv/app/.envdotenv
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使用加载:
dotenvbash
npm install dotenv@16.4.5javascript
import "dotenv/config";systemd unit (production)
systemd单元文件(生产环境)
Path:
/etc/systemd/system/messaging-api.serviceini
[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.targetSecrets file path: (chmod 600)
/etc/messaging-api/envbash
sudo install -m 600 -o root -g root /dev/null /etc/messaging-api/envExample :
/etc/messaging-api/envbash
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路径:
/etc/systemd/system/messaging-api.serviceini
[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密钥文件路径:(权限设置为600)
/etc/messaging-api/envbash
sudo install -m 600 -o root -g root /dev/null /etc/messaging-api/env/etc/messaging-api/envbash
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=3000NGINX reverse proxy (webhook endpoints)
NGINX反向代理(Webhook端点)
Path:
/etc/nginx/conf.d/messaging-api.confnginx
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;
}
}Important: signature validation depends on the URL Twilio used; ensure your app uses the external URL, not .
http://127.0.0.1路径:
/etc/nginx/conf.d/messaging-api.confnginx
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.1Integration Patterns
集成模式
Pattern: Outbound notifications with delivery-driven fallback
模式:带送达驱动备用方案的出站通知
Pipeline:
- Send SMS with .
statusCallback - On callback:
- if : mark success.
delivered - if /
undelivered: enqueue email via SendGrid or push notification.failed
- if
Pseudo-architecture:
- API service: accepts “notify user” request.
- Queue: stores send jobs.
- Worker: sends Twilio message.
- Webhook service: processes status callbacks and triggers fallback.
Example (status callback → SQS):
javascript
// 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 }),
});
}流程:
- 发送带的SMS。
statusCallback - 回调处理:
- 若为:标记成功。
delivered - 若为/
undelivered:通过SendGrid或推送通知加入邮件队列。failed
- 若为
伪架构:
- API服务:接收“通知用户”请求。
- 队列:存储发送任务。
- 工作进程:发送Twilio消息。
- Webhook服务:处理状态回调并触发备用方案。
示例(状态回调→SQS):
javascript
// 在/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 }),
});
}Pattern: Two-way support with ticketing
模式:带工单系统的双向支持
Inbound SMS:
- Validate signature.
- Normalize sender () and map to customer.
From - Create ticket in Jira/ServiceNow/Zendesk.
- Reply with ticket ID via TwiML.
Example TwiML reply:
xml
<Response>
<Message>Your ticket INC-2048 is open. Reply HELP for options.</Message>
</Response>入站SMS:
- 验证签名。
- 标准化发送方()并映射到客户。
From - 在Jira/ServiceNow/Zendesk中创建工单。
- 通过TwiML回复工单ID。
TwiML回复示例:
xml
<Response>
<Message>Your ticket INC-2048 is open. Reply HELP for options.</Message>
</Response>Pattern: Keyword-based workflows (HELP/STOP/START + custom)
模式:基于关键词的工作流(HELP/STOP/START + 自定义)
- : return support URL and contact.
HELP - : update internal preference store (Twilio also blocks).
STOP - Custom keywords: ,
STATUS <id>,ONCALL.ACK <incident>
Ensure parsing is robust and case-insensitive; log raw inbound payload for audit.
- :返回支持URL和联系方式。
HELP - :更新内部偏好存储(Twilio也会阻止发送)。
STOP - 自定义关键词:,
STATUS <id>,ONCALL。ACK <incident>
确保解析逻辑健壮且不区分大小写;记录原始入站载荷用于审计。
Pattern: Multi-region sending with geo-matching
模式:带地理匹配的多区域发送
- Use a single Messaging Service with:
- sender pool across regions
- geo-matching enabled
- For compliance, route by recipient country:
- US/CA: toll-free or A2P 10DLC long code
- UK: alphanumeric sender ID (if supported) or local number
- India: DLT constraints (outside scope here; treat as separate compliance module)
- 使用单个消息服务,包含:
- 跨区域的发送方池
- 启用地理匹配
- 合规性要求:按收件人国家路由:
- 美国/加拿大:免费电话或A2P 10DLC长码
- 英国:字母数字发送方ID(若支持)或本地号码
- 印度:DLT约束(超出本文范围;视为独立合规模块)
Pattern: Idempotent send API
模式:幂等发送API
If your upstream retries, you must avoid duplicate SMS.
Approach:
- Accept from caller.
idempotencyKey - Store mapping .
idempotencyKey -> MessageSid - If key exists, return existing .
MessageSid
Example DB constraint:
- Unique index on .
idempotency_key
若上游系统重试,必须避免重复发送SMS。
实现方式:
- 接收调用方传入的。
idempotencyKey - 存储的映射。
idempotencyKey -> MessageSid - 若密钥已存在,返回已有的。
MessageSid
数据库约束示例:
- 为创建唯一索引。
idempotency_key
Error Handling & Troubleshooting
错误处理与故障排除
Handle errors at two layers:
- REST API call errors (synchronous)
- Status callback errors (asynchronous delivery failures)
Below are common Twilio errors with root cause and fix.
在两个层面处理错误:
- REST API调用错误(同步)
- 状态回调错误(异步送达失败)
以下是常见Twilio错误的根因和修复方案。
1) 21211 — invalid To
number
To1) 21211 — 无效To
号码
ToError text (typical):
Twilio could not find a Channel with the specified From addressor:
The 'To' number +1415555 is not a valid phone number.Root causes:
- Not E.164 formatted.
- Contains spaces/parentheses.
- Invalid country code.
Fix:
- Normalize to E.164 before sending.
- Validate with libphonenumber.
- Reject at API boundary with clear error.
典型错误文本:
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 — authentication error
2) 20003 — 认证错误
Error text:
Authenticateor:
Unable to create record: AuthenticateRoot causes:
- Wrong .
TWILIO_AUTH_TOKEN - Using test credentials against live endpoint or vice versa.
- Account SID mismatch.
Fix:
- Verify env vars in runtime.
- Rotate token if leaked.
- In CI, ensure correct secret scope.
错误文本:
Authenticate或:
Unable to create record: Authenticate根因:
- 错误。
TWILIO_AUTH_TOKEN - 测试凭证用于生产环境或反之。
- 账户SID不匹配。
修复:
- 验证运行时环境变量。
- 泄露后轮换令牌。
- 在CI中确保正确的密钥范围。
3) 20429 — rate limit exceeded
3) 20429 — 超出速率限制
Error text:
Too Many RequestsRoot causes:
- Bursty sends exceeding Twilio or Messaging Service limits.
- Excessive API polling.
Fix:
- Implement client-side rate limiting and backoff (exponential with jitter).
- Batch sends via queue workers.
- Use Messaging Service / short code for higher throughput.
错误文本:
Too Many Requests根因:
- 突发发送超出Twilio或消息服务限制。
- 过度API轮询。
修复:
- 实现客户端速率限制和退避(指数退避加抖动)。
- 通过队列工作进程批量发送。
- 使用消息服务/短码提升吞吐量。
4) 21610 — recipient opted out
4) 21610 — 收件人已退订
Error text:
The message From/To pair violates a blacklist rule.Root causes:
- Recipient replied STOP (or carrier-level block).
- Your system keeps retrying.
Fix:
- Suppress sends to opted-out recipients in your DB.
- Provide opt-in flow (START).
- Do not retry 21610; treat as terminal.
错误文本:
The message From/To pair violates a blacklist rule.根因:
- 收件人回复STOP(或运营商级阻止)。
- 系统持续重试。
修复:
- 在数据库中抑制向已退订收件人发送消息。
- 提供重新订阅流程(START)。
- 不要重试21610错误;视为终端错误。
5) 30003 — Unreachable destination handset / carrier violation
5) 30003 — 目标手机无法到达/运营商违规
Error text (common):
Unreachable destination handsetRoot causes:
- Device off/out of coverage.
- Carrier filtering.
- Invalid or deactivated number.
Fix:
- Treat as non-retryable after limited attempts.
- Trigger fallback channel.
- Monitor spikes by carrier/country; check A2P registration and content.
常见错误文本:
Unreachable destination handset根因:
- 设备关机/无信号。
- 运营商过滤。
- 号码无效或已停用。
修复:
- 有限次数尝试后视为不可重试。
- 触发备用渠道。
- 按运营商/国家监控峰值;检查A2P注册和内容。
6) 30005 — Unknown destination handset
6) 30005 — 未知目标手机
Error text:
Unknown destination handsetRoot causes:
- Number not assigned.
- Porting issues.
Fix:
- Mark number invalid after repeated failures.
- Ask user to update phone number.
错误文本:
Unknown destination handset根因:
- 号码未分配。
- 号码转移问题。
修复:
- 多次失败后标记号码无效。
- 要求用户更新电话号码。
7) 21614 — 'To' is not a valid mobile number (MMS)
7) 21614 — 'To'号码不支持MMS
Error text:
'To' number is not a valid mobile numberRoot causes:
- Attempting MMS to a number/carrier that doesn’t support MMS.
- Landline.
Fix:
- Detect MMS capability; fallback to SMS with link.
- Use Lookup (Twilio Lookup API) if you must preflight (cost tradeoff).
错误文本:
'To' number is not a valid mobile number根因:
- 尝试向不支持MMS的号码/运营商发送MMS。
- 固定电话。
修复:
- 检测MMS能力;回退到带链接的SMS。
- 若必须预检查,使用Twilio Lookup API(权衡成本)。
8) 21606 — From number not capable of sending SMS
8) 21606 — 发送方号码无法发送SMS
Error text:
The From phone number +14155552671 is not a valid, SMS-capable inbound phone number or short code for your account.Root causes:
- Using a voice-only number.
- Number not owned by your account.
- Wrong sender configured.
Fix:
- Use Messaging Service with verified senders.
- Confirm number capabilities in Console or via IncomingPhoneNumbers API.
错误文本:
The From phone number +14155552671 is not a valid, SMS-capable inbound phone number or short code for your account.根因:
- 使用仅支持语音的号码。
- 号码不属于你的账户。
- 发送方配置错误。
修复:
- 使用带已验证发送方的消息服务。
- 在控制台或通过IncomingPhoneNumbers API确认号码能力。
9) Webhook signature failures (your logs)
9) Webhook签名失败(日志中)
Typical app log:
Invalid signatureRoot causes:
- Validating against wrong URL (http vs https, host mismatch, path mismatch).
- Proxy rewrites.
- Body parsing differences.
Fix:
- Validate against the exact public URL configured in Twilio.
- Ensure is used for form payloads.
express.urlencoded() - If behind proxy, set and reconstruct URL carefully.
app.set('trust proxy', true)
典型应用日志:
Invalid signature根因:
- 验证使用错误URL(http与https、主机不匹配、路径不匹配)。
- 代理重写。
- 请求体解析方式差异。
修复:
- 使用Twilio中配置的精确公开URL进行验证。
- 确保表单载荷使用解析。
express.urlencoded() - 若在代理后,设置并小心重构URL。
app.set('trust proxy', true)
10) Status callback not firing
10) 状态回调未触发
Symptoms:
- Message shows delivered in console but your system never receives callback.
Root causes:
- not set on message or service.
statusCallback - Callback URL returns non-2xx; Twilio retries then gives up.
- Firewall blocks Twilio IPs (don’t IP allowlist unless you maintain Twilio ranges).
Fix:
- Set status callback at Messaging Service level.
- Ensure endpoint returns 2xx quickly.
- Log request bodies and response codes; add metrics.
症状:
- 控制台显示消息已送达,但系统从未收到回调。
根因:
- 消息或服务未设置。
statusCallback - 回调URL返回非2xx;Twilio重试后放弃。
- 防火墙阻止Twilio IP(除非维护Twilio IP范围,否则不要IP白名单)。
修复:
- 在消息服务层面设置状态回调。
- 确保端点快速返回2xx。
- 记录请求体和响应码;添加指标。
Security Hardening
安全加固
Secrets handling
密钥处理
- Do not store in repo.
TWILIO_AUTH_TOKEN - Use secret manager + short-lived deployment injection.
- Rotate tokens on incident; treat as high-impact credential.
- 不要在代码库中存储。
TWILIO_AUTH_TOKEN - 使用密钥管理器+短期部署注入。
- 事件发生后轮换令牌;视为高影响凭证。
Webhook validation (mandatory)
Webhook验证(强制要求)
- Validate for inbound and status callbacks.
X-Twilio-Signature - Reject invalid signatures with 403.
- Log minimal metadata; avoid logging full message bodies if sensitive.
- 验证入站和状态回调的。
X-Twilio-Signature - 无效签名返回403。
- 记录最小元数据;敏感内容避免记录完整消息体。
TLS and transport
TLS与传输
- Enforce HTTPS for all webhook endpoints.
- Disable TLS 1.0/1.1; prefer TLS 1.2+.
- If using NGINX, follow CIS NGINX Benchmark guidance for strong ciphers (adapt to your org baseline).
- 所有Webhook端点强制使用HTTPS。
- 禁用TLS 1.0/1.1;优先使用TLS 1.2+。
- 若使用NGINX,遵循CIS NGINX基准指南配置强加密套件(适配组织基线)。
Request handling
请求处理
- Limit request body size (protect against abuse):
- Express:
express.urlencoded({ limit: "64kb", extended: false })
- Express:
- Apply rate limiting on webhook endpoints (careful: Twilio retries; don’t block legitimate retries).
- Use WAF rules to block obvious abuse, but do not rely on IP allowlists.
- 限制请求体大小(防止滥用):
- Express:
express.urlencoded({ limit: "64kb", extended: false })
- Express:
- Webhook端点应用速率限制(注意:Twilio会重试;不要阻止合法重试)。
- 使用WAF规则阻止明显滥用,但不要依赖IP白名单。
Data minimization
数据最小化
- Store only what you need:
- ,
MessageSid,To, timestamps, status, error codesFrom
- If storing message content, encrypt at rest and restrict access.
- 仅存储必要数据:
- ,
MessageSid,To, 时间戳, 状态, 错误码From
- 若存储消息内容,静态加密并限制访问。
OS/service hardening (Linux)
OS/服务加固(Linux)
- systemd hardening flags (see unit file above).
- Run as non-root user.
- Follow CIS Linux Benchmarks (Ubuntu 22.04 / RHEL/Fedora equivalents):
- restrict file permissions on env files ()
chmod 600 - audit access to secrets
- enable automatic security updates where appropriate
- restrict file permissions on env files (
- systemd加固标志(见上方单元文件)。
- 以非root用户运行。
- 遵循CIS Linux基准(Ubuntu 22.04 / RHEL/Fedora等效版本):
- 限制环境文件权限()
chmod 600 - 审计密钥访问
- 适当启用自动安全更新
- 限制环境文件权限(
Performance Tuning
性能调优
Reduce webhook latency (p95)
降低Webhook延迟(p95)
Goal: respond to Twilio webhooks in < 200ms p95.
Actions:
- Do not call external services synchronously in webhook handler.
- Enqueue work (SQS/Kafka/Redis) and return 204/200 immediately.
- Pre-parse and validate quickly; avoid heavy logging.
Expected impact:
- Before: webhook handler does DB + ticket creation → 1–3s p95, retries increase load.
- After: enqueue only → <100–200ms p95, fewer retries, lower Twilio webhook backlog.
目标:Twilio Webhook响应时间p95 < 200ms。
措施:
- Webhook处理程序中不要同步调用外部服务。
- 将工作加入队列(SQS/Kafka/Redis)并立即返回204/200。
- 快速预解析和验证;避免大量日志。
预期影响:
- 优化前:Webhook处理程序执行DB+工单创建→p95 1–3s,重试增加负载。
- 优化后:仅加入队列→p95 <100–200ms,重试减少,Twilio Webhook积压降低。
Throughput scaling for outbound sends
出站发送吞吐量扩展
- Use worker pool with concurrency control.
- Implement token bucket rate limiting per Messaging Service / sender type.
- Prefer Messaging Service with appropriate sender (short code/toll-free) for higher throughput.
Expected impact:
- Prevents 20429 spikes and reduces carrier filtering due to bursty patterns.
- 使用带并发控制的工作进程池。
- 为消息服务/发送方类型实现令牌桶速率限制。
- 优先使用合适发送方(短码/免费电话)的消息服务提升吞吐量。
预期影响:
- 防止20429错误峰值,减少突发模式导致的运营商过滤。
Cost optimization
成本优化
- Use Messaging Service geo-matching to reduce cross-region costs where applicable.
- Avoid MMS when SMS + link suffices.
- Use carefully; too low increases failures.
maxPrice
- 使用消息服务地理匹配减少跨区域成本(适用场景)。
- 能用SMS+链接替代时避免MMS。
- 谨慎使用;设置过低会增加失败率。
maxPrice
Encoding and segmentation
编码与分段
- GSM-7 vs UCS-2 affects segment count and cost.
- If you send non-GSM characters (e.g., emoji, some accented chars), messages may switch to UCS-2 and segment at 70 chars.
Mitigation:
- Normalize content where acceptable.
- Keep messages short; move details to links.
- GSM-7与UCS-2影响分段数量和成本。
- 若发送非GSM字符(如表情符号、部分带重音字符),消息可能切换为UCS-2并按70字符分段。
缓解措施:
- 可接受的情况下标准化内容。
- 保持消息简短;详细内容放在链接中。
Advanced Topics
高级主题
Idempotency across retries and deploys
重试与部署中的幂等性
Twilio REST is not idempotent by default. If your worker retries after a timeout, you may double-send.
messages.createMitigations:
- Use an application-level idempotency key stored in DB.
- On timeout, query by your own correlation ID (store in query string or in
statusCallbackis not safe). Better:body- store job ID → message SID mapping once created
- if create call times out, attempt to detect if message was created by checking Twilio logs is unreliable; prefer conservative “may have sent” handling and alert.
Twilio REST 默认不幂等。若工作进程超时后重试,可能重复发送。
messages.create缓解方案:
- 使用应用层面的幂等密钥存储在数据库中。
- 超时后,通过查询自己的关联ID检测消息是否已创建(不要在查询字符串或
statusCallback中存储;不可靠)。更优方案:body- 存储任务ID→消息SID的映射
- 创建调用超时后,检测消息是否已创建不可靠;优先采用保守的“可能已发送”处理并告警。
Handling duplicate status callbacks
处理重复状态回调
Twilio may send multiple callbacks for the same status or out of order.
- Store transitions with monotonic state machine:
- <
queued<sending<sentdelivered - terminal failures: /
undeliveredfailed
- If you receive after
delivered, keep both but treat delivered as final if timestamp is later (rare but possible due to carrier reporting quirks).undelivered
Twilio可能为同一状态发送多个回调或乱序发送。
- 使用单调状态机存储转换:
- <
queued<sending<sentdelivered - 终端失败:/
undeliveredfailed
- 若先收到后收到
undelivered,保留两者但以时间戳较晚的delivered为最终状态(罕见,因运营商报告 quirks)。delivered
Multi-tenant systems
多租户系统
If you operate multiple Twilio subaccounts:
- Store per-tenant Account SID/Auth Token (or use API Keys).
- Validate webhooks per tenant:
- signature validation uses the tenant’s auth token
- route by number or by dedicated webhook URL per tenant
To
若运营多个Twilio子账户:
- 按租户存储账户SID/认证令牌(或使用API密钥)。
- 按租户验证Webhook:
- 签名验证使用租户的认证令牌
- 按号码或专用Webhook URL路由
To
Media URL security for MMS
MMS媒体URL安全
- Twilio fetches media from your URL; it must be reachable.
- If using signed URLs:
- TTL must exceed Twilio fetch window (minutes to tens of minutes; be conservative).
- Do not require cookies.
- If you require auth headers, Twilio cannot provide them; use pre-signed URLs.
- Twilio会从你的URL获取媒体;必须可访问。
- 若使用签名URL:
- 有效期必须超过Twilio获取窗口(数分钟到数十分钟;保守设置)。
- 不要要求Cookie。
- 若要求认证头,Twilio无法提供;使用预签名URL。
Compliance gotchas
合规陷阱
- Do not send marketing content from unregistered A2P campaigns.
- Maintain HELP/STOP responses and honor opt-out across channels if your policy requires it.
- Keep audit logs for consent (timestamp, source, IP/user agent if applicable).
- 不要从未注册的A2P活动发送营销内容。
- 若政策要求,跨渠道维护HELP/STOP响应并尊重退订。
- 保留同意审计日志(时间戳、来源、IP/用户代理,若适用)。
Usage Examples
使用示例
1) Production outbound SMS with Messaging Service + status tracking (Node)
1) 生产环境带消息服务+状态跟踪的出站SMS(Node.js)
Files:
/srv/messaging-api/src/send.js
javascript
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 };
}Run:
bash
node -e 'import("./src/send.js").then(m=>m.sendDeployNotification({to:"+14155550123",buildId:"742"}).then(console.log))'文件:
/srv/messaging-api/src/send.js
javascript
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 };
}运行:
bash
node -e 'import("./src/send.js").then(m=>m.sendDeployNotification({to:"+14155550123",buildId:"742"}).then(console.log))'2) Inbound SMS → create ticket → reply with TwiML (FastAPI)
2) 入站SMS→创建工单→TwiML回复(FastAPI)
/srv/messaging-api/app.py
python
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")Run:
bash
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/srv/messaging-api/app.py
python
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:
# 替换为实际集成逻辑
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")运行:
bash
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 30003) Status callback → metrics + fallback enqueue (Express)
3) 状态回调→指标+备用队列(Express)
/srv/messaging-api/src/status.js
javascript
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);/srv/messaging-api/src/status.js
javascript
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;
// 示例:发送指标
console.log("twilio_status", { MessageSid, MessageStatus, ErrorCode });
if (MessageStatus === "failed" || MessageStatus === "undelivered") {
// 加入备用队列(伪代码)
console.log("enqueue_fallback", { to: To, messageSid: MessageSid, reason: ErrorCode });
}
res.status(204).send();
});
app.listen(3000);4) MMS with signed URL (S3 presigned) + fallback to SMS link
4) 带签名URL(S3预签名)的MMS+SMS链接回退
Pseudo-flow:
- Generate presigned URL valid for 2 hours.
- Send MMS with media URL.
- If status callback returns or
21614, send SMS with link.undelivered
Python snippet:
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",
)Fallback SMS body:
MMS failed on your carrier. View: https://app.example.com/incidents/INC-2048伪流程:
- 生成有效期2小时的预签名URL。
- 发送带媒体URL的MMS。
- 若状态回调返回或
21614,发送带链接的SMS。undelivered
Python代码片段:
python
msg = client.messages.create(
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
to="+14155550123",
body="Incident screenshot attached.",
media_url=[presigned_url], # 有效期≥2h
status_callback="https://api.example.com/twilio/status",
)回退SMS内容:
MMS failed on your carrier. View: https://app.example.com/incidents/INC-20485) Opt-out aware bulk send with concurrency + suppression
5) 带并发+抑制的退订感知批量发送
- Load recipients.
- Filter out opted-out in DB.
- Send with concurrency 20.
- On 21610, mark opted-out.
Node (sketch):
javascript
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))));Install:
bash
npm install p-limit@5.0.0 twilio@4.23.0- 加载收件人。
- 过滤数据库中已退订的收件人。
- 并发数20发送。
- 收到21610错误时标记为已退订。
Node.js(示例):
javascript
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) {
// 标记为已退订
return null;
}
throw e;
}
}
await Promise.all(recipients.map((to) => limit(() => sendOne(to))));安装依赖:
bash
npm install p-limit@5.0.0 twilio@4.23.0Quick Reference
快速参考
| Task | Command / API | Key flags / fields |
|---|---|---|
| Send SMS (CLI) | | |
| Send MMS (CLI) | | |
| Fetch message | | |
| List messages | | |
| Create Messaging Service | | |
| Update service webhooks | | |
| Attach number to service | | |
| Inbound webhook | HTTP POST | Validate |
| Status callback | HTTP POST | |
| 任务 | 命令/API | 关键参数/字段 |
|---|---|---|
| 发送SMS(CLI) | | |
| 发送MMS(CLI) | | |
| 获取消息详情 | | |
| 列出消息 | | |
| 创建消息服务 | | |
| 更新服务Webhook | | |
| 关联号码到服务 | | |
| 入站Webhook | HTTP POST | 验证 |
| 状态回调 | HTTP POST | |
Graph Relationships
依赖关系
DEPENDS_ON
DEPENDS_ON
- SDK (Node 4.23.0 / Python 9.4.1)
twilio - Public HTTPS ingress (NGINX/ALB/API Gateway)
- Persistent store for idempotency and message state (Postgres/DynamoDB)
- Queue for async processing (SQS/Kafka/Redis Streams) recommended
- SDK(Node 4.23.0 / Python 9.4.1)
twilio - 公共HTTPS入口(NGINX/ALB/API网关)
- 幂等性和消息状态持久化存储(Postgres/DynamoDB)
- 推荐用于异步处理的队列(SQS/Kafka/Redis Streams)
COMPOSES
COMPOSES
- (IVR can trigger SMS follow-ups; missed-call → SMS)
twilio-voice - (OTP via SMS; share webhook infra and signature validation patterns)
twilio-verify - (fallback channel on undelivered; unified notification service)
sendgrid-email - (metrics/logging/tracing for webhook latency and delivery rates)
observability
- (IVR可触发SMS跟进;未接来电→SMS)
twilio-voice - (SMS发送OTP;共享Webhook基础设施和签名验证模式)
twilio-verify - (未送达时的备用渠道;统一通知服务)
sendgrid-email - (Webhook延迟和送达率的指标/日志/追踪)
observability
SIMILAR_TO
SIMILAR_TO
- (SMS sending + delivery receipts, different semantics)
aws-sns-sms - /
messagebird-sms(carrier routing + webhook patterns)vonage-sms - (delivery callbacks conceptually similar, different channel)
firebase-cloud-messaging
- (SMS发送+送达回执,语义不同)
aws-sns-sms - /
messagebird-sms(运营商路由+Webhook模式)vonage-sms - (送达回调概念类似,渠道不同)
firebase-cloud-messaging