关于 twilio-conversations
面向使用 OpenClaw 的工程师,提供在生产环境中实施和运营 Twilio Conversations 的指导。内容涵盖跨短信、WhatsApp 和聊天管理会话、参与者与消息,以及 Webhook、合规和可观测性实践。
主要功能
- 01创建和管理对话与参与者
- 02通过短信、WhatsApp 和聊天发送消息
- 03处理 Webhook 事件并跟踪送达状态
- 04使用幂等处理和重试
- 05执行退订合规与成本控制
- 06排查 Twilio 错误和 Webhook 故障
适用场景
- 01构建跨渠道统一消息线程
- 02处理消息送达、已读和失败事件
- 03运营支持短信退订的消息服务
- 04诊断 Webhook 故障并响应事件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ alphaonedev/ openclaw-graph --skill twilio-conversations安装后在 Claude Code 中输入/twilio-conversations确认已加载。
详细介绍
twilio-conversations
用途
让 OpenClaw 能够在生产环境中实现和运行 Twilio Conversations:创建和管理跨 SMS、WhatsApp 和聊天渠道的对话、参与者及消息;集成用于送达、已读和失败状态的 Webhook;强制执行退订和合规要求;并与 Twilio Programmable Messaging/Voice/Verify/Studio/SendGrid 模式组合使用。
对工程师的具体价值:
- 构建跨渠道(SMS、WhatsApp、应用内聊天)的统一“会话”抽象,并提供一致的参与者/消息 API。
- 实现由 Webhook 驱动的可靠状态管理(送达、已读、失败),支持幂等性、重试和背压。
- 支持大规模运行:速率限制、分页、成本控制(Messaging Services、地理位置匹配)和合规要求(STOP、10DLC、免费电话)。
- 提供生产级可观测性,以及针对 Twilio 错误代码和 Webhook 故障的事件响应手册。
先决条件
账户和 Twilio 设置
- 已启用 Conversations 的 Twilio 账户。
- 至少具备以下一项:
- 支持 SMS 的电话号码(适用时,需为美国 A2P 注册 10DLC)
- WhatsApp 发件人(通过 Twilio 接入 WhatsApp Business API)
- Messaging Service(建议使用,以便扩展并控制成本)
- Twilio 可访问的 Webhook 端点(公开 HTTPS)。本地开发可使用
ngrok或cloudflared。
运行时版本(已测试)
- Node.js:20.11.1(LTS)
- Python:3.11.7
- Twilio Node SDK:4.23.0
- Twilio Python SDK:9.4.1
- OpenSSL:1.1.1w 或 3.0.13(取决于平台)
- Docker:25.0.3(可选)
- PostgreSQL:15.5(可选,用于持久化 Webhook/事件)
- Redis:7.2.4(可选,用于幂等键/速率限制)
操作系统支持
- Ubuntu 22.04 LTS(x86_64、arm64)
- Fedora 39(x86_64)
- macOS 14 Sonoma(Intel 和 Apple Silicon)
身份验证设置
生产服务建议使用 Twilio API Key,而非 Account SID + Auth Token。
- 创建 API Key:
- Twilio Console → Account → API keys & tokens → Create API key
- 保存:
TWILIO_ACCOUNT_SID(以AC...开头)TWILIO_API_KEY_SID(以SK...开头)TWILIO_API_KEY_SECRET
环境变量(示例):
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_API_KEY_SID="YOUR_API_KEY_SID"
export TWILIO_API_KEY_SECRET="YOUR_API_KEY_SECRET"
如果必须使用 Auth Token(旧方式):
export TWILIO_AUTH_TOKEN="your_auth_token"
网络和防火墙
- 允许通过 HTTPS 出站访问
api.twilio.com和conversations.twilio.com。 - 入站 Webhook 端点必须允许 Twilio IP 范围访问(或验证签名;建议验证签名,而非使用 IP 允许列表)。
核心概念
Conversations 心智模型
- Conversation:会话容器。包含
sid(CH...)、uniqueName、属性、计时器和状态。 - Participant:对话中的实体。类型包括:
- 聊天参与者(基于身份):
identity="user-123" - 消息参与者(基于电话号码):
messagingBinding.address="+14155550100"和proxyAddress(Twilio 号码或 Messaging Service 发件人) - WhatsApp 参与者:地址类似于
whatsapp:+14155550100
- 聊天参与者(基于身份):
- Message:在对话中发送的内容。包含作者、正文、媒体、属性和送达回执(取决于渠道)。
- Webhook 事件:Twilio 会针对对话、消息、参与者生命周期及送达状态发送 HTTP 回调。
架构概览(生产环境)
- 入口:
- 入站 SMS/WhatsApp 会触发 Programmable Messaging Webhook。
- 应用内聊天使用 Conversations SDK(Web/移动端)或 REST API。
- 路由:
- 将入站消息映射到 Conversation(根据电话号码、身份或业务键)。
- 添加或确保参与者存在。
- 出口:
- 通过 Conversations API 发送消息(统一会话时优先使用),或通过 Messaging API 发送(用于非会话式群发)。
- 状态和可观测性:
- 持久化 Webhook 事件(仅追加),并据此推导状态(送达、已读、失败)。
- 使用幂等键处理 Twilio 重试。
- 合规:
- 处理 STOP/START/HELP(Messaging 合规要求),并移除参与者或将其加入黑名单。
- 美国流量需完成 10DLC/免费电话验证;WhatsApp 需遵守模板规则。
关键标识符
- Account SID:
AC... - Conversation SID:
CH... - Participant SID:
MB...(因情况而异) - Message SID:
IM... - Service SID(Conversations Service):
IS...(如果使用 Services) - Messaging Service SID:
MG...
Webhook 签名验证
Twilio 使用 X-Twilio-Signature 对 Webhook 请求签名。请使用 Twilio 调用时的确切 URL(包括查询字符串)和 POST 参数进行验证。
安装与设置
官方 Python SDK — Conversations
仓库: https://github.com/twilio/twilio-python
PyPI: pip install twilio · 支持: Python 3.7–3.13
from twilio.rest import Client
client = Client()
# Create conversation
conv = client.conversations.v1.conversations.create(
friendly_name="Support Chat #123"
)
# Add participant (SMS)
p = client.conversations.v1.conversations(conv.sid) \
.participants.create(
messaging_binding_address="+15558675309",
messaging_binding_proxy_address="+15017250604"
)
# Send message
client.conversations.v1.conversations(conv.sid) \
.messages.create(body="Welcome to support chat!", author="system")
来源:twilio/twilio-python — conversations
1) 系统依赖项
Ubuntu 22.04
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 # v20.11.1 (or newer 20.x)
npm -v
Python 3.11:
sudo apt-get install -y python3.11 python3.11-venv python3-pip
python3.11 --version
Fedora 39
sudo dnf install -y jq curl ca-certificates
sudo dnf module install -y nodejs:20
node -v
sudo dnf install -y python3.11 python3.11-pip
python3.11 --version
macOS 14(Intel/Apple Silicon)
Homebrew:
brew update
brew install jq node@20 [email protected]
node -v
python3.11 --version
2) 项目依赖项
Node(推荐用于 Webhook 服务)
mkdir -p twilio-conversations-service && cd twilio-conversations-service
npm init -y
npm install [email protected] [email protected] [email protected] [email protected] [email protected]
npm install --save-dev [email protected] [email protected] @types/[email protected] @types/[email protected]
Python(批处理作业 / 管理工具)
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip==24.0
pip install twilio==9.4.1 requests==2.31.0
3) 使用签名验证的 Webhook 端点(Express)
创建 src/server.ts:
import express from "express";
import bodyParser from "body-parser";
import pinoHttp from "pino-http";
import twilio from "twilio";
const app = express();
app.use(pinoHttp());
// Twilio signature validation requires the raw body for some frameworks.
// For Express with urlencoded, Twilio helper can validate using parsed params.
// Ensure you use the exact URL configured in Twilio Console.
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
const {
TWILIO_AUTH_TOKEN,
PUBLIC_WEBHOOK_BASE_URL,
} = process.env;
if (!TWILIO_AUTH_TOKEN) throw new Error("TWILIO_AUTH_TOKEN is required for webhook signature validation");
if (!PUBLIC_WEBHOOK_BASE_URL) throw new Error("PUBLIC_WEBHOOK_BASE_URL is required (e.g., https://example.com)");
app.post("/twilio/conversations/webhook", (req, res) => {
const signature = req.header("X-Twilio-Signature") || "";
const url = `${PUBLIC_WEBHOOK_BASE_URL}/twilio/conversations/webhook`;
const isValid = twilio.validateRequest(
TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
if (!isValid) {
req.log.warn({ signature }, "Invalid Twilio signature");
return res.status(403).send("Forbidden");
}
// Idempotency: Twilio may retry. Use EventSid or MessageSid as a dedupe key.
const eventType = req.body.EventType;
const eventSid = req.body.EventSid;
req.log.info({ eventType, eventSid, body: req.body }, "Twilio Conversations webhook");
// TODO: enqueue to a worker; respond fast.
res.status(200).send("ok");
});
const port = Number(process.env.PORT || 3000);
app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`listening on :${port}`);
});
运行:
npx ts-node src/server.ts
使用 ngrok 暴露服务:
ngrok http 3000
# Set PUBLIC_WEBHOOK_BASE_URL to the https URL ngrok gives you
4) 配置 Conversations Webhook
在 Twilio Console 中:
- Conversations → Services →(你的服务)→ Webhooks
- 配置:
- Pre-event Webhook(可选;用于授权 / 路由决策)
- Post-event Webhook(推荐;用于事件接收)
- 指向:
https://your-domain.example.com/twilio/conversations/webhook
如果要将入站 SMS/WhatsApp 消息接入 Conversations,也要将 Programmable Messaging 入站 Webhook 配置到你的应用(如果你要自定义映射),或者使用 Conversations 的消息绑定(首选)。
主要功能
创建和管理 Conversations
- 使用
uniqueName创建会话,以便进行幂等查找。 - 设置
attributesJSON,以保存业务元数据(tenantId、caseId、SLA)。 - 管理生命周期:
state(active/inactive/closed)、计时器和清理。
添加参与者(聊天 + 消息 + WhatsApp)
- 通过
identity添加聊天参与者(适用于 SDK 用户)。 - 通过
messagingBinding.address和proxyAddress(Twilio 发件人)添加消息参与者。 - WhatsApp 地址使用
whatsapp:+E164。
生产环境实践:
- 始终确保业务实体 → 会话 uniqueName 之间有确定性映射。
- 添加参与者前,先实施参与者数量限制和拦截名单检查。
发送带送达跟踪的消息
- 使用
author和body发送消息。 - 附加
attributes,以记录关联 ID。 - 通过 Webhook 事件和/或消息状态回调(取决于渠道)跟踪送达情况。
Webhook:事件接收、重试与幂等性
- 验证
X-Twilio-Signature。 - 在 5 秒内响应;将工作加入队列。
- 根据
EventSid(首选)或(MessageSid, EventType, Timestamp)去重。
合规:STOP/START/HELP 和选择退出
- 对于 SMS/WhatsApp,STOP 处理主要属于 Programmable Messaging 的职责。
- 如果你将入站消息接入 Conversations,仍必须遵守选择退出要求:
- 按电话号码维护抑制名单。
- 收到 STOP 后,移除参与者或将其标记为已拦截;阻止发送出站消息。
使用 Messaging Services 优化成本
- 使用具备以下功能的 Messaging Service:
- 地理位置匹配
- 固定发件人
- 智能编码
- 对于 Conversations 消息参与者,将
proxyAddress设置为 Twilio 号码;大规模使用时,优先选择 Messaging Service(如果你的设计支持;通常通过 Messaging API 发送出站消息,同时将 Conversations 保留为系统记录)。
与 Voice/Verify/Studio/SendGrid 组合使用
- 将会话升级为 Voice(拨号/会议),并将通话资料发布回会话。
- 添加参与者或显示敏感信息前,使用 Verify 进行增强身份验证。
- 在会话事件发生时触发 Studio 工作流。
- 发送 SendGrid 事务性电子邮件,并将其作为消息/属性同步到会话中。
命令参考
此技能假设 OpenClaw 可以通过以下方式执行:
- Twilio SDK(Node/Python)
- 直接调用 REST(curl)
- Twilio CLI(可选;对 Conversations 的支持有限)
REST API 基础地址
- Conversations API 基础地址:
https://conversations.twilio.com/v1
身份验证选项:
- 使用 API Key 进行基本身份验证:
- 用户名:
TWILIO_API_KEY_SID - 密码:
TWILIO_API_KEY_SECRET
- 用户名:
- 或使用 Account SID + Auth Token。
curl 辅助程序(API Key)
export TWILIO_API_KEY_SID="YOUR_API_KEY_SID"
export TWILIO_API_KEY_SECRET="YOUR_API_KEY_SECRET"
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
Conversations
创建对话
端点:
POST /v1/Conversations
标志/字段:
FriendlyName(字符串)UniqueName(字符串;用于实现幂等性)Attributes(字符串化 JSON)MessagingServiceSid(字符串;可选)State(active|inactive|closed)Timers.Inactive(ISO-8601 时长字符串,例如PT1H;取决于 API 支持情况)
curl:
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "FriendlyName=Support Case 10492" \
--data-urlencode "UniqueName=case-10492" \
--data-urlencode 'Attributes={"tenantId":"acme","caseId":10492,"priority":"p1"}'
Node:
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,
});
const conv = await client.conversations.v1.conversations.create({
friendlyName: "Support Case 10492",
uniqueName: "case-10492",
attributes: JSON.stringify({ tenantId: "acme", caseId: 10492, priority: "p1" }),
});
console.log(conv.sid);
获取对话
GET /v1/Conversations/{ConversationSid}
curl -sS "https://conversations.twilio.com/v1/Conversations/CHXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
列出对话(分页)
GET /v1/Conversations?PageSize=50&PageToken=...
查询参数:
PageSize(1–1000;实际建议:50–200)PageToken(字符串)- 根据 API 版本,可能支持按
State筛选
curl -sS "https://conversations.twilio.com/v1/Conversations?PageSize=50" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
更新对话
POST /v1/Conversations/{ConversationSid}
字段:
FriendlyNameAttributes(字符串化 JSON)State
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/CHXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "State=closed" \
--data-urlencode 'Attributes={"tenantId":"acme","caseId":10492,"priority":"p1","closedBy":"agent-7"}'
删除对话
DELETE /v1/Conversations/{ConversationSid}
curl -sS -X DELETE "https://conversations.twilio.com/v1/Conversations/CHXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
参与者
添加聊天参与者(身份)
POST /v1/Conversations/{ConversationSid}/Participants
字段:
Identity(字符串)Attributes(字符串化 JSON)
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/CH.../Participants" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "Identity=user-123" \
--data-urlencode 'Attributes={"role":"customer"}'
添加消息参与者(SMS)
字段:
MessagingBinding.Address(E.164 格式,例如+14155550100)MessagingBinding.ProxyAddress(E.164 格式的 Twilio 号码,例如+14155551234)Attributes
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/CH.../Participants" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "MessagingBinding.Address=+14155550100" \
--data-urlencode "MessagingBinding.ProxyAddress=+14155551234" \
--data-urlencode 'Attributes={"role":"customer","channel":"sms"}'
添加消息参与者(WhatsApp)
使用 whatsapp: 前缀:
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/CH.../Participants" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "MessagingBinding.Address=whatsapp:+14155550100" \
--data-urlencode "MessagingBinding.ProxyAddress=whatsapp:+14155559876" \
--data-urlencode 'Attributes={"role":"customer","channel":"whatsapp"}'
列出参与者
GET /v1/Conversations/{ConversationSid}/Participants?PageSize=50
curl -sS "https://conversations.twilio.com/v1/Conversations/CH.../Participants?PageSize=50" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
移除参与者
DELETE /v1/Conversations/{ConversationSid}/Participants/{ParticipantSid}
curl -sS -X DELETE "https://conversations.twilio.com/v1/Conversations/CH.../Participants/MB..." \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
消息
发送消息
POST /v1/Conversations/{ConversationSid}/Messages
字段:
Author(字符串;身份或系统标签)Body(字符串)Attributes(字符串化 JSON)MediaSid/ 媒体字段(使用媒体时;取决于 API)
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/CH.../Messages" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "Author=agent-7" \
--data-urlencode "Body=We’re looking into this now. ETA 15 minutes." \
--data-urlencode 'Attributes={"correlationId":"req-01HPQ9K7Z9Y7J8V7Z0","visibility":"customer"}'
列出消息
GET /v1/Conversations/{ConversationSid}/Messages?PageSize=50&Order=asc|desc
curl -sS "https://conversations.twilio.com/v1/Conversations/CH.../Messages?PageSize=50&Order=desc" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
获取消息
GET /v1/Conversations/{ConversationSid}/Messages/{MessageSid}
curl -sS "https://conversations.twilio.com/v1/Conversations/CH.../Messages/IM..." \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
删除消息(内容审核)
DELETE /v1/Conversations/{ConversationSid}/Messages/{MessageSid}
curl -sS -X DELETE "https://conversations.twilio.com/v1/Conversations/CH.../Messages/IM..." \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET"
Webhook(Conversations 服务)
Conversations 通常使用 Service 来配置默认设置和 Webhook。
- 列出服务:
GET /v1/Services - 创建服务:
POST /v1/Services - 配置 Webhook:
POST /v1/Services/{ServiceSid}/Configuration/Webhooks
关键字段(因 Webhook 类型而异):
PostWebhookUrlPostWebhookMethod(GET|POST)Filters(事件类型数组)PreWebhookUrl、PreWebhookMethodWebhookTimeout(秒;若受支持)
由于 Webhook 配置字段会不断变化,建议在初始设置时优先使用 SDK 类型定义或控制台;之后尽可能将配置导出到 IaC(Terraform)。
配置参考
环境变量
建议文件:/etc/openclaw/twilio-conversations.env(Linux)或 ~/.config/openclaw/twilio-conversations.env(开发环境)
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_API_KEY_SID=YOUR_API_KEY_SID
TWILIO_API_KEY_SECRET=replace_me
TWILIO_AUTH_TOKEN=replace_me_for_webhook_validation_only
PUBLIC_WEBHOOK_BASE_URL=https://conversations-webhooks.acme.example
TWILIO_CONVERSATIONS_SERVICE_SID=YOUR_IS_SID
DEFAULT_PROXY_ADDRESS=+14155551234
DEFAULT_WHATSAPP_PROXY=whatsapp:+14155559876
# Compliance / suppression
SUPPRESSION_REDIS_URL=redis://:[email protected]:6379/2
使用 systemd 单元加载:
/etc/systemd/system/openclaw-twilio-conversations.service:
[Unit]
Description=OpenClaw Twilio Conversations Webhook Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=/etc/openclaw/twilio-conversations.env
WorkingDirectory=/opt/openclaw/twilio-conversations
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=2
User=openclaw
Group=openclaw
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/openclaw /var/log/openclaw
[Install]
WantedBy=multi-user.target
OpenClaw 技能配置(示例)
/opt/openclaw/config/skills/twilio-conversations.toml:
[twilio_conversations]
service_sid = "YOUR_IS_SID"
default_proxy_address = "+14155551234"
default_whatsapp_proxy = "whatsapp:+14155559876"
[twilio_conversations.webhooks]
public_base_url = "https://conversations-webhooks.acme.example"
path = "/twilio/conversations/webhook"
validate_signature = true
max_processing_ms = 2000
[twilio_conversations.idempotency]
backend = "redis"
redis_url = "redis://:[email protected]:6379/2"
ttl_seconds = 86400
[twilio_conversations.compliance]
enforce_stop = true
stop_keywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"]
start_keywords = ["START", "YES", "UNSTOP"]
help_keywords = ["HELP", "INFO"]
NGINX 反向代理
/etc/nginx/conf.d/openclaw-twilio-conversations.conf:
server {
listen 443 ssl http2;
server_name conversations-webhooks.acme.example;
ssl_certificate /etc/letsencrypt/live/conversations-webhooks.acme.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/conversations-webhooks.acme.example/privkey.pem;
client_max_body_size 1m;
location /twilio/conversations/webhook {
proxy_pass http://127.0.0.1:3000/twilio/conversations/webhook;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
}
}
集成模式
模式:入站 SMS → 映射到对话 → Agent 回复
工作流:
- Programmable Messaging 入站 webhook 接收 SMS。
- 根据
uniqueName = "sms:+14155550100"查找或创建对话(也可以按租户范围)。 - 确保消息参与者存在,且
MessagingBinding.Address= 发送方。 - 将入站消息发布到对话中(作者 = 电话号码)。
- Agent 通过 Conversations 消息 API 回复。
关键点:入站 SMS 已作为 Messaging 事件存在;你要将其镜像到 Conversations,以便统一会话线程。确保不会重复发送出站消息。
模式:以 Conversations 作为事实来源,使用 Messaging Service 发送出站消息
- 在 Conversations 中存储所有消息,以供 UI 展示和查看历史记录。
- 大规模发送出站 SMS 时,通过 Messaging API 使用
MessagingServiceSid=MG...,以实现地理位置匹配和吞吐量优化。 - 将出站消息镜像到 Conversations,并附上用于关联 Messaging
MessageSid的属性。
这样既能避免管理发送方的复杂性,也能保留单一会话线程。
模式:添加新的电话参与者前先验证
- 用户请求将电话号码添加到对话中。
- 向该号码触发 Verify V2 SMS。
- 只有验证成功后,才添加为消息参与者。
模式:升级到 Voice,并附加通话录音/转录文本
- 对话中出现升级处理关键词时,创建 Voice 呼叫(TwiML Dial/Conference)。
- 启用录音和转录。
- 将录音 URL/转录文本作为消息或对话属性发布回对话中。
模式:由对话事件触发 Studio 流程
- 事件后 webhook 接收
onMessageAdded。 - 对于特定意图,调用 Studio REST Trigger API 来运行流程。
- 流程可以通过 Conversations 或 Messaging 发送消息。
错误处理与故障排查
根据代码、HTTP 状态和是否可重试来处理 Twilio 错误。始终记录:
- Twilio
X-Twilio-Request-Id - HTTP 状态
- 错误代码/消息
- 相关 SID
1)认证失败(20003)
典型错误:
- HTTP 401
- 正文包含:
"code": 20003"message": "Authenticate"
根本原因:
- API 密钥 secret 错误、身份验证令牌错误,或在 SDK 配置中使用 API 密钥时未提供
accountSid。
解决方法:
- 验证
TWILIO_API_KEY_SID/SECRET。 - 在 Node SDK 中使用 API 密钥时,传入
{ accountSid: TWILIO_ACCOUNT_SID }。 - 轮换已泄露的密钥。
2)速率限制(20429)
错误:
- HTTP 429
"code": 20429"message": "Too Many Requests"
根本原因:
- 突发流量超过 Twilio API 限制或账户并发限制。
解决方法:
- 实现带随机抖动的指数退避(例如,基础延迟 250ms,最大 5s)。
- 将写入操作加入队列;对列表操作进行批处理;发生超时时减小
PageSize。 - 为每个账户/服务设置工作进程并发上限。
3)电话号码无效(21211)
错误:
- HTTP 400
"code": 21211"message": "The 'To' number +1415555 is not a valid phone number."
根本原因:
- 格式不是 E.164、缺少国家代码或包含无效字符。
解决方法:
- 添加消息参与者前,将号码规范化为 E.164 格式。
- 在应用层使用 libphonenumber。
- 尽早拒绝无效号码,并提供明确的错误信息。
4)Messaging 消息发送失败(30003)
错误(Messaging 状态回调):
"ErrorCode": "30003""MessageStatus": "undelivered""ErrorMessage": "Unreachable destination handset."(取决于运营商)
根本原因:
- 运营商网络无法连接、手机关机、号码已屏蔽或路由无效。
解决方法:
- 将参与者标记为无法联系;回退到其他渠道(WhatsApp/电子邮件/语音)。
- 重试策略:限制重试次数,避免无限循环。
- 对于 WhatsApp,确保模板和用户选择加入设置正确。
5) Webhook 签名验证失败
日志:
Invalid Twilio signature
根本原因:
PUBLIC_WEBHOOK_BASE_URL与 Twilio 实际调用的 URL 不匹配(协议/主机/路径)。- NGINX 重写了路径,或验证 URL 中缺少查询字符串。
- Twilio 发送
application/x-www-form-urlencoded(或反之)时使用了 JSON 请求体解析。
修复方法:
- 确保验证 URL 与 Twilio 配置完全一致。
- 通过
X-Forwarded-*保留原始主机/协议,并正确重建 URL。 - 确认内容类型和请求体解析方式。
6) 参与者已存在
常见 API 错误:
- HTTP 409
- 类似以下内容的消息:
"message": "Participant already exists"
根本原因:
- 重试/并发情况下,添加参与者的调用不具备幂等性。
修复方法:
- 如果参与者符合预期绑定,则将 409 视为成功。
- 创建前按确定性方式查找参与者(列出参与者并按身份/地址匹配)。
- 按会话串行执行参与者创建。
7) 未找到会话
错误:
- HTTP 404
"message": "The requested resource /Conversations/CH... was not found"
根本原因:
- SID 错误、会话已删除,或 SID 属于其他账户。
修复方法:
- 验证 SID 前缀和长度。
- 确保使用正确的账户凭据。
- 如果使用
uniqueName,请通过列出/筛选重新解析(或在数据库中存储映射)。
8) Webhook 超时和重试
表现:
- Twilio 多次重试同一事件;日志中出现重复记录。
根本原因:
- Webhook 处理程序超过 Twilio 超时时间,或返回非 2xx 状态码。
- 下游依赖项(数据库)响应缓慢。
修复方法:
- 入队后立即返回 200。
- 使用持久化队列(SQS/Kafka/RabbitMQ)并异步处理。
- 使用以
EventSid为键、TTL 为 24 小时以上的 Redis 实现幂等性。
9) WhatsApp 模板/会话问题(常见)
表现:
- WhatsApp 出站消息因政策相关错误发送失败(因地区/账户而异)。
根本原因:
- 尝试在 24 小时会话窗口外发送自由格式消息;此时必须使用模板。
修复方法:
- 使用已批准的模板重新联系用户。
- 跟踪每位参与者最后一条入站消息的时间;确保按要求使用模板。
10) 违反退订要求
表现:
- 运营商过滤、投诉或 Twilio 合规警告。
根本原因:
- 用户发送 STOP 后仍继续发送 SMS,或未在各系统中遵守退订要求。
修复方法:
- 使用集中式屏蔽列表;每次发送出站消息前都进行检查。
- 收到 STOP 入站消息后,立即将用户加入屏蔽列表,并根据政策确认退订。
安全加固
密钥管理
- 不要将
TWILIO_API_KEY_SECRET存储在代码库中。 - 使用:
- AWS Secrets Manager / GCP Secret Manager / Vault
- 在可用时使用 systemd
LoadCredential=(systemd 252+)
- 每季度轮换 API 密钥;发生安全事件时立即轮换。
Webhook 验证
- 始终验证
X-Twilio-Signature。 - 仅允许 HTTPS;在边缘层将 HTTP 重定向到 HTTPS。
- 拒绝缺少签名的请求。
最小权限
- 为每项服务使用单独的 API 密钥(Webhook 与批处理作业分开)。
- 根据内部政策限制密钥的使用(Twilio 密钥适用于整个账户;通过网络分段和密钥分发实施限制)。
数据最小化
- 如果不需要,避免存储完整消息正文;仅存储哈希/元数据。
- 如果存储消息正文,请对静态数据加密(Postgres TDE 的替代方案:磁盘加密 + 应用级信封加密)。
符合 CIS 的主机加固(实用映射)
- CIS Ubuntu Linux 22.04 LTS 基准:
- 禁用 SSH 密码认证;强制使用密钥认证。
- 启用自动安全更新。
- 将入站端口限制为仅允许 Webhook 边缘使用 443。
- 以非 root 用户(
openclaw用户)运行服务。 - systemd 沙箱设置:
NoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=true
审计日志
- 记录所有管理操作:
- 创建/关闭/删除会话
- 添加/移除参与者
- 发送出站消息
- 包含关联 ID 和操作者身份。
性能调优
Webhook 接收延迟
目标:Webhook 处理器的 p95 延迟 < 50ms,且始终 < 500ms。
优化措施:
- 解析并验证签名,然后将请求加入队列并返回 200。
- 避免在请求线程中同步写入数据库。
预期影响:
- 优化前:数据库竞争时 p95 延迟为 800–1500ms → Twilio 重试。
- 优化后:p95 延迟为 10–30ms;重试几乎为零。
API 速率限制与批处理
- 列表操作使用
PageSize=200以减少往返次数,但要留意响应大小和超时。 - 在 Redis 中按
uniqueName缓存对话 SID(TTL 为 1h),避免调用列表或搜索接口。
预期影响:
- 在高流量路由服务中,将 Twilio API 调用减少 60–90%。
参与者查询策略
- 维护自己的映射表:
(tenantId, externalUserId) -> conversationSid(tenantId, phoneE164) -> conversationSid
- 避免通过列出参与者或消息来“发现”状态。
预期影响:
- 避免 O(n) 扫描;随着对话规模增大,延迟仍保持稳定。
消息扇出控制
如果添加许多参与者(群聊),外发消息扇出可能代价高昂且缓慢。
- 为每个对话设置参与者人数上限(策略)。
- 对于广播,使用 Messaging API 和分段发送,而不是使用单个对话。
高级主题
用于授权的事件前 Webhook
使用事件前 Webhook 来:
- 阻止未知身份添加参与者。
- 强制执行租户边界(身份必须与对话属性匹配)。
- 在接受消息前拒绝包含不允许内容(PII 泄露)的消息。
实现说明:
- 事件前 Webhook 必须具有高可用性;故障可能阻断消息流。
- 按照 Twilio 事件前 Webhook 契约,明确返回允许或拒绝。
幂等的对话创建
Twilio 不保证并发调用方能够通过 uniqueName 原子地执行“若不存在则创建”。
模式:
- 尝试使用
UniqueName创建。 - 如果发生冲突或重复,则通过映射数据库(首选)或列表/筛选(备用)按
UniqueName获取。 - 存储映射。
多租户隔离
- 在
uniqueName前添加租户前缀:t_acme_case_10492 - 在
attributes中存储tenantId。 - 处理每个 Webhook 事件前验证租户。
消息排序与最终一致性
- Webhook 事件可能乱序到达。
- 使用事件时间戳和消息索引(如果提供)进行排序。
- 将 Webhook 视为事件流;构建支持重放能力的派生状态。
媒体处理
- 媒体消息可能需要单独的检索和存储策略。
- 强制执行内容类型允许列表和大小限制。
- 在向内部用户公开入站媒体前,考虑进行病毒扫描。
与 Programmable Messaging 状态回调的互操作
你可能会收到:
- Conversations 事件后 Webhook(添加消息)
- Messaging 状态回调(已送达/失败)
通过以下方式统一处理:
- 通过 Messaging API 发送时,将 Messaging
MessageSid存储在 Conversations 消息属性中。 - 收到状态回调后,更新内部消息状态,并可选择向对话中发送系统消息(或更新外部 UI)。
使用示例
场景 1:为支持工单创建对话,添加 SMS 客户和聊天 Agent,并发送初始消息
# 1) Create conversation
CONV_SID=$(curl -sS -X POST "https://conversations.twilio.com/v1/Conversations" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "FriendlyName=Support Case 10492" \
--data-urlencode "UniqueName=t_acme_case_10492" \
--data-urlencode 'Attributes={"tenantId":"acme","caseId":10492,"priority":"p1"}' | jq -r .sid)
echo "$CONV_SID"
# 2) Add SMS participant (customer)
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/$CONV_SID/Participants" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "MessagingBinding.Address=+14155550100" \
--data-urlencode "MessagingBinding.ProxyAddress=+14155551234" \
--data-urlencode 'Attributes={"role":"customer","channel":"sms"}' | jq .
# 3) Add chat participant (agent)
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/$CONV_SID/Participants" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "Identity=agent-7" \
--data-urlencode 'Attributes={"role":"agent"}' | jq .
# 4) Send message
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/$CONV_SID/Messages" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "Author=agent-7" \
--data-urlencode "Body=Hi—this is Acme Support. We’re on it." \
--data-urlencode 'Attributes={"correlationId":"req-01HPQ9K7Z9Y7J8V7Z0"}' | jq .
场景 2:使用抑制列表(Redis)处理入站 STOP 并移除参与者
用于处理入站消息 Webhook(来自 Messaging)并强制执行 STOP 的 Python 代码片段:
import os
import redis
from twilio.rest import Client
STOP_WORDS = {"STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"}
START_WORDS = {"START", "YES", "UNSTOP"}
r = redis.Redis.from_url(os.environ["SUPPRESSION_REDIS_URL"])
client = Client(os.environ["TWILIO_API_KEY_SID"], os.environ["TWILIO_API_KEY_SECRET"], os.environ["TWILIO_ACCOUNT_SID"])
def handle_inbound_sms(from_e164: str, body: str, conversation_sid: str, participant_sid: str):
normalized = body.strip().upper()
key = f"suppress:sms:{from_e164}"
if normalized in STOP_WORDS:
r.set(key, "1")
# Remove participant to prevent further outbound via Conversations
client.conversations.v1.conversations(conversation_sid).participants(participant_sid).delete()
return {"action": "suppressed"}
if normalized in START_WORDS:
r.delete(key)
return {"action": "unsuppressed"}
if r.get(key):
return {"action": "ignored_suppressed"}
return {"action": "accepted"}
场景 3:使用 EventSid 作为键,通过幂等性处理 Webhook 接收(Redis)
核心逻辑的 Node 代码片段:
import Redis from "ioredis";
const redis = new Redis(process.env.SUPPRESSION_REDIS_URL);
export async function dedupeEvent(eventSid) {
const key = `twilio:event:${eventSid}`;
const ok = await redis.set(key, "1", "NX", "EX", 86400);
return ok === "OK"; // true if first time
}
在 Webhook 处理器中:
- 如果
dedupeEvent(EventSid)为 false,则立即返回 200。
场景 4:升级到 Voice 会议,并将录音链接发送回对话
高级步骤:
- 通过 TwiML
<Dial><Conference record="record-from-start">case-10492</Conference></Dial>创建 Voice 会议。 - 收到
recording.completedWebhook 后,将包含录音 URL 的消息发送到对话中。
发送回对话:
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/CH.../Messages" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "Author=system" \
--data-urlencode "Body=Call recording available: https://api.twilio.com/2010-04-01/Accounts/AC.../Recordings/RE... .mp3" \
--data-urlencode 'Attributes={"type":"call_recording","recordingSid":"RE0123456789abcdef0123456789abcdef"}'
场景 5:使用模板重新触达 WhatsApp 用户并同步到会话
模式:
- 如果距离上次收到消息已超过 24 小时,则通过 Messaging API 发送 WhatsApp 模板(不能发送自由格式消息)。
- 使用属性将模板发送记录同步到会话中。
同步消息:
curl -sS -X POST "https://conversations.twilio.com/v1/Conversations/CH.../Messages" \
-u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
--data-urlencode "Author=system" \
--data-urlencode "Body=Sent WhatsApp template: order_update_v2" \
--data-urlencode 'Attributes={"channel":"whatsapp","template":"order_update_v2","messagingMessageSid":"SM0123456789abcdef0123456789abcdef"}'
场景 6:安全批量关闭不活跃会话
Python 批处理任务:
- 列出会话,根据最后活动时间进行筛选(从你的数据库获取,或使用可用的 Twilio 字段),然后设置
State=closed。
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"])
for conv in client.conversations.v1.conversations.list(limit=200):
# Prefer your own last_activity tracking; Twilio fields vary by API.
if conv.state == "active" and conv.friendly_name.startswith("Support Case"):
client.conversations.v1.conversations(conv.sid).update(state="closed")
print("closed", conv.sid)
快速参考
| 任务 | 端点 / 命令 | 关键字段 / 标志 |
|---|---|---|
| 创建会话 | POST /v1/Conversations | FriendlyName、UniqueName、Attributes、State |
| 列出会话 | GET /v1/Conversations | PageSize、PageToken |
| 更新会话 | POST /v1/Conversations/{CH} | State、Attributes、FriendlyName |
| 删除会话 | DELETE /v1/Conversations/{CH} | 不适用 |
| 添加聊天参与者 | POST /v1/Conversations/{CH}/Participants | Identity、Attributes |
| 添加 SMS 参与者 | 同上 | MessagingBinding.Address、MessagingBinding.ProxyAddress |
| 添加 WhatsApp 参与者 | 同上 | MessagingBinding.Address=whatsapp:+E164、ProxyAddress=whatsapp:+E164 |
| 列出参与者 | GET /v1/Conversations/{CH}/Participants | PageSize、PageToken |
| 移除参与者 | DELETE /v1/Conversations/{CH}/Participants/{MB} | 不适用 |
| 发送消息 | POST /v1/Conversations/{CH}/Messages | Author、Body、Attributes |
| 列出消息 | GET /v1/Conversations/{CH}/Messages | PageSize、Order、PageToken |
| Webhook 验证 | X-Twilio-Signature | 根据确切的 URL 和参数进行验证 |
| 重试处理 | 不适用 | 按 EventSid 去重,在遇到 20429 时进行退避 |
图关系
DEPENDS_ON
twilio-core-auth(API 密钥、令牌轮换、请求签名验证模式)webhook-ingestion(幂等性、排队、背压、签名验证)redis(可选;幂等性 / 抑制)postgres(可选;事件存储 / 审计日志)
COMPOSES
twilio-programmable-messaging(SMS/MMS/WhatsApp 送达回调、STOP 处理、10DLC/免费电话)twilio-voice(升级处理、录音、转录、IVR 状态机)twilio-verify(添加参与者 / 执行敏感操作前的逐步验证)twilio-studio(基于会话事件的流程触发器)sendgrid(将事务性电子邮件同步到会话线程)
SIMILAR_TO
slack-conversations(线程 + 参与者模型、事件驱动更新)zendesk-ticketing(将案例 / 工单生命周期映射到会话状态)intercom-messaging(通过身份 + 联系人绑定实现全渠道消息传递)