关于 twilio-whatsapp
twilio-whatsapp 是一项面向 OpenClaw 的技能,供工程师通过 Twilio 实施和运维 WhatsApp Business 消息服务。内容涉及消息类型、媒体、Webhook 处理、同意管理、重试与生产环境投递约束。
主要功能
- 01会话消息与模板消息
- 02媒体附件处理
- 03入站与状态 Webhook
- 04加入、退订与抑制管理
- 05支持安全重试的回调处理
- 06速率限制与成本控制
适用场景
- 01在会话窗口外发送已批准模板
- 02处理入站消息与投递状态更新
- 03落实同意记录与 STOP 请求
- 04在 WhatsApp 投递前验证媒体
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ alphaonedev/ openclaw-graph --skill twilio-whatsapp安装后在 Claude Code 中输入/twilio-whatsapp确认已加载。
详细介绍
twilio-whatsapp
用途
让 OpenClaw 能够在生产环境中实现并运营 Twilio WhatsApp Business 消息功能:
- 发送模板消息(预先批准)和会话消息(24 小时客户服务窗口)。
- 附加媒体(图片/文档/音频),并遵守正确的 MIME 类型和大小限制。
- 接收并验证Webhook(收到的消息 + 消息状态回调)。
- 实施选择加入/选择退出和合规控制(处理 STOP、记录同意、遵守地区限制)。
- 在 Twilio 生产环境限制下可靠运行:速率限制、重试、幂等性、错误代码,以及通过 Messaging Services 控制成本。
对工程师的实际价值:交付一个可观测、合规、能抵御 Webhook 重试,并且可安全扩展且故障模式可预测的 WhatsApp 消息子系统。
先决条件
账户与 Twilio 端设置
- 已启用 WhatsApp 发送方的 Twilio 账户:
- 可选择 Twilio Sandbox for WhatsApp(仅用于开发)或连接到 Twilio 的 WhatsApp Business Profile(用于生产)。
- 一个 Twilio Messaging Service(生产环境推荐),其中包含:
- 已关联的 WhatsApp 发送方(例如,
whatsapp:+14155238886或你已批准的 WA 号码)。 - 已配置状态回调 URL(可选,但推荐)。
- 已关联的 WhatsApp 发送方(例如,
- 已在 Meta Business Manager 中批准的 WhatsApp 模板(通过 Twilio Console 模板管理器)。
本地工具版本(固定)
- Node.js 20.11.1(LTS)或 18.19.1(LTS)
- Python 3.11.8 或 3.12.2
- Twilio 辅助库:
twilio(Node)4.23.0twilio(Python)9.0.5
- Twilio CLI 5.16.0(用于诊断;运行时不需要)
- ngrok 3.13.1(用于本地 Webhook 测试)
身份验证与密钥
选择以下其中一种:
-
API Key(推荐):
TWILIO_API_KEY_SID(以SK...开头)TWILIO_API_KEY_SECRETTWILIO_ACCOUNT_SID(以AC...开头)
-
Account SID + Auth Token:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN
将密钥存储在:
- Kubernetes:
Secret+ 挂载的环境变量 - AWS:Secrets Manager + IRSA
- GCP:Secret Manager + Workload Identity
- 本地开发:
.env(切勿提交)
网络与 Webhook 要求
- 用于 Webhook 的公共 HTTPS 端点(大多数生产环境中 Twilio 都要求使用 HTTPS)。
- Twilio Webhook IP 范围并不固定;应使用 X-Twilio-Signature 进行验证,而不是使用 IP 允许列表。
- 确保端点能够处理重试和乱序传递。
核心概念
WhatsApp 消息类型(从 Twilio 的角度)
-
模板消息(超出 24 小时窗口):
- 必须使用预先批准的模板。
- 用于通知、OTP、配送更新等。
- 在 Twilio 中,模板通常通过 Content API(首选)发送,或根据账户配置通过模板集成发送。
-
会话消息(在 24 小时窗口内):
- 允许发送自由格式的文本/媒体(须遵守 WhatsApp 政策)。
- 24 小时窗口从用户向你发送消息时开始。
-
媒体消息:
- WhatsApp 支持图片、文档、音频和视频,但有相应限制。
- Twilio 通过
MediaUrl(可公开访问的 URL)发送媒体;某些流程也会使用 Twilio 托管的媒体。
标识符与地址
- Twilio 中的 WhatsApp 地址使用
whatsapp:前缀:From:whatsapp:+14155238886To:whatsapp:+14155550123
- Messaging Service SID:
MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - Message SID:
SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Webhook
两种主要的 Webhook 类别:
-
收到的消息 Webhook(用户向你发送消息时):
- Twilio 会发送一个 HTTP 请求,其中包含表单编码的参数,如
From、To、Body、NumMedia、MediaUrl0等。
- Twilio 会发送一个 HTTP 请求,其中包含表单编码的参数,如
-
消息状态回调(传递生命周期):
- 状态值:
queued、sent、delivered、read、failed、undelivered - 对于非 2xx 响应,Twilio 会采用退避策略进行重试。
- 状态值:
幂等性与重试
- Twilio 可能会重试 Webhook;处理程序必须具备幂等性。
- 状态回调可能会乱序到达(例如,先收到
delivered,再收到read;或在临时状态之后收到failed)。 - 使用
MessageSid+MessageStatus+ 时间戳进行去重。
合规:选择加入/选择退出
- WhatsApp 要求用户选择加入;必须保存同意证明。
- STOP 处理:
- 对于 SMS,Twilio 内置 STOP 处理功能。
- 对于 WhatsApp,必须实现选择退出关键词并遵守用户选择(例如,“STOP”、“UNSUBSCRIBE”)。
- 维护一个以 E.164 电话号码为键的抑制列表。
安装与设置
官方 Python SDK — WhatsApp
仓库: https://github.com/twilio/twilio-python
PyPI: pip install twilio · 支持版本: Python 3.7–3.13
from twilio.rest import Client
client = Client()
# Send WhatsApp message (Sandbox: from_ = 'whatsapp:+14155238886')
msg = client.messages.create(
body="Your order is confirmed!",
from_="whatsapp:+14155238886",
to="whatsapp:+15558675309"
)
# Send template message (approved HSM)
msg = client.messages.create(
from_="whatsapp:+14155238886",
to="whatsapp:+15558675309",
content_sid="HX...", # pre-approved template SID
content_variables='{"1":"Alice","2":"12345"}'
)
来源:twilio/twilio-python — messages
Ubuntu 22.04 LTS (x86_64)
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jq
通过 NodeSource 安装 Node.js 20.11.1:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v
npm -v
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 -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
| sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb 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)
sudo dnf install -y curl jq nodejs python3 python3-virtualenv
node -v
python3 --version
Twilio CLI:
sudo npm install -g [email protected]
twilio --version
ngrok:
sudo dnf install -y ngrok
ngrok version
macOS 14 (Sonoma) — Intel + Apple Silicon
Homebrew:
brew update
brew install node@20 [email protected] jq ngrok/ngrok/ngrok
node -v
python3 --version
ngrok version
Twilio CLI:
npm install -g [email protected]
twilio --version
身份验证设置(CLI + 环境变量)
Twilio CLI 登录(写入 ~/.twilio-cli/config.json):
twilio login
运行时环境变量(建议使用 API Key):
export TWILIO_ACCOUNT_SID="AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1"
export TWILIO_API_KEY_SID="SK3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8"
export TWILIO_API_KEY_SECRET="a_very_long_secret_value"
export TWILIO_MESSAGING_SERVICE_SID="MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5"
本地 .env(示例路径:/srv/whatsapp/.env):
TWILIO_ACCOUNT_SID=AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1
TWILIO_API_KEY_SID=SK3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8
TWILIO_API_KEY_SECRET=a_very_long_secret_value
TWILIO_MESSAGING_SERVICE_SID=MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5
PUBLIC_BASE_URL=https://wa.example.com
主要功能
发送会话消息(文本 + 媒体)
- 使用 Twilio Programmable Messaging
MessagesAPI。 - 确保
To和From包含whatsapp:前缀。 - 建议使用
MessagingServiceSid,而不是将From硬编码,以便路由并支持未来扩展发件方。
Node (twilio 4.23.0):
import twilio from "twilio";
const client = twilio(
process.env.TWILIO_API_KEY_SID,
process.env.TWILIO_API_KEY_SECRET,
{ accountSid: process.env.TWILIO_ACCOUNT_SID }
);
export async function sendSessionText(toE164, body) {
const msg = await client.messages.create({
to: `whatsapp:${toE164}`,
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
body,
statusCallback: `${process.env.PUBLIC_BASE_URL}/twilio/status`
});
return msg.sid;
}
Python (twilio 9.0.5):
import os
from twilio.rest import Client
client = Client(
os.environ["TWILIO_API_KEY_SID"],
os.environ["TWILIO_API_KEY_SECRET"],
os.environ["TWILIO_ACCOUNT_SID"],
)
def send_session_media(to_e164: str, body: str, media_url: str) -> str:
msg = client.messages.create(
to=f"whatsapp:{to_e164}",
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
body=body,
media_url=[media_url],
status_callback=f"{os.environ['PUBLIC_BASE_URL']}/twilio/status",
)
return msg.sid
运行限制:
- 媒体 URL 必须可供 Twilio 公开访问(私有 S3 URL 除非使用预签名 URL)。
- 如果发送媒体,请在发送前验证内容类型和大小,以减少失败。
发送模板消息(会话窗口之外)
生产环境建议:如果账户支持,请使用 Twilio Content API(又称“Content Templates”)。这可将模板定义与代码解耦,并支持本地化和变量。
使用 Content API 发送(通过 contentSid 调用 Messages API)
Node:
export async function sendTemplate(toE164) {
const msg = await client.messages.create({
to: `whatsapp:${toE164}`,
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
contentSid: "HXb5b62575e6e4ff6129ad7c8efe1f983e",
contentVariables: JSON.stringify({
"1": "Ava",
"2": "Order #18473",
"3": "2026-02-21"
}),
statusCallback: `${process.env.PUBLIC_BASE_URL}/twilio/status`
});
return msg.sid;
}
Python:
import json
def send_template(to_e164: str) -> str:
msg = client.messages.create(
to=f"whatsapp:{to_e164}",
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
content_sid="HXb5b62575e6e4ff6129ad7c8efe1f983e",
content_variables=json.dumps({"1": "Ava", "2": "Order #18473", "3": "2026-02-21"}),
status_callback=f"{os.environ['PUBLIC_BASE_URL']}/twilio/status",
)
return msg.sid
说明:
- 根据 Twilio Content 变量索引规则,
contentVariables的键为字符串"1"、"2"等。 - 模板审批和类别(实用、营销、身份验证)会影响可送达性和政策合规性。
接收 WhatsApp 入站消息(Webhook 处理程序)
Twilio 默认发送 application/x-www-form-urlencoded。
Express (Node):
import express from "express";
import twilio from "twilio";
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post("/twilio/inbound", (req, res) => {
const signature = req.header("X-Twilio-Signature") || "";
const url = `${process.env.PUBLIC_BASE_URL}/twilio/inbound`;
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN, // validateRequest requires Auth Token, not API key secret
signature,
url,
req.body
);
if (!isValid) return res.status(403).send("invalid signature");
const from = req.body.From; // e.g. "whatsapp:+14155550123"
const body = req.body.Body || "";
const numMedia = parseInt(req.body.NumMedia || "0", 10);
// Idempotency: inbound messages have MessageSid
const messageSid = req.body.MessageSid;
// TODO: persist inbound event, dedupe by MessageSid
// TODO: implement opt-out keywords
res.type("text/xml").send("<Response></Response>");
});
app.listen(3000);
重要提示:validateRequest 需要 TWILIO_AUTH_TOKEN。即使使用 API Keys 进行 REST 调用,Webhook 签名验证仍需要 Auth Token。请单独存储并限制其访问权限。
FastAPI (Python):
import os
from fastapi import FastAPI, Request, Response
from twilio.request_validator import RequestValidator
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 = f"{os.environ['PUBLIC_BASE_URL']}/twilio/inbound"
if not validator.validate(url, dict(form), signature):
return Response(content="invalid signature", status_code=403)
message_sid = form.get("MessageSid")
from_ = form.get("From")
body = form.get("Body", "")
return Response(content="<Response></Response>", media_type="text/xml")
消息状态回调(delivered/read/failed)
为每条消息或在 Messaging Service 级别配置 statusCallback。
Twilio 会通过 POST 发送以下字段:
MessageSid、MessageStatus、To、From、ErrorCode、ErrorMessage
处理程序要求:
- 始终快速返回 2xx(约 2 秒以内)。
- 将处理任务加入作业队列(SQS、Pub/Sub、Kafka)。
- 按
(MessageSid, MessageStatus)去重。
示例(Express):
app.post("/twilio/status", (req, res) => {
// Validate signature same as inbound
const { MessageSid, MessageStatus, ErrorCode, ErrorMessage } = req.body;
// Persist status transition; do not assume ordering
// If failed/undelivered, capture ErrorCode + ErrorMessage for triage
res.sendStatus(204);
});
选择加入管理与抑制
实现以下功能:
- 记录同意信息(时间戳、来源、适用时的 IP/用户代理、证明文本)。
- 抑制列表:
- 如果用户发送“STOP”、“UNSUBSCRIBE”、“CANCEL”、“END”、“QUIT” → 标记为已抑制。
- 如果用户发送“START”、“UNSTOP”、“SUBSCRIBE” → 取消抑制(仅在政策允许时)。
关键词解析示例:
STOP_WORDS = {"stop", "unsubscribe", "cancel", "end", "quit"}
START_WORDS = {"start", "unstop", "subscribe"}
def classify_opt(body: str) -> str | None:
t = body.strip().lower()
if t in STOP_WORDS:
return "STOP"
if t in START_WORDS:
return "START"
return None
执行要求:
- 发送任何出站消息前,检查抑制列表。
- 对于模板消息,还需检查同意是否仍有效以及地区特定规则。
媒体处理(上传、验证和发送)
Twilio 要求 MediaUrl 可供 Twilio 访问。常见做法:
- 将媒体存储在 S3 中,并使用短时效的预签名 URL(例如,15 分钟)。
- 生成 URL 前验证 MIME 类型和大小。
限制可能有所不同;请采用保守的限制:
- 图片:<= 5 MB
- 文档:<= 100 MB(PDF),但为保证可靠性,请采用更小的限制
- 音频/视频:除非已确认你的账户/区域适用的限制,否则应限制为 <= 16 MB
示例:生成预签名 URL(AWS SDK v3,Node):
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: "us-east-1" });
export async function presign(bucket, key) {
const cmd = new GetObjectCommand({ Bucket: bucket, Key: key });
return await getSignedUrl(s3, cmd, { expiresIn: 900 });
}
Webhook 重放防护和幂等性
存储已处理的 webhook ID:
- 入站:
MessageSid - 状态:
MessageSid + ":" + MessageStatus
使用带 TTL(例如,7 天)的快速存储(Redis),以防止重复处理。
Redis 示例(伪代码):
SETNX twilio:inbound:SM... 1 EX 604800
SETNX twilio:status:SM...:delivered 1 EX 604800
命令参考
Twilio CLI(5.16.0)
身份验证
twilio login
标志:
--profile <name>:将凭据存储在指定名称的配置文件中--username <AC...>:账户 SID--password <auth_token>:身份验证令牌(省略时以交互方式输入)
列出消息
twilio api:core:messages:list
相关标志:
--to <string>:按收件人筛选(例如,whatsapp:+14155550123)--from <string>:按发件人筛选--date-sent <YYYY-MM-DD>:按日期筛选--page-size <int>:默认值为 50--limit <int>:返回的最大记录数--properties <csv>:选择字段(取决于 CLI)--output json|tsv|csv:输出格式(取决于 CLI)
示例:
twilio api:core:messages:list --to "whatsapp:+14155550123" --limit 20 --output json | jq .
获取消息
twilio api:core:messages:fetch --sid SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
标志:
--sid <SM...>:必填
创建消息(通过 Content API 发送会话消息或模板消息)
twilio api:core:messages:create \
--to "whatsapp:+14155550123" \
--messaging-service-sid MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5 \
--body "Hello from production"
所有相关标志(API/CLI 通常支持;是否可用可能因 CLI 版本而异):
--to <string>:必填--from <string>:使用 Messaging Service 时可选--messaging-service-sid <MG...>:推荐--body <string>:消息文本--media-url <url>:可重复指定以发送多个媒体--status-callback <url>:状态 webhook--max-price <decimal>:价格上限(取决于渠道)--provide-feedback <boolean>:请求提供送达反馈(取决于运营商)--attempt <int>/--validity-period <int>:取决于渠道;可能不适用于 WhatsApp--content-sid <HX...>:Content API 模板标识符--content-variables <json>:变量的 JSON 字符串
模板消息发送示例:
twilio api:core:messages:create \
--to "whatsapp:+14155550123" \
--messaging-service-sid MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5 \
--content-sid HXb5b62575e6e4ff6129ad7c8efe1f983e \
--content-variables '{"1":"Ava","2":"Order #18473","3":"2026-02-21"}'
使用 ngrok 在本地调试 webhook
ngrok http 3000
将 HTTPS 转发 URL 复制到:
- Twilio Console → Messaging → WhatsApp Sender / Messaging Service → 入站 webhook
- 状态回调 URL
配置参考
Node 服务配置
路径: /srv/whatsapp/config/whatsapp.production.toml
[twilio]
account_sid = "AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1"
messaging_service_sid = "MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5"
public_base_url = "https://wa.example.com"
[webhooks]
inbound_path = "/twilio/inbound"
status_path = "/twilio/status"
validate_signatures = true
signature_auth_token_env = "TWILIO_AUTH_TOKEN"
[opt]
stop_keywords = ["stop","unsubscribe","cancel","end","quit"]
start_keywords = ["start","unstop","subscribe"]
suppression_ttl_days = 3650
[media]
max_image_bytes = 5242880
max_doc_bytes = 26214400
presign_ttl_seconds = 900
allowed_mime_prefixes = ["image/","application/pdf"]
路径: /srv/whatsapp/.env(权限 0600)
TWILIO_ACCOUNT_SID=AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1
TWILIO_API_KEY_SID=SK3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8
TWILIO_API_KEY_SECRET=a_very_long_secret_value
TWILIO_AUTH_TOKEN=your_auth_token_for_signature_validation
TWILIO_MESSAGING_SERVICE_SID=MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5
PUBLIC_BASE_URL=https://wa.example.com
systemd 单元(Linux)
路径: /etc/systemd/system/whatsapp.service
[Unit]
Description=WhatsApp Messaging Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=whatsapp
Group=whatsapp
WorkingDirectory=/srv/whatsapp
EnvironmentFile=/srv/whatsapp/.env
ExecStart=/usr/bin/node /srv/whatsapp/dist/server.js
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/whatsapp /var/log/whatsapp
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Kubernetes 部署片段
路径: /srv/whatsapp/deploy/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: whatsapp
namespace: messaging
spec:
replicas: 6
selector:
matchLabels:
app: whatsapp
template:
metadata:
labels:
app: whatsapp
spec:
containers:
- name: whatsapp
image: ghcr.io/acme/whatsapp:2026.02.21
ports:
- containerPort: 3000
env:
- name: TWILIO_ACCOUNT_SID
valueFrom:
secretKeyRef:
name: twilio
key: account_sid
- name: TWILIO_API_KEY_SID
valueFrom:
secretKeyRef:
name: twilio
key: api_key_sid
- name: TWILIO_API_KEY_SECRET
valueFrom:
secretKeyRef:
name: twilio
key: api_key_secret
- name: TWILIO_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: twilio
key: auth_token
- name: TWILIO_MESSAGING_SERVICE_SID
valueFrom:
secretKeyRef:
name: twilio
key: messaging_service_sid
- name: PUBLIC_BASE_URL
value: "https://wa.example.com"
readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "1Gi"
集成模式
模式:用于 webhook 处理的 API 服务 + 队列
- Webhook 处理程序验证签名并将事件加入队列。
- Worker 消费事件、更新数据库并触发下游操作。
示例流水线:
- Twilio →
POST /twilio/inbound - API → 发布到 Kafka 主题
twilio.inbound.v1 - Worker → 解析、应用退订规则,并将请求路由到对话服务
- 对话服务 → 决定响应内容 → 通过 Twilio Messages API 发送
Kafka 消息架构(JSON):
{
"event_type": "twilio_inbound",
"received_at": "2026-02-21T18:22:11.123Z",
"message_sid": "SMd2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7",
"from": "whatsapp:+14155550123",
"to": "whatsapp:+14155238886",
"body": "Where is my order?",
"num_media": 0,
"raw": { "Body": "Where is my order?", "ProfileName": "Ava" }
}
模式:与 Twilio Verify 组合使用(通过 WhatsApp 发送 OTP)
- 在支持的情况下,通过 WhatsApp 渠道使用 Verify V2。
- 如果 WhatsApp 发送失败,则回退到 SMS。
流程:
- 尝试通过 WhatsApp 使用 Verify
- 如果错误表明渠道不可用,则回退到 SMS
重要的运维提示:Verify 有自己的速率限制和欺诈防护;除非你愿意承担合规和滥用风险,否则不要自行通过会话消息实现 OTP。
模式:搭配 SendGrid,实现电子邮件回退
- 如果 WhatsApp 模板发送失败,错误为
63016(超出窗口/需要模板),或用户已选择退订:- 通过 SendGrid 动态模板发送事务性电子邮件。
- 使用统一的通知日志,记录各渠道的尝试和结果。
模式:使用 Studio 快速迭代,使用 API 处理核心流程
- 使用 Twilio Studio 处理低风险流程(常见问题、路由)。
- 使用 REST Trigger API 从后端启动 Studio 流程。
- 对于高发送量、需审计的流程,在代码中实现 WhatsApp 消息发送。
错误处理与故障排查
从两个层面处理 Twilio 错误:
- 发送时的 REST API 错误
- 带有
ErrorCode/ErrorMessage的 Webhook 状态回调
至少记录以下信息:
MessageSid、To、From、MessagingServiceSid、ErrorCode、ErrorMessage、HTTP 状态码、Twilio 请求 ID(如果存在,则记录X-Twilio-Request-Id)
1) 21211 — To 号码无效
常见错误文本:
TwilioRestException: The 'To' number +1415555012 is not a valid phone number.
根本原因:
- 号码不是 E.164 格式、缺少数字,或缺少
whatsapp:前缀。
解决方法:
- 将号码规范化为 E.164 格式并添加前缀:
To=whatsapp:+14155550123。 - 调用 Twilio 前,使用 libphonenumber 验证号码。
2) 20003 — 身份验证失败
错误文本:
Authenticate
The AccountSid and AuthToken combination you have provided is invalid.
根本原因:
- 凭据错误、混用了 API 密钥密文和身份验证令牌,或账户 SID 错误。
解决方法:
- 使用 API 密钥调用 REST API 时,使用
(apiKeySid, apiKeySecret, accountSid)。 - 验证 Webhook 时,使用
TWILIO_AUTH_TOKEN。 - 轮换已泄露的凭据,并检查环境变量是否正确注入。
3) 20429 — 超出速率限制
错误文本:
Too Many Requests
Rate limit exceeded
根本原因:
- 短时间内突发发送、并发 API 调用过多,或达到账户级限制。
解决方法:
- 实现客户端限速(令牌桶)。
- 批量发送,并使用带并发控制的队列。
- 在适用时优先使用 Messaging Service,并分散到多个发送方。
4) 30003 — 目标不可达/运营商违规(常见于 SMS,也可能出现在混合服务中)
错误文本:
Message Delivery - Carrier violation
根本原因:
- 运营商过滤、目标无效或路由受阻。
解决方法:
- 对于 WhatsApp,确保目标号码支持 WhatsApp,且用户已选择接收消息。
- 确认发送方已获准向目标地区发送消息。
- 检查状态回调中的
ErrorCode和 Twilio Console 日志。
5) 63016 — 需要 WhatsApp 模板/超出会话窗口
常见的状态回调错误消息:
63016: Failed to send message because you are outside the allowed window.
根本原因:
- 在 24 小时窗口之外尝试发送非模板会话消息。
解决方法:
- 使用已获批准的模板(Content API)重新开启对话。
- 按用户跟踪其最近一次收到的入站消息时间戳。
6) 63018 — 未找到模板/模板未获批准/不匹配
常见错误:
63018: Content template not found or not approved for use.
根本原因:
contentSid错误、模板未获批准,或未为 WhatsApp 发送方启用。
解决方法:
- 在 Twilio Console 中确认模板的批准状态。
- 确保模板关联的是正确的 WhatsApp 发送方/企业。
- 在发布代码更新前部署模板变更。
7) 21610 — 用户已选择退订(更常见于 SMS;仍应统一处理抑制)
错误文本:
Attempt to send to unsubscribed recipient
根本原因:
- 收件人已选择退订(SMS 由 Twilio 管理),或已列入你自己的 WhatsApp 抑制名单。
解决方法:
- 对于 WhatsApp,发送前检查你的抑制名单。
- 提供重新订阅途径并记录用户同意。
8) Webhook 签名验证失败
服务日志:
invalid signature
根本原因:
PUBLIC_BASE_URL不匹配(ngrok URL 已更改)、身份验证令牌错误、代理重写 URL,或缺少表单解析。
解决方法:
- 确保验证时使用的确切 URL 与 Twilio 请求的 URL 一致(协议/主机/路径)。
- 在 Express 中,在处理程序之前使用
express.urlencoded({ extended: false })。 - 如果服务位于反向代理后,确保
PUBLIC_BASE_URL与外部 URL(而非内部 URL)一致。
9) 媒体获取失败
状态回调可能显示:
30007: Carrier violation
或者 Twilio 控制台显示媒体获取错误。
根本原因:
- 媒体 URL 无法公开访问、预签名 URL 已过期、被 WAF 拦截,或 TLS 配置错误。
解决方法:
- 预签名时设置足够长的 TTL(>= 10 分钟)。
- 允许 Twilio 用户代理通过 WAF,或对媒体存储桶绕过 WAF。
- 确保
Content-Type和Content-Length正确。
10) 11200 — HTTP 检索失败(Webhook 端点)
Twilio 调试器显示:
11200 - HTTP retrieval failure
根本原因:
- Webhook 端点超时、返回 5xx,或存在 DNS/TLS 问题。
解决方法:
- 快速返回 2xx;将工作加入队列。
- 增加服务器超时时间;确保 TLS 证书链正确。
- 添加运行状况检查和自动扩缩容。
安全加固
Webhook 验证(必需)
- 验证每个入站 Webhook 和状态 Webhook 中的
X-Twilio-Signature。 - 将
TWILIO_AUTH_TOKEN存储在受限的密钥存储中;不要将其暴露在应用日志中。 - 如果 REST 调用使用 API 密钥,仍需存储 Auth Token 以进行验证。
最小权限凭证
- REST 调用优先使用 API Keys,而非 Auth Token。
- 每季度轮换 API 密钥;怀疑密钥已泄露时立即轮换。
- 为每个环境(dev/stage/prod)和每项服务分别使用密钥。
传输安全
- 在公共端点强制使用 TLS 1.2+。
- 在你的域名上启用 HSTS。
- Webhook 不接受明文 HTTP。
数据最小化
- 仅存储必要的消息内容;考虑对以下内容进行哈希处理或脱敏:
- OTP 代码
- 付款详情(绝不应发送)
- 敏感 PII
- 应用保留策略(例如,消息正文保留 30–90 天,元数据保留更长时间)。
访问控制和审计
- 通过 SSO 和 MFA 限制 Twilio 控制台的访问。
- 记录所有模板更改和发送方更改。
- 如果组织结构支持,可使用独立子账户进行隔离。
符合 CIS 基准的主机加固(Linux)
参考:CIS Ubuntu Linux 22.04 LTS 基准(适用时)。
- 以非 root 用户(
whatsapp)运行服务。 - systemd 加固:
NoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=true
- 文件权限:
/srv/whatsapp/.env模式为0600,所有者为服务用户。
- 禁用服务用户的 shell 访问:
/usr/sbin/nologin
WAF / 反向代理注意事项
- 不要通过 IP 允许列表限制 Twilio;应改用签名验证。
- 确保代理原样保留请求正文;签名验证对参数变更十分敏感。
- 如果必须转换请求,应在转换前于边缘验证请求。
性能调优
1) Webhook 延迟:加入队列 + 204
目标:
- Webhook 处理程序延迟 p95 < 50ms(不包括网络延迟)
- 始终在 1s 内响应
预期影响:
- 减少 Twilio 重试和重复投递。
- 在突发流量下保持稳定。
实现:
- 解析并验证签名
- 写入最小事件记录
- 将任务加入队列
- 返回
204 No Content
2) 出站吞吐量:并发控制
问题:
- 不受限制的并发会触发
20429并增加尾延迟。
解决方案:
- 按发送方或 Messaging Service 使用令牌桶。
- 从每个 Pod 20–50 的并发数开始,根据观察到的 20429 发生率进行调优。
预期影响:
- 减少限流错误;提高持续吞吐量。
3) 连接复用
- 在 Twilio REST 调用中使用 HTTP keep-alive 代理(Node)。
- 在 Python 中复用客户端,避免每次请求都创建新客户端。
预期影响:
- 在高发送量下降低 CPU 占用和延迟。
4) 去重存储
- 使用 Redis 的
SETNX和 TTL 对 Webhook 去重。 - 使 TTL 与最大重放窗口(7–14 天)保持一致。
预期影响:
- 防止下游操作重复执行(重复回复、重复退款等)。
5) 通过 Messaging Service 优化成本
- 使用 Messaging Service 进行发送方池化和路由。
- 对于混合渠道(SMS/WhatsApp),请谨慎配置地理位置匹配和回退规则。
预期影响:
- 降低运维开销;减少错误路由;根据路由方式可能节省成本。
高级主题
处理乱序状态转换
不要将状态建模为严格排序的简单状态机,而应:
- 存储所有带时间戳的状态事件。
- 将“当前状态”设为优先级最高的终态:
failed/undelivered为终态负面状态read为终态正面状态delivered为正面状态sent/queued为临时状态
多租户 / 子账户架构
如果你要服务多个客户:
- 为每个租户使用 Twilio 子账户以实现隔离。
- 存储每个租户的
AccountSid和 API 密钥。 - 确保 Webhook 验证使用对应租户的 Auth Token(如果提供了
To号码或AccountSid,则根据它们进行映射)。
模板本地化
- 使用 Content API 提供本地化变体。
- 根据用户资料选择区域设置;默认回退到
en_US。 - 确保各区域设置中的模板变量保持一致。
媒体隐私与合规
- 转发预签名 URL 会泄露访问权限;应缩短有效期。
- 如果策略要求,可考虑通过你的域名并使用身份验证代理媒体,但要确保 Twilio 能够获取媒体。
灾难恢复
- 如果 Webhook 处理服务不可用,Twilio 会在有限时间内重试。
- 将原始 Webhook 负载持久化到耐久存储(S3/GCS),以便重放。
- 提供重放工具,按
MessageSid重新入队事件。
测试策略
- 单元测试:
- 签名验证(已知正确的测试样例)
- 退订关键词解析
- E.164 规范化
- 集成测试:
- 向沙盒号码发送消息
- 验证是否收到状态回调
- 负载测试:
- 模拟 Webhook 突发流量(例如 500 RPS),并确保返回 2xx 响应
使用示例
1) 生产环境:发送物流更新模板,然后处理回复
步骤:
- 用户在网站结账时选择加入。
- 发送模板“shipping_update”。
- 用户回复“我的包裹在哪里?”
- 使用会话消息进行回复。
Node(端到端示意):
// 1) consent stored elsewhere
const to = "+14155550123";
// 2) template send
const templateSid = "HXb5b62575e6e4ff6129ad7c8efe1f983e";
await client.messages.create({
to: `whatsapp:${to}`,
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
contentSid: templateSid,
contentVariables: JSON.stringify({ "1": "Ava", "2": "18473", "3": "UPS" }),
statusCallback: `${process.env.PUBLIC_BASE_URL}/twilio/status`
});
// 3/4) inbound webhook routes to agent/bot and responds within 24h window
2) 媒体:使用预签名 URL 发送发票 PDF
Python:
pdf_url = "https://files.example.com/presigned/invoices/18473.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=..."
sid = client.messages.create(
to="whatsapp:+14155550123",
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
body="Invoice for Order #18473",
media_url=[pdf_url],
status_callback=f"{os.environ['PUBLIC_BASE_URL']}/twilio/status",
).sid
print(sid)
3) 退订:用户发送 STOP,执行抑制
入站处理程序逻辑:
- 如果正文是 STOP 关键词:
- 标记为已抑制
- 回复确认消息(由于是用户发起的,因此允许发送会话消息)
响应示例(TwiML 留空也可以;你也可以通过 REST 发送出站消息):
<Response></Response>
然后通过 REST 发送确认消息:
twilio api:core:messages:create \
--to "whatsapp:+14155550123" \
--messaging-service-sid MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5 \
--body "You are opted out. Reply START to re-subscribe."
4) 基于状态的重试:处理临时故障
策略:
- 不要在未检查错误代码的情况下,盲目重试状态为
failed的 WhatsApp 发送。 - 仅在已知的临时状况下重试(例如 20429 限流),并采用退避策略。
伪代码:
- 如果 REST 调用返回 429 或错误 20429:
- 使用指数退避 + 随机抖动重试
- 如果状态回调返回 63016:
- 切换到模板消息或其他渠道
5) 本地开发:ngrok + 沙盒
- 在端口 3000 上启动服务器。
- 启动 ngrok:
ngrok http 3000
- 将
PUBLIC_BASE_URL设置为 ngrok HTTPS URL。 - 将 Twilio 沙盒的入站 Webhook 配置为:
https://<id>.ngrok-free.app/twilio/inbound
- 向沙盒号码发送 WhatsApp 消息;检查入站处理程序日志。
6) 多区域:根据用户区域设置和发送方路由
- 维护映射:
country_code -> messaging_service_sid
- 根据
To的国家/地区选择 Messaging Service。
映射文件示例:
路径: /srv/whatsapp/config/routing.yaml
default_messaging_service_sid: MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5
by_country:
US: MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5
GB: YOUR_MG_SID
DE: YOUR_MG_SID
快速参考
| 任务 | 命令 / API | 关键标志 / 字段 |
|---|---|---|
| 发送会话文本 | twilio api:core:messages:create | --to、--messaging-service-sid、--body、--status-callback |
| 发送媒体 | messages.create | media_url[] / --media-url |
| 发送模板(Content API) | messages.create | contentSid、contentVariables |
| 列出消息 | twilio api:core:messages:list | --to、--from、--date-sent、--limit |
| 获取消息 | twilio api:core:messages:fetch | --sid |
| 验证 Webhook | SDK 验证器 | X-Twilio-Signature、精确 URL、TWILIO_AUTH_TOKEN |
| 处理退订 | 入站消息解析 | STOP/START 关键词 + 抑制列表 |
| 诊断 Webhook 故障 | Twilio Console Debugger | 错误 11200、请求 / 响应详情 |
图关系
DEPENDS_ON
twilio-core(Twilio REST API 基础知识:身份验证、子账户、API 密钥)twilio-messaging(Programmable Messaging 模式:Messaging Services、状态回调)webhook-security(签名验证、重放保护)queueing(用于异步处理的 Kafka/SQS/PubSub 模式)secrets-management(KMS、Vault、云端密钥管理器)
COMPOSES
twilio-verify(在支持的地区通过 WhatsApp 发送 OTP;回退策略)sendgrid-transactional(WhatsApp 发送失败或用户已退订时使用电子邮件回退)twilio-studio(快速流程原型设计;REST 触发器集成)observability(结构化日志、追踪、指标,以及针对失败率的告警)
SIMILAR_TO
twilio-sms(发送 / 状态模式类似;合规要求和 STOP 语义不同)meta-whatsapp-cloud-api(直接使用 Meta API;Twilio 抽象了部分事项,但也增加了自身的限制)twilio-conversations(更高层级的对话编排;所用原语不同于原始 Messages API)