mcpField
T
twilio-whatsapp

twilio-whatsapp

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

关于 twilio-whatsapp

twilio-whatsapp 是一项面向 OpenClaw 的技能,供工程师通过 Twilio 实施和运维 WhatsApp Business 消息服务。内容涉及消息类型、媒体、Webhook 处理、同意管理、重试与生产环境投递约束。

主要功能

  1. 01会话消息与模板消息
  2. 02媒体附件处理
  3. 03入站与状态 Webhook
  4. 04加入、退订与抑制管理
  5. 05支持安全重试的回调处理
  6. 06速率限制与成本控制

适用场景

  1. 01在会话窗口外发送已批准模板
  2. 02处理入站消息与投递状态更新
  3. 03落实同意记录与 STOP 请求
  4. 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(可选,但推荐)。
  • 已在 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.0
    • twilio(Python)9.0.5
  • Twilio CLI 5.16.0(用于诊断;运行时不需要)
  • ngrok 3.13.1(用于本地 Webhook 测试)
身份验证与密钥

选择以下其中一种:

  1. API Key(推荐):

    • TWILIO_API_KEY_SID(以 SK... 开头)
    • TWILIO_API_KEY_SECRET
    • TWILIO_ACCOUNT_SID(以 AC... 开头)
  2. Account SID + Auth Token:

    • TWILIO_ACCOUNT_SID
    • TWILIO_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 的角度)
  1. 模板消息(超出 24 小时窗口):

    • 必须使用预先批准的模板。
    • 用于通知、OTP、配送更新等。
    • 在 Twilio 中,模板通常通过 Content API(首选)发送,或根据账户配置通过模板集成发送。
  2. 会话消息(在 24 小时窗口内):

    • 允许发送自由格式的文本/媒体(须遵守 WhatsApp 政策)。
    • 24 小时窗口从用户向你发送消息时开始。
  3. 媒体消息:

    • WhatsApp 支持图片、文档、音频和视频,但有相应限制。
    • Twilio 通过 MediaUrl(可公开访问的 URL)发送媒体;某些流程也会使用 Twilio 托管的媒体。
标识符与地址
  • Twilio 中的 WhatsApp 地址使用 whatsapp: 前缀:
    • From:whatsapp:+14155238886
    • To:whatsapp:+14155550123
  • Messaging Service SID:MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Message SID:SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Webhook

两种主要的 Webhook 类别:

  1. 收到的消息 Webhook(用户向你发送消息时):

    • Twilio 会发送一个 HTTP 请求,其中包含表单编码的参数,如 From、To、Body、NumMedia、MediaUrl0 等。
  2. 消息状态回调(传递生命周期):

    • 状态值: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 Messages API。
  • 确保 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 消费事件、更新数据库并触发下游操作。

示例流水线:

  1. Twilio → POST /twilio/inbound
  2. API → 发布到 Kafka 主题 twilio.inbound.v1
  3. Worker → 解析、应用退订规则,并将请求路由到对话服务
  4. 对话服务 → 决定响应内容 → 通过 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=true
    • ProtectSystem=strict
    • ProtectHome=true
    • PrivateTmp=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) 生产环境:发送物流更新模板,然后处理回复

步骤:

  1. 用户在网站结账时选择加入。
  2. 发送模板“shipping_update”。
  3. 用户回复“我的包裹在哪里?”
  4. 使用会话消息进行回复。

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 + 沙盒
  1. 在端口 3000 上启动服务器。
  2. 启动 ngrok:
ngrok http 3000
  1. 将 PUBLIC_BASE_URL 设置为 ngrok HTTPS URL。
  2. 将 Twilio 沙盒的入站 Webhook 配置为:
    • https://<id>.ngrok-free.app/twilio/inbound
  3. 向沙盒号码发送 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.createmedia_url[] / --media-url
发送模板(Content API)messages.createcontentSid、contentVariables
列出消息twilio api:core:messages:list--to、--from、--date-sent、--limit
获取消息twilio api:core:messages:fetch--sid
验证 WebhookSDK 验证器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)

评论

支持 Markdown