twilio-whatsapp について
twilio-whatsappは、Twilio経由のWhatsApp Businessメッセージングを実装・運用するエンジニア向けのOpenClawスキルです。メッセージ種別、メディア、Webhook処理、同意管理、再試行、本番環境の配信制約を扱います。
主な機能
- 01セッション・テンプレートメッセージ
- 02メディア添付の処理
- 03受信・ステータスWebhook
- 04オプトイン・オプトアウトと抑止
- 05再試行に対応するコールバック処理
- 06レート制限とコスト管理
活用シーン
- 01セッション時間外の承認済みテンプレート送信
- 02受信メッセージと配信状況の処理
- 03同意記録とSTOP要求の適用
- 04WhatsApp配信前のメディア検証
インストール
コマンドをコピーしてターミナルで実行します。
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 の処理、同意の記録、地域ごとの制約)を実装します。
- レート制限、再試行、冪等性、エラーコード、Messaging Services によるコスト管理など、Twilio の本番環境の制約下で確実に運用します。
エンジニアにとっての具体的な価値:監視可能で、コンプライアンスに準拠し、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 は固定ではないため、受信元を IP 許可リストで制限せず、X-Twilio-Signature で検証してください。
- エンドポイントが再試行と順序が前後した配信に対応できるようにしてください。
コアコンセプト
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 の種類は 2 つあります。
-
受信メッセージ Webhook(ユーザーからメッセージが届いたとき):
- Twilio は、
From、To、Body、NumMedia、MediaUrl0などのフォーム形式のパラメーターを含む HTTP リクエストを送信します。
- Twilio は、
-
メッセージステータスコールバック(配信ライフサイクル):
- ステータス値:
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 + env)
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:プレフィックスが含まれていることを確認します。- ルーティングや将来的な送信者の追加に備え、
FromをハードコードするのではなくMessagingServiceSidを優先します。
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 は使用できません)。
- メディアを送信する場合は、失敗を減らすため、送信前にコンテンツタイプとサイズを検証します。
テンプレートメッセージを送信する(セッションウィンドウ外)
本番環境では、アカウントで利用可能な場合は 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"などです。 - テンプレートの承認状況とカテゴリ(utility/marketing/authentication)は、配信可能性とポリシーへの準拠に影響します。
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 が必要です。REST 呼び出しに API Keys を使用する場合でも、Webhook の署名検証には Auth Token が必要です。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
ハンドラーの要件:
- 常に速やかに(約 2 秒以内に)2xx を返します。
- 処理をジョブキュー(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>:To で絞り込みます(例:whatsapp:+14155550123)--from <string>:From で絞り込みます。--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 ハンドラーが署名を検証し、イベントをキューに登録します。
- ワーカーがイベントを処理して DB を更新し、後続のアクションを実行します。
パイプラインの例:
- Twilio →
POST /twilio/inbound - API → Kafka トピック
twilio.inbound.v1に公開 - ワーカー → 解析し、オプトアウトを適用して、会話サービスにルーティング
- 会話サービス → 応答を決定し、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を使う
- FAQやルーティングなど、リスクの低いフローにはTwilio Studioを使います。
- REST Trigger APIを使って、バックエンドからStudioフローを開始します。
- 大量送信や監査が必要なフローでは、WhatsAppの送信をコード内で行います。
エラー処理とトラブルシューティング
Twilioのエラーは、次の2つの層で処理します。
- 送信時の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呼び出しでは、
(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 設定の誤り。
対処方法:
- 十分な有効期間(10 分以上)を設定して署名します。
- WAF で Twilio のユーザーエージェントを許可するか、メディアバケットを 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 呼び出しでは、Auth Token より API キーを優先します。
- 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 Benchmark(該当する場合)。
- サービスは root 以外のユーザー(
whatsapp)として実行します。 - systemd の強化:
NoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=true
- ファイル権限:
/srv/whatsapp/.envのモードを0600にし、サービスユーザーの所有とします。
- サービスユーザーのシェルアクセスを無効にします。
/usr/sbin/nologin
WAF/リバースプロキシに関する考慮事項
- Twilio の IP 許可リストは使用せず、署名を検証してください。
- プロキシでリクエスト本文が正確に保持されるようにします。署名の検証はパラメーターの変更に敏感です。
- リクエストを変換する必要がある場合は、変換前にエッジで検証してください。
パフォーマンスのチューニング
1) Webhook のレイテンシ:キューに登録して 204 を返す
目標:
- Webhook ハンドラーのレイテンシを p95 で 50 ms 未満にする(ネットワークを除く)
- 常に 1 秒以内に応答する
期待される効果:
- Twilio の再試行と重複配信が減少します。
- バーストトラフィック時の安定性が向上します。
実装:
- 解析と署名の検証
- 最小限のイベントレコードを書き込む
- ジョブをキューに登録する
204 No Contentを返す
2) 送信スループット:同時実行数の制御
問題:
- 同時実行数が無制限だと
20429が発生し、テールレイテンシが増加します。
解決策:
- 送信者ごと、または Messaging Service ごとにトークンバケットを使用します。
- ポッドあたりの同時実行数を 20~50 から始め、観測された 20429 の発生率に応じて調整します。
期待される効果:
- レート制限エラーが減り、持続的なスループットが向上します。
3) 接続の再利用
- Twilio REST 呼び出しには HTTP keep-alive エージェント(Node)を使用します。
- Python ではクライアントを再利用し、リクエストごとに作成しないでください。
期待される効果:
- 送信量が多い場合に CPU 使用率とレイテンシが低下します。
4) 重複排除ストレージ
- Webhook の重複排除には、
SETNXと TTL を使用した Redis を使います。 - 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)本番環境:配送状況更新のテンプレートを送信し、返信を処理する
手順:
- ユーザーがWebサイトのチェックアウト時にオプトインします。
- テンプレート「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を次のURLに設定します。
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 を直接使う場合とはプリミティブが異なる)