twilio-sms について
twilio-smsは、Twilio Programmable Messagingを使うシステムを構築するOpenClaw向けの実践ガイドです。SMS/MMS送信、Webhook、配信追跡、コンプライアンス、運用上の対策を扱います。
主な機能
- 01SMSとMMSの送信
- 02受信メッセージとTwiML返信の処理
- 03メッセージ配信状況の追跡
- 04STOPによる配信停止とキーワード処理
- 05Webhook署名の検証と冪等性の確保
- 06A2P 10DLC要件の確認
活用シーン
- 01顧客通知パイプラインの構築
- 02双方向SMSサポートの実装
- 03配信失敗の診断
- 04配信停止やWebhook再試行のリスク低減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ alphaonedev/ openclaw-graph --skill twilio-smsインストール後、Claude Code で/twilio-smsを入力して読み込まれたか確認します。
詳細
twilio-sms
目的
OpenClaw が Twilio Programmable Messaging(SMS/MMS)を本番環境で実装・運用できるようにします。
- SMS/MMS を確実に送信します(Messaging Services、地域マッチング、送信者の固定、メディアの制約)。
- Webhook 経由で受信メッセージを受け取り、TwiML で応答します。
- ステータスコールバックで配信のライフサイクルを追跡します(queued/sent/delivered/undelivered/failed)。
- オプトアウト/STOP への準拠とキーワードのワークフローを実装します。
- A2P 10DLC(米国のロングコード)とトールフリー認証の制約に対応します。
- デバッグと堅牢化を行います:署名の検証、再試行、冪等性、レート制限、通信事業者のエラー。
このスキルは、メッセージングパイプライン、顧客通知、双方向サポート、コンプライアンスが重視されるメッセージングを構築するエンジニア向けです。
前提条件
アカウントと Twilio Console の設定
- Programmable Messaging が有効な Twilio アカウント。
- 次のいずれかを少なくとも 1 つ用意します。
- 送信者プール(ロングコード/トールフリー/ショートコード)を備えた Messaging Service(推奨)。
- SMS/MMS に対応した専用の Phone Number。
- 米国の A2P 10DLC の場合:
- Twilio Console(Messaging → Regulatory Compliance / A2P 10DLC)でブランドとキャンペーンの登録を完了します。
- トールフリーの場合:
- 米国/カナダに大量送信する場合は、トールフリー認証を申請し、承認を受けます。
ランタイムのバージョン(テスト済み)
- Node.js 20.11.1(LTS)+ npm 10.2.4
- Python 3.11.7
- Twilio SDK:
twilio(Node)4.23.0twilio(Python)9.4.1
- Web フレームワークの例:
- Express 4.18.3
- FastAPI 0.109.2 + Uvicorn 0.27.1
- オプションのツール:
- Twilio CLI 5.16.0
- ngrok 3.13.1(ローカルでの Webhook トンネリング)
- Docker 25.0.3 + Compose v2 2.24.6
認証情報と認証
必要なもの:
TWILIO_ACCOUNT_SID(AC...で始まります)TWILIO_AUTH_TOKEN- 次のいずれか:
TWILIO_MESSAGING_SERVICE_SID(MG...で始まります)推奨TWILIO_FROM_NUMBER(E.164 形式、例:+14155552671)
シークレットはシークレットマネージャー(AWS Secrets Manager/GCP Secret Manager/Vault)に保存してください。ローカル開発では .env を使用できます。
ネットワークと Webhook の要件
- 受信メッセージとステータスコールバック用の公開 HTTPS エンドポイント。
- エンドポイントに応じて、
application/x-www-form-urlencodedと JSON のいずれか、または両方を受け付ける必要があります(Twilio のデフォルトはapplication/x-www-form-urlencoded)。 - 受信 Webhook で Twilio の署名(
X-Twilio-Signature)を検証します。
コア概念
Programmable Messaging のオブジェクト
- Message:送信または受信した単一の SMS/MMS。
MessageSid(SM...)で識別されます。 - Messaging Service:送信者を抽象化したもので、次の機能をサポートします。
- 送信者プール
- 地域マッチング
- 送信者の固定
- スマートエンコーディング
- ステータスコールバックの設定
- Status Callback:メッセージの状態が変化した際に呼び出される Webhook。
- Inbound Webhook:Twilio があなたの番号またはサービス宛ての受信メッセージを受け取った際に呼び出される Webhook。
配信のライフサイクル(実務上)
一般的に確認できる MessageStatus の値:
queued→sending→sent→delivered- 失敗時の状態:
undelivered(通信事業者による拒否/到達不能)failed(設定や認証の問題により Twilio が送信できなかった)
sent は「通信事業者に引き渡された」という意味であり、「配信済み」ではありません。
メッセージング用 TwiML
受信 SMS/MMS の Webhook は、TwiML で応答できます。
<Response>
<Message>Thanks. We received your message.</Message>
</Response>
同期応答には TwiML を、非同期ワークフローには REST API を使用してください。
オプトアウトへの準拠
- Twilio は標準的なオプトアウトキーワード(例:
STOP、UNSUBSCRIBE)を自動的に処理します。 - ユーザーがオプトアウトすると、そのユーザーが再びオプトイン(
START)するまで、Twilio はその送信者またはサービスから該当の受信者への以後のメッセージをブロックします。 - アプリでは次の対応を行ってください。
- オプトアウトを最優先で扱う状態として管理する
- ブロックされた受信者への過剰な再試行を避ける
- オプトアウトした番号への送信を記録し、抑止する
A2P 10DLC/ショートコード/トールフリー
- 米国の A2P 10DLC:米国のロングコードを使って大規模にアプリケーションから個人へメッセージを送信する場合に必要です。未登録のトラフィックはフィルタリングまたはブロックされる場合があります。
- ショートコード:スループットは高い一方、費用がかかり、プロビジョニングに時間がかかります。
- トールフリー:米国/カナダ向けに適しています。認証を受けることで、配信率とスループットが向上します。
Webhook の再試行と冪等性
Twilio は 2xx 以外の応答に対して Webhook を再試行します。Webhook ハンドラーには、次の特性が必要です。
- 冪等であること(
MessageSid/SmsSidで重複を排除する) - 高速であること(すばやく応答し、処理をキューに登録する)
- 回復性があること(受け付けたら 2xx を返す)
インストールとセットアップ
公式 Python SDK — Messaging
リポジトリ: https://github.com/twilio/twilio-python
PyPI: pip install twilio · 対応バージョン: Python 3.7–3.13
from twilio.rest import Client
client = Client() # TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN from env
# Send SMS
msg = client.messages.create(
body="Hello from Python!",
from_="+15017250604",
to="+15558675309"
)
print(msg.sid)
# List recent messages
for m in client.messages.list(limit=20):
print(m.body, m.status)
ソース:twilio/twilio-python — messages
Ubuntu 22.04(x86_64 / ARM64)
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jq
NodeSource 経由の Node.js 20:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # v20.11.1 (or later 20.x)
npm -v # 10.2.4 (or later)
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 -fsSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
| sudo gpg --dearmor -o /usr/share/keyrings/ngrok.gpg
echo "deb [signed-by=/usr/share/keyrings/ngrok.gpg] 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 / ARM64)
sudo dnf install -y curl jq nodejs python3.11 python3.11-pip
node -v
python3.11 --version
Twilio CLI:
sudo npm install -g [email protected]
twilio --version
macOS(Intel + Apple Silicon)
Homebrew:
brew update
brew install node@20 [email protected] jq
PATH を確認します。
echo 'export PATH="/opt/homebrew/opt/node@20/bin:/opt/homebrew/opt/[email protected]/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
node -v
python3.11 --version
Twilio CLI:
npm install -g [email protected]
twilio --version
ngrok:
brew install ngrok/ngrok/ngrok
ngrok version
Docker(すべてのプラットフォーム)
docker --version
docker compose version
Twilio CLI の認証
対話形式でログインします。
twilio login
または環境変数を設定します(CI)。
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_AUTH_TOKEN="your_auth_token"
確認します。
twilio api:core:accounts:fetch --sid "$TWILIO_ACCOUNT_SID"
ローカル Webhook のトンネリング(ngrok)
ngrok http 3000
# note the https forwarding URL, e.g. https://f3a1-203-0-113-10.ngrok-free.app
Twilio の受信 Webhook を次のように設定します。
https://.../twilio/inbound- ステータスコールバック:
https://.../twilio/status
主な機能
SMS/MMS の送信(REST API)
- 本番環境では Messaging Service(
messagingServiceSid)を使用します。 - 配信確認には
statusCallbackを使用します。 - キャリアからのフィードバックには(対応している場合)
provideFeedback=trueを使用します。
Node(SMS):
import twilio from "twilio";
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const msg = await client.messages.create({
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
to: "+14155550123",
body: "Build 742 deployed. Reply STOP to opt out.",
statusCallback: "https://api.example.com/twilio/status",
provideFeedback: true,
});
console.log(msg.sid, msg.status);
Python(MMS):
from twilio.rest import Client
import os
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
msg = client.messages.create(
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
to="+14155550123",
body="Here is the incident screenshot.",
media_url=["https://cdn.example.com/incidents/INC-2048.png"],
status_callback="https://api.example.com/twilio/status",
)
print(msg.sid, msg.status)
MMS の本番環境での制約:
- メディアは HTTPS 経由で公開アクセス可能である必要があります。
- コンテンツタイプとサイズの上限はキャリアによって異なります。可能であれば、画像は小さく(< 500KB)してください。
- 非公開の場合は、有効期限(TTL)が十分に長い(>= 1 時間)署名付き URL を使用します。
受信 SMS/MMS(Webhook)
受信 Webhook には、次のようなフォーム形式のフィールドが含まれます。
From、To、BodyMessageSid(またはレガシーのSmsSid)NumMedia、MediaUrl0、MediaContentType0、...
署名検証を行う Express ハンドラー:
import express from "express";
import twilio from "twilio";
const app = express();
// Twilio sends application/x-www-form-urlencoded by default
app.use(express.urlencoded({ extended: false }));
app.post("/twilio/inbound", (req, res) => {
const signature = req.header("X-Twilio-Signature") || "";
const url = "https://api.example.com/twilio/inbound"; // must match public URL exactly
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
if (!isValid) {
return res.status(403).send("Invalid signature");
}
const from = req.body.From;
const body = (req.body.Body || "").trim();
// Fast path: respond immediately; enqueue work elsewhere
const twiml = new twilio.twiml.MessagingResponse();
if (body.toUpperCase() === "HELP") {
twiml.message("Support: https://status.example.com. Reply STOP to opt out.");
} else {
twiml.message("Received. Ticket created.");
}
res.type("text/xml").send(twiml.toString());
});
app.listen(3000);
FastAPI ハンドラー:
from fastapi import FastAPI, Request, Response
from twilio.request_validator import RequestValidator
from twilio.twiml.messaging_response import MessagingResponse
import os
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 = "https://api.example.com/twilio/inbound"
if not validator.validate(url, dict(form), signature):
return Response("Invalid signature", status_code=403)
body = (form.get("Body") or "").strip()
resp = MessagingResponse()
resp.message("Received.")
return Response(str(resp), media_type="text/xml")
配信確認(ステータスコールバック Webhook)
ステータスコールバックには、次の情報が含まれます。
MessageSidMessageStatus(queued、sent、delivered、undelivered、failed)To、FromErrorCode(例:30003)ErrorMessage(含まれる場合があります)
Express:
app.post("/twilio/status", express.urlencoded({ extended: false }), async (req, res) => {
// Validate signature same as inbound; use exact public URL
const messageSid = req.body.MessageSid;
const status = req.body.MessageStatus;
const errorCode = req.body.ErrorCode ? Number(req.body.ErrorCode) : null;
// Idempotency: upsert by messageSid + status
// Example: write to DB with unique constraint (messageSid, status)
console.log({ messageSid, status, errorCode });
res.status(204).send();
});
運用上のガイダンス:
- コールバックは少なくとも 1 回配信されるものとして扱います。
- 状態遷移を永続化し、順序が保証されると想定しないでください。
- コールバックを次の用途に使用します。
- 通知を配信済みとしてマークする
undelivered/failedの場合に代替チャネル(メール/プッシュ)を起動する- キャリア/地域別の配信可能性メトリクスを算出する
オプトアウト/STOP の処理
Twilio は、オプトアウトした宛先へのメッセージを自動的にブロックします。システムでは次の対応が必要です。
- 受信メッセージ内のオプトアウトキーワードを検出し、自システムの連絡先設定を更新します。
- 21610 エラーが繰り返し発生しないよう、オプトアウトした宛先への送信を抑止します。
受信キーワードの処理:
const normalized = body.trim().toUpperCase();
const isStop = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"].includes(normalized);
const isStart = ["START", "YES", "UNSTOP"].includes(normalized);
if (isStop) {
// mark user opted out in your DB
}
if (isStart) {
// mark user opted in
}
送信時には、事前にデータベースでオプトアウト状態を確認します。それでも Twilio にブロックされた場合は、エラーコード 21610 を処理します。
Messaging Services:送信者プール、地域照合、送信者固定
Messaging Service を使用すると、次のことができます。
Fromのハードコードを避ける- 送信者を安全にローテーションする
- 地域照合(地域に応じた番号の使用)を有効にする
- 送信者固定(宛先ごとに一貫した
Fromを使用)を有効にする
サービスを作成します(CLI)。
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"
サービスに電話番号を追加します。
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdef
送信者固定/地域照合を有効にします(API 経由。CLI での対応状況はバージョンによって異なります)。
twilio api:messaging:v1:services:update \
--sid YOUR_MG_SID \
--sticky-sender true \
--area-code-geomatch true
A2P 10DLC の運用チェック(米国)
コードで適用すること:
- 米国の番号(
+1...)宛てに米国のロングコードから送信する場合:- 送信元が承認済みの A2P キャンペーンに関連付けられていることを確認します。
- メッセージの内容がキャンペーンのユースケースに合致していることを確認します(内容の逸脱を避けます)。
- フィルタリングを監視します。
30003/30005およびundeliveredの割合の上昇- キャリアによる違反判定およびスパムフラグ
登録状態の信頼できる情報源は Twilio Console です。CI/CD では、キャンペーン ID とサービス SID を設定値として扱います。
ショートコードとフリーダイヤル番号
- ショートコード:スループットが高く、到達率も最も優れていますが、開通までに時間がかかります。
- フリーダイヤル番号:米国/カナダで有効です。規模を拡大するには認証が必要です。
API レベルでの実装は同じです。違いはプロビジョニングとコンプライアンスにあります。
Webhook のセキュリティ:署名の検証と URL の正確性
次の場合、署名の検証が正常に機能しないことがあります。
- 間違った URL を使って検証している(Twilio が使用した公開 URL と一致させる必要があります)。
- プロキシがスキーム/ホストを変更している。
- Twilio の想定と異なる方法で本文を解析している。
リバースプロキシ(ALB/NGINX)の背後にある場合は、転送ヘッダーを慎重に使って公開 URL を再構築するか、ルートごとに既知の公開 URL をハードコードしてください。
コマンドリファレンス
Twilio CLI(5.16.0)
認証/環境
twilio login
twilio profiles:list
twilio profiles:use <profile>
単一のコマンドに環境変数を設定します。
TWILIO_ACCOUNT_SID=AC... TWILIO_AUTH_TOKEN=... twilio api:core:accounts:fetch --sid AC...
メッセージを送信する(CLI)
Twilio CLI には twilio api:core:messages:create コマンド(コア API)があります。よく使われるフラグ:
twilio api:core:messages:create \
--to "+14155550123" \
--from "+14155552671" \
--body "Deploy complete." \
--status-callback "https://api.example.com/twilio/status" \
--provide-feedback true \
--max-price 0.015 \
--application-sid AP0123456789abcdef0123456789abcdef
フラグに関する注意:
--to(必須):宛先の E.164 番号。--from:送信元番号(E.164)。代わりに Messaging Service を使うことを推奨します。--messaging-service-sid MG...:サービスを使用します。--fromとは同時に指定できません。--body:SMS テキスト。--media-url:複数回指定可能。MMS メディア URL。--status-callback:ステータス更新用の Webhook。--provide-feedback:キャリアへのフィードバックを要求します(常に利用できるわけではありません)。--max-price:メッセージの上限価格(USD)を設定します。低すぎると失敗することがあります。--application-sid:TwiML アプリとの関連付けに使用します(Messaging ではまれです)。
MMS の例:
twilio api:core:messages:create \
--to "+14155550123" \
--messaging-service-sid YOUR_MG_SID \
--body "Photo" \
--media-url "https://cdn.example.com/a.png" \
--media-url "https://cdn.example.com/b.jpg"
メッセージを取得します。
twilio api:core:messages:fetch --sid SM0123456789abcdef0123456789abcdef
メッセージを一覧表示します(フィルターは異なる場合があります。以下は主なものです)。
twilio api:core:messages:list --limit 50
twilio api:core:messages:list --to "+14155550123" --limit 20
twilio api:core:messages:list --from "+14155552671" --limit 20
メッセージレコードを削除します(まれなケースで、主にクリーンアップ/テスト用です)。
twilio api:core:messages:remove --sid SM0123456789abcdef0123456789abcdef
Messaging Services
作成:
twilio api:messaging:v1:services:create \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status"
更新:
twilio api:messaging:v1:services:update \
--sid YOUR_MG_SID \
--friendly-name "prod-notifications" \
--status-callback "https://api.example.com/twilio/status" \
--inbound-request-url "https://api.example.com/twilio/inbound" \
--inbound-method POST
一覧表示:
twilio api:messaging:v1:services:list --limit 50
取得:
twilio api:messaging:v1:services:fetch --sid YOUR_MG_SID
サービスに関連付けられた電話番号:
twilio api:messaging:v1:services:phone-numbers:list \
--service-sid YOUR_MG_SID \
--limit 50
番号を関連付ける:
twilio api:messaging:v1:services:phone-numbers:create \
--service-sid YOUR_MG_SID \
--phone-number-sid PN0123456789abcdef0123456789abcdef
サービスから番号を削除する:
twilio api:messaging:v1:services:phone-numbers:remove \
--service-sid YOUR_MG_SID \
--sid PN0123456789abcdef0123456789abcdef
着信電話番号(PN SID の検索)
番号を一覧表示します。
twilio api:core:incoming-phone-numbers:list --limit 50
取得:
twilio api:core:incoming-phone-numbers:fetch --sid PN0123456789abcdef0123456789abcdef
番号の Webhook を更新します(Messaging Service の着信 URL を使用しない場合)。
twilio api:core:incoming-phone-numbers:update \
--sid PN0123456789abcdef0123456789abcdef \
--sms-url "https://api.example.com/twilio/inbound" \
--sms-method POST \
--sms-fallback-url "https://api.example.com/twilio/fallback" \
--sms-fallback-method POST \
--status-callback "https://api.example.com/twilio/status"
設定リファレンス
環境変数
推奨する変数:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENTWILIO_MESSAGING_SERVICE_SID(推奨)TWILIO_FROM_NUMBER(サービスを使用しない場合のみ)TWILIO_STATUS_CALLBACK_URLTWILIO_INBOUND_WEBHOOK_PUBLIC_URL(署名検証用)
Node.js .env
パス:/srv/app/.env(Linux)またはローカル開発用のプロジェクトルート。
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_AUTH_TOKEN=0123456789abcdef0123456789abcdef
TWILIO_MESSAGING_SERVICE_SID=YOUR_MG_SID
TWILIO_STATUS_CALLBACK_URL=https://api.example.com/twilio/status
TWILIO_INBOUND_WEBHOOK_PUBLIC_URL=https://api.example.com/twilio/inbound
dotenv で読み込みます。
npm install [email protected]
import "dotenv/config";
systemd unit(本番環境)
パス:/etc/systemd/system/messaging-api.service
[Unit]
Description=Messaging API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=messaging
Group=messaging
WorkingDirectory=/srv/messaging-api
EnvironmentFile=/etc/messaging-api/env
ExecStart=/usr/bin/node /srv/messaging-api/dist/server.js
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/messaging-api /var/log/messaging-api
AmbientCapabilities=
CapabilityBoundingSet=
[Install]
WantedBy=multi-user.target
シークレットファイルのパス:/etc/messaging-api/env(chmod 600)
sudo install -m 600 -o root -g root /dev/null /etc/messaging-api/env
/etc/messaging-api/env の例:
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_AUTH_TOKEN=0123456789abcdef0123456789abcdef
TWILIO_MESSAGING_SERVICE_SID=YOUR_MG_SID
TWILIO_STATUS_CALLBACK_URL=https://api.example.com/twilio/status
TWILIO_INBOUND_WEBHOOK_PUBLIC_URL=https://api.example.com/twilio/inbound
PORT=3000
NGINX リバースプロキシ(Webhook エンドポイント)
パス:/etc/nginx/conf.d/messaging-api.conf
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location /twilio/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 10s;
}
}
重要:署名の検証は Twilio が使用した URL に依存します。アプリが http://127.0.0.1 ではなく、外部 URL を使うようにしてください。
統合パターン
パターン:配信状況に応じたフォールバックを行う通知
パイプライン:
statusCallbackを指定して SMS を送信します。- コールバック受信時:
deliveredの場合:成功として記録します。undelivered/failedの場合:SendGrid 経由のメールまたはプッシュ通知をキューに追加します。
疑似アーキテクチャ:
- API サービス:「ユーザーに通知する」リクエストを受け付けます。
- キュー:送信ジョブを保存します。
- ワーカー:Twilio メッセージを送信します。
- Webhook サービス:ステータスコールバックを処理し、フォールバックを実行します。
例(ステータスコールバック → SQS):
// on /twilio/status
if (status === "undelivered" || status === "failed") {
await sqs.sendMessage({
QueueUrl: process.env.FALLBACK_QUEUE_URL,
MessageBody: JSON.stringify({ messageSid, to: req.body.To, reason: req.body.ErrorCode }),
});
}
パターン:チケット管理を伴う双方向サポート
受信 SMS:
- 署名を検証します。
- 送信者(
From)を正規化し、顧客にマッピングします。 - Jira/ServiceNow/Zendesk でチケットを作成します。
- TwiML を使ってチケット ID を返信します。
TwiML の返信例:
<Response>
<Message>Your ticket INC-2048 is open. Reply HELP for options.</Message>
</Response>
パターン:キーワードベースのワークフロー(HELP/STOP/START + カスタムキーワード)
HELP:サポート URL と連絡先を返します。STOP:内部の設定ストアを更新します(Twilio もブロックします)。- カスタムキーワード:
STATUS <id>、ONCALL、ACK <incident>。
解析は堅牢で、大文字と小文字を区別しないようにしてください。監査用に受信したペイロードの生データを記録します。
パターン:地域照合による複数リージョンからの送信
- 次の設定を使用して、単一の Messaging Service を使います。
- 複数リージョンにまたがる送信者プール
- 地域照合を有効化
- コンプライアンスに準拠するため、受信者の国に応じてルーティングします。
- 米国/カナダ:フリーダイヤル番号または A2P 10DLC の長い番号
- 英国:英数字の送信者 ID(対応している場合)または現地番号
- インド:DLT の制約(ここでは対象外です。別個のコンプライアンスモジュールとして扱ってください)
パターン:冪等性のある送信 API
上流システムが再試行する場合は、SMS の重複を防ぐ必要があります。
方法:
- 呼び出し元から
idempotencyKeyを受け取ります。 idempotencyKey -> MessageSidの対応関係を保存します。- キーが存在する場合は、既存の
MessageSidを返します。
データベース制約の例:
idempotency_keyに一意インデックスを設定します。
エラー処理とトラブルシューティング
エラーは次の 2 つの層で処理します。
- REST API 呼び出しエラー(同期)
- ステータスコールバックエラー(非同期の配信失敗)
以下に、一般的な Twilio エラーの根本原因と修正方法を示します。
1) 21211 — 無効な To 番号
エラーテキスト(一般的な例):
Twilio could not find a Channel with the specified From address
または:
The 'To' number +1415555 is not a valid phone number.
根本原因:
- E.164 形式ではありません。
- スペースや丸括弧が含まれています。
- 国番号が無効です。
修正方法:
- 送信前に E.164 形式に正規化します。
- libphonenumber で検証します。
- API の境界で、分かりやすいエラーとともに拒否します。
2) 20003 — 認証エラー
エラーテキスト:
Authenticate
または:
Unable to create record: Authenticate
根本原因:
TWILIO_AUTH_TOKENが間違っています。- テスト用認証情報を本番エンドポイントに使用している、またはその逆です。
- アカウント SID が一致していません。
修正方法:
- 実行環境の環境変数を確認します。
- トークンが漏洩した場合はローテーションします。
- CI で正しいシークレットスコープが設定されていることを確認します。
3) 20429 — レート制限超過
エラーテキスト:
Too Many Requests
根本原因:
- 急増した送信が Twilio または Messaging Service の制限を超えています。
- API のポーリングが過剰です。
修正方法:
- クライアント側でレート制限とバックオフ(ジッターを加えた指数バックオフ)を実装します。
- キューワーカーを使って送信をまとめます。
- より高いスループットを得るには、Messaging Service またはショートコードを使用します。
4) 21610 — 受信者がオプトアウト済み
エラーテキスト:
The message From/To pair violates a blacklist rule.
根本原因:
- 受信者が STOP と返信しました(またはキャリアレベルでブロックされています)。
- システムが再試行を続けています。
修正方法:
- データベースでオプトアウト済みの受信者への送信を抑止します。
- オプトインのフロー(START)を用意します。
- 21610 は再試行せず、終端エラーとして扱います。
5) 30003 — 宛先の携帯端末に到達できない/キャリア違反
エラーテキスト(一般的な例):
Unreachable destination handset
根本原因:
- 端末の電源が切れている、または圏外です。
- キャリアによるフィルタリングです。
- 番号が無効であるか、利用停止されています。
修正方法:
- 試行回数を限定した後は、再試行不可として扱います。
- フォールバックチャネルを起動します。
- キャリア/国別の急増を監視し、A2P 登録とコンテンツを確認します。
6) 30005 — 宛先の携帯端末が不明
エラーテキスト:
Unknown destination handset
根本原因:
- 番号が割り当てられていません。
- 番号移行に問題があります。
修正方法:
- 失敗が繰り返されたら、番号を無効としてマークします。
- ユーザーに電話番号の更新を依頼します。
7) 21614 — 「To」が有効な携帯電話番号ではない(MMS)
エラーテキスト:
'To' number is not a valid mobile number
根本原因:
- MMS に対応していない番号/キャリアに MMS を送信しようとしています。
- 固定電話です。
修正方法:
- MMS の対応状況を検出し、リンク付きの SMS に切り替えます。
- 事前確認が必要な場合は Lookup(Twilio Lookup API)を使用します(コストとの兼ね合いがあります)。
8) 21606 — From 番号が SMS の送信に対応していない
エラーテキスト:
The From phone number +14155552671 is not a valid, SMS-capable inbound phone number or short code for your account.
根本原因:
- 音声通話専用の番号を使用しています。
- 番号がアカウントに所有されていません。
- 送信元の設定が間違っています。
修正方法:
- 認証済みの送信元を設定した Messaging Service を使用します。
- Console または IncomingPhoneNumbers API で番号の機能を確認します。
9) Webhook 署名の検証失敗(アプリのログ)
一般的なアプリのログ:
Invalid signature
根本原因:
- URL の検証先が間違っています(http と https の違い、ホストの不一致、パスの不一致)。
- プロキシによる書き換え。
- リクエスト本文の解析方法の違い。
修正方法:
- Twilio に設定されている正確な公開 URL に対して検証します。
- フォームのペイロードには
express.urlencoded()が使用されていることを確認します。 - プロキシの背後で動作している場合は、
app.set('trust proxy', true)を設定し、URL を慎重に再構築します。
10) ステータスコールバックが実行されない
症状:
- Console ではメッセージが配信済みと表示されますが、システムではコールバックを受信できません。
根本原因:
- メッセージまたはサービスに
statusCallbackが設定されていません。 - コールバック URL が 2xx 以外を返しています。Twilio は再試行した後、あきらめます。
- ファイアウォールが Twilio の IP をブロックしています(Twilio の IP 範囲を継続して管理できない限り、IP 許可リストは使用しないでください)。
修正方法:
- Messaging Service レベルでステータスコールバックを設定します。
- エンドポイントが速やかに 2xx を返すことを確認します。
- リクエスト本文とレスポンスコードを記録し、メトリクスを追加します。
セキュリティ強化
シークレットの取り扱い
- リポジトリに
TWILIO_AUTH_TOKENを保存しないでください。 - シークレットマネージャーと、デプロイ時に注入する有効期間の短い認証情報を使用します。
- インシデント発生時はトークンをローテーションし、影響の大きい認証情報として扱います。
Webhook の検証(必須)
- インバウンドコールバックとステータスコールバックの
X-Twilio-Signatureを検証します。 - 無効な署名は 403 で拒否します。
- 最小限のメタデータのみを記録します。機密性が高い場合は、メッセージ本文全体を記録しないでください。
TLS と通信
- すべての Webhook エンドポイントで HTTPS を強制します。
- TLS 1.0/1.1 を無効にし、TLS 1.2 以降を優先します。
- NGINX を使用する場合は、強力な暗号スイートについて CIS NGINX Benchmark のガイダンスに従います(組織のベースラインに合わせて調整してください)。
リクエストの処理
- 悪用を防ぐため、リクエスト本文のサイズを制限します。
- Express:
express.urlencoded({ limit: "64kb", extended: false })
- Express:
- Webhook エンドポイントにレート制限を適用します(Twilio は再試行するため、正当な再試行をブロックしないよう注意してください)。
- 明らかな悪用をブロックする WAF ルールを使用します。ただし、IP 許可リストには依存しないでください。
データの最小化
- 必要な情報のみを保存します。
MessageSid、To、From、タイムスタンプ、ステータス、エラーコード
- メッセージの内容を保存する場合は、保存時に暗号化し、アクセスを制限します。
OS/サービスの強化(Linux)
- systemd の強化フラグを使用します(上記のユニットファイルを参照)。
- root 以外のユーザーとして実行します。
- CIS Linux Benchmarks(Ubuntu 22.04/RHEL/Fedora 相当)に従います。
- 環境ファイルの権限を制限します(
chmod 600)。 - シークレットへのアクセスを監査します。
- 適切な場合は自動セキュリティアップデートを有効にします。
- 環境ファイルの権限を制限します(
パフォーマンスのチューニング
Webhook のレイテンシ(p95)を削減する
目標:Twilio の Webhook に p95 で 200ms 未満に応答します。
対策:
- Webhook ハンドラー内で外部サービスを同期的に呼び出さないでください。
- 処理をキュー(SQS/Kafka/Redis)に追加し、直ちに 204/200 を返します。
- 事前解析と検証をすばやく行い、負荷の高いログ記録を避けます。
期待される効果:
- 対策前:Webhook ハンドラーで DB 処理とチケット作成を行うため、p95 は 1~3 秒となり、再試行によって負荷が増加します。
- 対策後:キューへの追加のみを行うため、p95 は 100~200ms 未満となり、再試行が減少し、Twilio Webhook の滞留が軽減されます。
アウトバウンド送信のスループットを拡大する
- 並行数を制御するワーカープールを使用します。
- Messaging Service/送信元の種類ごとに、トークンバケット方式のレート制限を実装します。
- より高いスループットが必要な場合は、適切な送信元(ショートコード/フリーダイヤル)を設定した Messaging Service を優先します。
期待される効果:
- 20429 の急増を防ぎ、送信が急増するパターンによるキャリアのフィルタリングを軽減します。
コストの最適化
- 適用可能な場合は、Messaging Service の地域マッチングを使用して、リージョン間のコストを削減します。
- SMS とリンクで十分な場合は、MMS を避けます。
maxPriceは慎重に使用します。低く設定しすぎると失敗が増えます。
エンコーディングとセグメント分割
- GSM-7 と UCS-2 の違いは、セグメント数とコストに影響します。
- GSM に対応していない文字(絵文字、一部のアクセント付き文字など)を送信すると、メッセージが UCS-2 に切り替わり、70 文字ごとに分割される場合があります。
対策:
- 問題がない場合は、コンテンツを正規化します。
- メッセージを短く保ち、詳細はリンクに移します。
高度なトピック
再試行やデプロイをまたいだ冪等性
Twilio REST の messages.create は、デフォルトでは冪等ではありません。ワーカーがタイムアウト後に再試行すると、二重送信が発生する可能性があります。
対策:
- アプリケーションレベルの冪等性キーを使用し、データベースに保存します。
- タイムアウト時は、独自の相関 ID を使って照会します(相関 ID を
statusCallbackのクエリ文字列に保存してください。bodyに保存するのは安全ではありません)。より望ましい方法は次のとおりです。- メッセージの作成後、ジョブ ID とメッセージ SID の対応関係を保存します。
- create 呼び出しがタイムアウトした場合、Twilio のログを確認してメッセージが作成されたか検出しようとする方法は信頼性に欠けます。「送信された可能性がある」と保守的に扱い、アラートを出すことを推奨します。
ステータスコールバックの重複への対処
Twilio は、同じステータスに対するコールバックを複数回送信したり、順不同で送信したりする場合があります。
- 単調な状態遷移を持つステートマシンで遷移を保存します。
queued<sending<sent<delivered- 終端の失敗状態:
undelivered/failed
undeliveredの後にdeliveredを受信した場合、両方を保持しますが、タイムスタンプが後であればdeliveredを最終状態として扱います(キャリアの報告の癖により、まれに発生する可能性があります)。
マルチテナントシステム
複数の Twilio サブアカウントを運用する場合:
- テナントごとの Account SID/Auth Token を保存するか、API Keys を使用します。
- テナントごとに Webhook を検証します。
- 署名の検証には、そのテナントの auth token を使用します。
To番号、またはテナント専用の Webhook URL でルーティングします。
MMS のメディア URL のセキュリティ
- Twilio はお客様の URL からメディアを取得するため、その URL にアクセスできる必要があります。
- 署名付き URL を使用する場合:
- TTL は Twilio の取得にかかる時間(数分から数十分)を超える必要があります。余裕を持たせてください。
- Cookie を必須にしないでください。
- 認証ヘッダーが必要な場合、Twilio からは指定できません。事前署名 URL を使用してください。
コンプライアンス上の注意点
- 登録されていない A2P キャンペーンからマーケティングコンテンツを送信しないでください。
- ポリシーで求められる場合は、HELP/STOP への応答を維持し、すべてのチャネルでオプトアウトを尊重してください。
- 同意に関する監査ログを保持してください(該当する場合は、タイムスタンプ、ソース、IP/ユーザーエージェントを含む)。
使用例
1) Messaging Service とステータス追跡を使用した本番環境向けの送信 SMS(Node)
ファイル:
/srv/messaging-api/src/send.js
import "dotenv/config";
import twilio from "twilio";
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
export async function sendDeployNotification({ to, buildId }) {
const msg = await client.messages.create({
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
to,
body: `Build ${buildId} deployed to prod. Reply HELP for support, STOP to opt out.`,
statusCallback: process.env.TWILIO_STATUS_CALLBACK_URL,
provideFeedback: true,
});
return { sid: msg.sid, status: msg.status };
}
実行:
node -e 'import("./src/send.js").then(m=>m.sendDeployNotification({to:"+14155550123",buildId:"742"}).then(console.log))'
2) 受信 SMS → チケット作成 → TwiML で返信(FastAPI)
/srv/messaging-api/app.py
from fastapi import FastAPI, Request, Response
from twilio.request_validator import RequestValidator
from twilio.twiml.messaging_response import MessagingResponse
import os
app = FastAPI()
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
def create_ticket(from_number: str, body: str) -> str:
# Replace with real integration
return "INC-2048"
@app.post("/twilio/inbound")
async def inbound(request: Request):
form = await request.form()
signature = request.headers.get("X-Twilio-Signature", "")
url = os.environ["TWILIO_INBOUND_WEBHOOK_PUBLIC_URL"]
if not validator.validate(url, dict(form), signature):
return Response("Invalid signature", status_code=403)
from_number = form.get("From") or ""
body = (form.get("Body") or "").strip()
ticket = create_ticket(from_number, body)
resp = MessagingResponse()
resp.message(f"Created {ticket}. Reply HELP for options. Reply STOP to opt out.")
return Response(str(resp), media_type="text/xml")
実行:
python3.11 -m venv .venv && source .venv/bin/activate
pip install fastapi==0.109.2 uvicorn==0.27.1 twilio==9.4.1 python-multipart==0.0.9
uvicorn app:app --host 0.0.0.0 --port 3000
3) ステータスコールバック → メトリクス + フォールバックのキュー登録(Express)
/srv/messaging-api/src/status.js
import express from "express";
import twilio from "twilio";
const app = express();
app.use(express.urlencoded({ extended: false, limit: "64kb" }));
app.post("/twilio/status", async (req, res) => {
const signature = req.header("X-Twilio-Signature") || "";
const url = process.env.TWILIO_STATUS_CALLBACK_PUBLIC_URL;
const ok = twilio.validateRequest(process.env.TWILIO_AUTH_TOKEN, signature, url, req.body);
if (!ok) return res.status(403).send("Invalid signature");
const { MessageSid, MessageStatus, ErrorCode, To } = req.body;
// Example: emit metric
console.log("twilio_status", { MessageSid, MessageStatus, ErrorCode });
if (MessageStatus === "failed" || MessageStatus === "undelivered") {
// enqueue fallback (pseudo)
console.log("enqueue_fallback", { to: To, messageSid: MessageSid, reason: ErrorCode });
}
res.status(204).send();
});
app.listen(3000);
4) 署名付き URL(S3 の事前署名 URL)を使用した MMS + SMS リンクへのフォールバック
疑似フロー:
- 2 時間有効な事前署名 URL を生成します。
- メディア URL を指定して MMS を送信します。
- ステータスコールバックが
21614またはundeliveredを返した場合は、リンクを含む SMS を送信します。
Python のスニペット:
msg = client.messages.create(
messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
to="+14155550123",
body="Incident screenshot attached.",
media_url=[presigned_url], # TTL >= 2h
status_callback="https://api.example.com/twilio/status",
)
フォールバック SMS の本文:
MMS failed on your carrier. View: https://app.example.com/incidents/INC-2048
5) 並行処理と抑制を使用した、オプトアウトに配慮した一括送信
- 受信者を読み込みます。
- データベースでオプトアウト済みの受信者を除外します。
- 並行数 20 で送信します。
- 21610 の場合は、オプトアウト済みとしてマークします。
Node(概略):
import pLimit from "p-limit";
import twilio from "twilio";
const limit = pLimit(20);
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
async function sendOne(to) {
try {
return await client.messages.create({
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
to,
body: "Maintenance tonight 01:00-02:00 UTC. Reply STOP to opt out.",
statusCallback: process.env.TWILIO_STATUS_CALLBACK_URL,
});
} catch (e) {
const code = e?.code;
if (code === 21610) {
// mark opted out
return null;
}
throw e;
}
}
await Promise.all(recipients.map((to) => limit(() => sendOne(to))));
インストール:
npm install [email protected] [email protected]
クイックリファレンス
| タスク | コマンド/API | 主なフラグ/フィールド |
|---|---|---|
| SMS を送信(CLI) | twilio api:core:messages:create | --to、--from または --messaging-service-sid、--body、--status-callback、--provide-feedback、--max-price |
| MMS を送信(CLI) | twilio api:core:messages:create | --media-url(繰り返し指定可能)、--body |
| メッセージを取得 | twilio api:core:messages:fetch --sid SM... | --sid |
| メッセージを一覧表示 | twilio api:core:messages:list | --to、--from、--limit |
| Messaging Service を作成 | twilio api:messaging:v1:services:create | --friendly-name、--status-callback |
| サービスのウェブフックを更新 | twilio api:messaging:v1:services:update | --inbound-request-url、--inbound-method、--status-callback |
| 電話番号をサービスに紐付け | twilio api:messaging:v1:services:phone-numbers:create | --service-sid、--phone-number-sid |
| インバウンドウェブフック | HTTP POST /twilio/inbound | X-Twilio-Signature を検証し、フォームフィールドを解析 |
| ステータスコールバック | HTTP POST /twilio/status | MessageSid、MessageStatus、ErrorCode |
グラフの関係
DEPENDS_ON
twilioSDK(Node 4.23.0/Python 9.4.1)- 公開 HTTPS イングレス(NGINX/ALB/API Gateway)
- 冪等性とメッセージ状態を管理する永続ストア(Postgres/DynamoDB)
- 非同期処理用のキュー(SQS/Kafka/Redis Streams)を推奨
COMPOSES
twilio-voice(IVR から SMS のフォローアップをトリガー可能。不在着信から SMS へ)twilio-verify(SMS による OTP。ウェブフックのインフラと署名検証のパターンを共有)sendgrid-email(配信失敗時の代替チャネル。統合通知サービス)observability(ウェブフックの遅延と配信率のメトリクス/ログ/トレース)
SIMILAR_TO
aws-sns-sms(SMS 送信と配信確認。動作の仕組みは異なります)messagebird-sms/vonage-sms(キャリアルーティングとウェブフックのパターン)firebase-cloud-messaging(配信コールバックの概念は類似していますが、チャネルは異なります)