twilio-conversations について
OpenClawを使うエンジニア向けに、Twilio Conversationsの本番導入と運用を案内するスキルです。SMS、WhatsApp、チャットでの会話・参加者・メッセージ管理に加え、Webhook、コンプライアンス、可観測性を扱います。
主な機能
- 01会話と参加者の作成・管理
- 02SMS、WhatsApp、チャットでのメッセージ送信
- 03Webhook処理と配信状況の追跡
- 04冪等性と再試行の適用
- 05オプトアウト対応とコスト管理
- 06TwilioエラーとWebhook障害の調査
活用シーン
- 01複数チャネルを統合したメッセージスレッドの構築
- 02配信・既読・失敗イベントの処理
- 03SMSの配信停止に対応するサービス運用
- 04Webhook障害の診断とインシデント対応
インストール
コマンドをコピーしてターミナルで実行します。
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 アカウント。
- 次のうち少なくとも 1 つ:
- 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(任意、冪等性キー/レート制限用)
OS のサポート
- Ubuntu 22.04 LTS(x86_64、arm64)
- Fedora 39(x86_64)
- macOS 14 Sonoma(Intel および Apple Silicon)
認証の設定
本番サービスでは、Account SID + Auth Token の代わりに Twilio API Key を使用してください(推奨)。
- 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"
ネットワークとファイアウォール
api.twilio.comおよびconversations.twilio.comへの HTTPS アウトバウンド通信を許可します。- インバウンドの Webhook エンドポイントは Twilio の IP 範囲からの通信を受け入れる必要があります(または署名を検証します。IP の許可リストより署名検証を推奨します)。
主な概念
Conversations の基本モデル
- Conversation:スレッドのコンテナです。
sid(CH...)、uniqueName、属性、タイマー、状態を持ちます。 - Participant:Conversation 内のエンティティです。種類は次のとおりです:
- Chat participant(identity ベース):
identity="user-123" - Messaging participant(電話番号ベース):
messagingBinding.address="+14155550100"とproxyAddress(Twilio 番号または Messaging Service の送信者) - WhatsApp participant:
whatsapp:+14155550100のようなアドレス
- Chat participant(identity ベース):
- Message:Conversation 内で送信されるコンテンツです。作成者、本文、メディア、属性、配信確認(チャネルに依存)を持ちます。
- Webhook イベント:Twilio は、Conversation/メッセージ/参加者のライフサイクルと配信状況について HTTP コールバックを送信します。
アーキテクチャの概要(本番環境)
- Ingress(受信):
- インバウンドの SMS/WhatsApp は Programmable Messaging の Webhook に到達します。
- アプリ内チャットでは Conversations SDK(Web/モバイル)または REST API を使用します。
- Routing(ルーティング):
- インバウンドメッセージを Conversation に対応付けます(電話番号、identity、またはビジネスキーを使用)。
- 参加者を追加または確保します。
- Egress(送信):
- Conversations API(統一されたスレッドには推奨)または Messaging API(スレッド化しない一斉送信用)を介してメッセージを送信します。
- State & observability(状態と可観測性):
- Webhook イベントを追記専用で永続化し、状態(配信、既読、失敗)を導出します。
- Twilio の再試行に対応するため、冪等性キーを使用します。
- Compliance(コンプライアンス):
- STOP/START/HELP の処理(Messaging コンプライアンス)と、参加者の削除/ブラックリスト登録を行います。
- 米国向けトラフィックでは 10DLC/トールフリーの認証を行い、WhatsApp のテンプレートルールに従います。
主要な識別子
- Account SID:
AC... - Conversation SID:
CH... - Participant SID:
MB...(変動します) - Message SID:
IM... - Service SID(Conversations Service):
IS...(Service を使用する場合) - Messaging Service SID:
MG...
Webhook 署名の検証
Twilio は Webhook リクエストに X-Twilio-Signature で署名します。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(推奨。イベントの取り込み用)
- 次の URL を指定します:
https://your-domain.example.com/twilio/conversations/webhook
受信 SMS/WhatsApp を Conversations に取り込む場合、独自のマッピングを行うなら、Programmable Messaging の受信 Webhook もアプリに設定してください。または、Conversations の messaging bindings を使用してください(こちらを推奨)。
主な機能
Conversations の作成と管理
- 冪等な検索ができるよう、
uniqueNameを指定して会話を作成します。 - ビジネスメタデータ(tenantId、caseId、SLA)用の JSON を
attributesに設定します。 state(active/inactive/closed)、タイマー、クリーンアップによってライフサイクルを制御します。
参加者の追加(チャット+メッセージング+WhatsApp)
- チャット参加者には
identity(SDK ユーザー用)を指定します。 - メッセージング参加者には
messagingBinding.addressとproxyAddress(Twilio の送信者)を指定します。 - WhatsApp のアドレスには
whatsapp:+E164を使用します。
本番環境でのパターン:
- ビジネスエンティティから会話の uniqueName への決定的なマッピングを必ず確保します。
- 参加者を追加する前に、参加者数の上限とブロックリストを適用します。
配信追跡付きメッセージの送信
authorとbodyを指定してメッセージを送信します。- 関連付け ID 用の
attributesを付加します。 - 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(Dial/Conference)にエスカレーションし、通話の成果物を会話に投稿します。
- 参加者の追加や機密情報の開示前に、段階的認証として Verify を使用します。
- 会話イベントをトリガーに Studio フローを実行します。
- SendGrid でトランザクションメールを送信し、メッセージ/属性として会話にも反映します。
コマンドリファレンス
このスキルでは、OpenClaw が次の方法で実行できることを前提としています。
- Twilio SDK(Node/Python)
- REST の直接呼び出し(curl)
- Twilio CLI(オプション。Conversations の対応範囲は限定的)
REST API のベース URL
- Conversations API のベース URL:
https://conversations.twilio.com/v1
認証オプション:
- API Key を使用した Basic 認証:
- ユーザー名:
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"
参加者
チャット参加者を追加する(ID)
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(文字列。ID またはシステムラベル)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 では、通常、デフォルト設定と Webhook の設定に Service を使用します。
- サービスを一覧表示:
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 → Conversation にマッピング → エージェントが返信
パイプライン:
- Programmable Messaging の受信 Webhook が SMS を受信します。
uniqueName = "sms:+14155550100"(またはテナントスコープ)で Conversation を検索/作成します。MessagingBinding.Address= 送信者として、メッセージング参加者が存在することを確認します。- 受信メッセージを Conversation に投稿します(作成者 = 電話番号)。
- エージェントは Conversations のメッセージ API 経由で返信します。
重要な点:受信 SMS はすでに Messaging イベントとして存在するため、統合されたスレッドにするために Conversations にミラーリングします。送信メッセージを二重に送信しないようにしてください。
パターン:記録の基準システムとしての Conversations +送信に Messaging Service を使用
- UI/履歴用に、すべてのメッセージを Conversations に保存します。
- 大量の送信 SMS には、地理的な送信元のマッチングとスループットを実現するため、
MessagingServiceSid=MG...を指定して Messaging API 経由で送信します。 - Messaging の
MessageSidへのリンクを含む属性を付けて、送信メッセージを Conversations にミラーリングします。
これにより、単一のスレッドを維持しながら、送信者の管理を複雑にせずに済みます。
パターン:新しい電話番号の参加者を追加する前に確認
- ユーザーが会話への電話番号の追加をリクエストします。
- その番号に Verify V2 の SMS を送信します。
- 確認が成功した後にのみ、メッセージング参加者として追加します。
パターン:Voice にエスカレーションし、通話録音/文字起こしを添付
- 会話でエスカレーションキーワードが検出されたら、Voice の通話(TwiML Dial/Conference)を作成します。
- 録音と文字起こしを有効にします。
- 録音 URL/文字起こしをメッセージまたは会話属性として会話に投稿します。
パターン:会話イベントによってトリガーされる Studio フロー
- イベント後の Webhook が
onMessageAddedを取り込みます。 - 特定のインテントに対して、Studio REST Trigger API を呼び出してフローを実行します。
- フローは Conversations または Messaging 経由でメッセージを送信できます。
エラー処理とトラブルシューティング
Twilio のエラーは、コード、HTTP ステータス、再試行可能かどうかに基づいて処理します。必ず以下をログに記録してください。
- Twilio の
X-Twilio-Request-Id - HTTP ステータス
- エラーコード/メッセージ
- 関連する SID
1) 認証エラー(20003)
エラー(一般的な例):
- HTTP 401
- 本文に以下が含まれます:
"code": 20003"message": "Authenticate"
根本原因:
- API キーシークレットまたは認証トークンが誤っているか、SDK の設定で
accountSidを指定せずに API キーを使用しています。
修正方法:
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) メッセージ配信の失敗(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-*経由で元のホスト/プロトコルを保持し、正しく再構築します。- コンテンツタイプとボディの解析方法を確認します。
6) 参加者がすでに存在します
一般的な API エラー:
- HTTP 409
- 次のようなメッセージ:
"message": "Participant already exists"
根本原因:
- リトライや同時実行時に、べき等でない参加者追加リクエストが行われています。
対処方法:
- 参加者が目的のバインディングと一致する場合は、409 を成功として扱います。
- 作成前に、決定的な方法で参加者を検索します(参加者を一覧表示し、ID/アドレスで照合します)。
- 会話ごとに参加者の作成を直列化します。
7) 会話が見つかりません
エラー:
- HTTP 404
"message": "The requested resource /Conversations/CH... was not found"
根本原因:
- SID が間違っている、会話が削除されている、または別アカウントの SID が使われています。
対処方法:
- SID のプレフィックスと長さを検証します。
- 正しいアカウント認証情報が使用されていることを確認します。
uniqueNameを使用している場合は、一覧表示/フィルタリングで再度解決するか、マッピングを DB に保存します。
8) Webhook のタイムアウトとリトライ
症状:
- Twilio が同じイベントを複数回リトライし、ログに重複が記録されます。
根本原因:
- Webhook ハンドラーが Twilio のタイムアウトを超過するか、2xx 以外のステータスコードを返しています。
- 下流の依存サービス(DB)の応答が遅くなっています。
対処方法:
- イベントをキューに追加した直後に 200 を返します。
- 永続キュー(SQS/Kafka/RabbitMQ)を使用し、非同期で処理します。
EventSidをキーとし、有効期限を 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 Benchmark:
- SSH のパスワード認証を無効にし、鍵ベースの認証を必須にします。
- セキュリティ更新の自動適用を有効にします。
- Webhook エッジへの受信ポートを 443 のみに制限します。
- サービスは root 以外のユーザー(
openclawユーザー)で実行します。 - systemd のサンドボックス化:
NoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=true
監査ログ
- すべての管理者操作を記録します:
- 会話の作成/終了/削除
- 参加者の追加/削除
- 送信メッセージの送信
- 相関 ID と実行者の ID を含めます。
パフォーマンスのチューニング
Webhook 取り込みのレイテンシ
目標:Webhook ハンドラーの p95 を 50ms 未満、常に 500ms 未満にします。
最適化:
- 署名を解析して検証したら、キューに登録して 200 を返します。
- リクエスト処理スレッドでの同期的な DB 書き込みは避けます。
期待される効果:
- 最適化前:DB の競合時に 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 を使用して、次の処理を行います。
- 不明な ID からの参加者追加をブロックします。
- テナント境界を適用します(ID は会話の属性と一致する必要があります)。
- 許可されていないコンテンツ(PII の漏えい)を含むメッセージを、受理される前に拒否します。
実装上の注意:
- イベント前 Webhook は高可用性である必要があります。障害が発生すると、メッセージの流れがブロックされる場合があります。
- Twilio のイベント前 Webhook の仕様に従い、許可または拒否を明示的に返します。
冪等な会話の作成
Twilio は、並行する呼び出し間で「uniqueName による、存在しない場合のみ作成」の原子性を保証しません。
パターン:
UniqueNameを指定して作成を試みます。- 競合または重複が発生した場合は、マッピング DB(推奨)または一覧取得/フィルター(代替手段)を使用して、
UniqueNameで検索します。 - マッピングを保存します。
マルチテナントの分離
uniqueNameにテナントをプレフィックスとして付けます:t_acme_case_10492attributesにtenantIdを保存します。- 処理前に、すべての Webhook イベントでテナントを検証します。
メッセージの順序と結果整合性
- Webhook イベントは順不同で到着する場合があります。
- イベントのタイムスタンプと、メッセージインデックス(提供されている場合)を使用して順序を決定します。
- Webhook をイベントストリームとして扱い、リプレイ機能を備えた派生状態を構築します。
メディアの処理
- メディアメッセージには、個別の取得ポリシーと保存ポリシーが必要な場合があります。
- 許可するコンテンツタイプのリストとサイズ制限を適用します。
- 受信したメディアを社内ユーザーに公開する前に、ウイルススキャンを検討してください。
Programmable Messaging のステータスコールバックとの相互運用
次のイベントを受信する場合があります。
- Conversations のイベント後 Webhook(メッセージ追加)
- Messaging のステータスコールバック(配信済み/失敗)
次の方法で相関付けを行い、統合します。
- Messaging API 経由で送信する際に、Messaging の
MessageSidを Conversations のメッセージ属性に保存します。 - ステータスコールバック時に、内部のメッセージ状態を更新し、必要に応じてシステムメッセージを会話に投稿します(または外部 UI を更新します)。
使用例
シナリオ 1:サポートケースの会話を作成し、SMS の顧客とチャットエージェントを追加して、最初のメッセージを送信する
# 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 をキーとする冪等性(Redis)を用いた Webhook の取り込み
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 のバッチジョブ:
- 会話を一覧表示し、最終アクティビティでフィルタリングして(DB または利用可能な場合は 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(ID と連絡先のバインディングを使ったオムニチャネルメッセージング)