mcpField
T
twilio-voice

twilio-voice

github.com/alphaonedev開発84 回インストール
リポジトリ

twilio-voice について

twilio-voiceは、Twilio Programmable Voiceを使った本番音声システムの構築・運用を支援するOpenClaw向けスキルです。通話フロー、IVR、録音、文字起こし、SIP接続、会議、障害対応を扱います。

主な機能

  1. 01着信・発信の通話フロー
  2. 02IVRステートマシンの指針
  3. 03録音・文字起こしパイプライン
  4. 04SIP、BYOC、エッジルーティング
  5. 05会議管理
  6. 06障害の診断と対処

活用シーン

  1. 01Webhookを使った通話処理の構築
  2. 02再試行に対応するIVRフローの実装
  3. 03録音と文字起こしの管理
  4. 04通話配信やWebhookの問題の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/alphaonedev/openclaw-graph --skill twilio-voice

インストール後、Claude Code で/twilio-voiceを入力して読み込まれたか確認します。

詳細

twilio-voice

目的

OpenClaw が Twilio Programmable Voice を本番環境で設計、実装、運用できるようにします。

  • TwiML(<Dial>、<Conference>、<Gather>、<Record>、Amazon Polly 音声を使用する <Say>)を使って、着信/発信の通話フローを構築します。
  • 決定論的な遷移、再試行、冪等性を備えた IVR ステートマシンを実装します。
  • 保持制御とコンプライアンスガードレールを備えた、通話録音と文字起こしのパイプラインを運用します。
  • SIP トランキング/SIP インターフェース、BYOC(Bring Your Own Carrier)、エッジルーティングを統合します。
  • 大規模な会議通話を運用します(参加者管理、モデレーション、録音、イベント)。
  • Twilio Voice でよく発生する障害(認証、無効な電話番号、レート制限、キャリアの問題、Webhook の再試行)を診断し、修復します。

このスキルは、厳格な信頼性、可観測性、セキュリティ要件を満たす本番環境の音声システムを開発、保守するエンジニア向けです。


前提条件

アカウントと Twilio Console の設定
  • 次の情報を備えた Twilio アカウント:
    • Account SID(AC...)
    • Auth Token(シークレットとして扱ってください)
    • 少なくとも 1 つの Voice 対応電話番号(E.164)または SIP ドメイン
  • Twilio の電話番号に Voice webhook を設定します。
    • Console → Phone Numbers → Manage → Active numbers →(電話番号)→ Voice configuration
    • A CALL COMES IN に webhook URL(HTTPS)とメソッド(POST を推奨)を設定します。
    • 通話の進行状況イベント用に Status Callback を設定します(任意ですが推奨)。
ランタイムのバージョン(テスト済み)

バックエンドのスタックを 1 つ選択してください。例では Node と Python を取り上げます。

  • Node.js:20.11.1 LTS(または 18.19.1 LTS)
  • Python:3.11.8(または 3.10.13)
  • Twilio ヘルパーライブラリ
  • ngrok(ローカルでの webhook 開発用):3.14.2
  • Docker(任意):25.0.3
  • OpenSSL:3.0.x(Linux)、3.2.x(Homebrew 経由の macOS)
OS のサポート
  • Ubuntu 22.04 LTS(x86_64、arm64)
  • Fedora 39/40
  • macOS 14 Sonoma(Intel および Apple Silicon)
ネットワークと DNS
  • Twilio webhook 用のパブリック HTTPS エンドポイント(TLS 1.2 以上)。
  • SIP を使用する場合:
    • パブリックから到達可能な SIP インフラストラクチャ、または Twilio SIP Domains。
    • SIP シグナリング/メディア用のファイアウォールルール(または、セキュアなシグナリングを備えた Twilio Elastic SIP Trunking を使用)。
シークレット管理
  • TWILIO_ACCOUNT_SID と TWILIO_AUTH_TOKEN は、シークレットマネージャーに保存します。
    • AWS Secrets Manager / GCP Secret Manager / Vault / 1Password CLI
  • 認証情報を git にコミットしないでください。
  • インシデント対応時またはスタッフの変更時には、Auth Token をローテーションしてください。

コア概念

TwiML の実行モデル
  • 通話の着信時(または TwiML URL を指定して発信通話を開始したとき)、Twilio は webhook をリクエストします。
  • サーバーは命令を TwiML(XML)で返します。
  • Twilio は TwiML を順次実行します。一部の動詞(例:<Dial>)は、完了するまで処理をブロックします。
  • TwiML は ステートレスです。状態は外部(DB/Redis)に保存し、パラメーター経由で参照する必要があります。
通話レッグ、SID、相関付け
  • CallSid は通話レッグ(着信レッグ、発信レッグ、子通話)を識別します。
  • <Dial> では、Twilio は発信先の相手に対して 子通話を作成することがよくあります。次の情報で相関付けます。
    • ParentCallSid(ステータスコールバック/通話ログ内)
  • ログとトレースでは、主な相関キーとして CallSid を使用します。
Webhook と再試行
  • Twilio webhook は、インフラストラクチャへの HTTP リクエストです。
  • Twilio は特定の障害(タイムアウト、5xx)で再試行します。次の実装が必要です。
    • 冪等性(CallSid + イベントタイプ + タイムスタンプで重複を排除)
    • 迅速な応答(通常の目標は 2 秒未満)と、キューへの非同期処理のオフロード
ステータスコールバック(イベント)
  • Voice のステータスコールバックイベントには、次のものがあります。
    • initiated、ringing、answered、completed
  • 会議通話と録音には、追加のコールバックがあります。
  • コールバックは 少なくとも 1 回配信されるものとして扱ってください。
IVR ステートマシン
  • <Gather> を使用して、DTMF または音声を収集します。
  • IVR をステートマシンとしてモデル化します。
    • 状態はサーバー側(Redis/DB)に保存します。
    • 入力、タイムアウト、再試行に応じて遷移します。
    • 無効な入力と入力なしの経路を必ず処理します。
録音と文字起こし
  • 録音は、<Dial>、<Conference>、または <Record> を使って有効にできます。
  • 文字起こしには、次の方法があります。
    • Twilio の文字起こし機能(利用可能な場合)
    • 外部の文字起こしパイプライン(制御性と品質の面から推奨)
  • 保持期間とアクセス制御を定義してください。録音データは機密情報です。
SIP、BYOC、エッジ
  • SIP ドメイン:クライアント向けに Twilio がホストする SIP エンドポイントです。
  • Elastic SIP Trunking:PBX/通信事業者を Twilio に接続します。
  • BYOC:自社の通信事業者を利用し、アプリと機能には Twilio を使います。
  • 遅延を低減するために、Twilio の エッジロケーション(例:ashburn、dublin、singapore)を使用してください。

インストールとセットアップ

公式 Python SDK — 音声

リポジトリ: https://github.com/twilio/twilio-python
PyPI: pip install twilio · サポート対象: Python 3.7–3.13

from twilio.rest import Client
from twilio.twiml.voice_response import VoiceResponse

client = Client()

# Outbound call
call = client.calls.create(
    url="https://demo.twilio.com/docs/voice.xml",
    to="+15558675309",
    from_="+15017250604"
)
print(call.sid)

# TwiML response (in webhook handler)
resp = VoiceResponse()
resp.say("Hello from Twilio Python!")
resp.record(transcribe=True, transcribe_callback="/transcription")

出典:twilio/twilio-python — calls

1) 依存関係をインストールする(Ubuntu 22.04)
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jq
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v   # v20.11.1
npm -v    # 10.x

Python の場合:

sudo apt-get update
sudo apt-get install -y python3.11 python3.11-venv python3-pip
python3.11 -V  # Python 3.11.8

ngrok:

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  # 3.14.2
2) 依存関係をインストールする(Fedora 39/40)
sudo dnf install -y nodejs20 jq python3.11 python3.11-pip
node -v
python3.11 -V

ngrok(手動):

curl -L -o /tmp/ngrok.tgz https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
sudo tar -C /usr/local/bin -xzf /tmp/ngrok.tgz ngrok
ngrok version
3) 依存関係をインストールする(macOS 14、Intel + Apple Silicon)

Homebrew:

brew update
brew install node@20 [email protected] jq ngrok/ngrok/ngrok
node -v
python3.11 -V
ngrok version

PATH に Homebrew の Node が含まれていることを確認してください。

echo 'export PATH="/opt/homebrew/opt/node@20/bin:$PATH"' >> ~/.zshrc  # Apple Silicon
echo 'export PATH="/usr/local/opt/node@20/bin:$PATH"' >> ~/.zshrc     # Intel
source ~/.zshrc
4) 最小構成の Voice webhook サービスを作成する(Node.js)

プロジェクトの構成:

  • /srv/twilio-voice/(Linux)または ~/src/twilio-voice/(macOS)
  • 設定ファイルは /etc/twilio/voice.env(Linux)または ./.env(ローカル開発環境)
mkdir -p ~/src/twilio-voice && cd ~/src/twilio-voice
npm init -y
npm install [email protected] [email protected] [email protected] [email protected]

server.js を作成します。

const express = require("express");
const bodyParser = require("body-parser");
const twilio = require("twilio");
const pino = require("pino");

const log = pino({ level: process.env.LOG_LEVEL || "info" });

const app = express();

// Twilio sends application/x-www-form-urlencoded by default
app.use(bodyParser.urlencoded({ extended: false }));

// Optional: validate Twilio signature (recommended in production)
const validateTwilio = (req, res, next) => {
  const authToken = process.env.TWILIO_AUTH_TOKEN;
  const signature = req.header("X-Twilio-Signature");
  const url = `${process.env.PUBLIC_BASE_URL}${req.originalUrl}`;
  const isValid = twilio.validateRequest(authToken, signature, url, req.body);
  if (!isValid) return res.status(403).send("Invalid Twilio signature");
  next();
};

app.post("/voice/inbound", validateTwilio, (req, res) => {
  const vr = new twilio.twiml.VoiceResponse();

  // Example: simple IVR greeting + gather
  const gather = vr.gather({
    input: "dtmf",
    numDigits: 1,
    timeout: 5,
    action: "/voice/menu",
    method: "POST",
  });

  gather.say(
    { voice: "Polly.Joanna", language: "en-US" },
    "Press 1 for sales. Press 2 for support."
  );

  vr.say({ voice: "Polly.Joanna", language: "en-US" }, "No input received. Goodbye.");
  vr.hangup();

  res.type("text/xml").send(vr.toString());
});

app.post("/voice/menu", validateTwilio, (req, res) => {
  const digit = req.body.Digits;
  const vr = new twilio.twiml.VoiceResponse();

  if (digit === "1") {
    vr.dial({ callerId: process.env.TWILIO_CALLER_ID }, "+14155550100");
  } else if (digit === "2") {
    vr.dial({ callerId: process.env.TWILIO_CALLER_ID }, "+14155550101");
  } else {
    vr.say({ voice: "Polly.Joanna", language: "en-US" }, "Invalid choice.");
    vr.redirect({ method: "POST" }, "/voice/inbound");
  }

  res.type("text/xml").send(vr.toString());
});

app.get("/healthz", (req, res) => res.status(200).send("ok"));

const port = Number(process.env.PORT || 3000);
app.listen(port, () => log.info({ port }, "twilio-voice server listening"));

ローカルで実行します。

export TWILIO_AUTH_TOKEN="your_auth_token"
export PUBLIC_BASE_URL="https://example.ngrok-free.app"
export TWILIO_CALLER_ID="+14155551234"
node server.js

ngrok 経由で公開します。

ngrok http 3000
# copy the https URL into PUBLIC_BASE_URL and Twilio Console webhook
5) Python の代替方法(FastAPI)
mkdir -p ~/src/twilio-voice-py && cd ~/src/twilio-voice-py
python3.11 -m venv .venv
source .venv/bin/activate
pip install "twilio==9.0.5" "fastapi==0.109.2" "uvicorn[standard]==0.27.1"

app.py:

import os
from fastapi import FastAPI, Request, Response
from twilio.twiml.voice_response import VoiceResponse, Gather
from twilio.request_validator import RequestValidator

app = FastAPI()

def validate_twilio(request: Request, form: dict) -> None:
    auth_token = os.environ["TWILIO_AUTH_TOKEN"]
    validator = RequestValidator(auth_token)
    signature = request.headers.get("X-Twilio-Signature", "")
    url = os.environ["PUBLIC_BASE_URL"] + str(request.url.path)
    if not validator.validate(url, form, signature):
        raise PermissionError("Invalid Twilio signature")

@app.post("/voice/inbound")
async def inbound(request: Request):
    form = dict(await request.form())
    validate_twilio(request, form)

    vr = VoiceResponse()
    gather = Gather(input="dtmf", num_digits=1, timeout=5, action="/voice/menu", method="POST")
    gather.say("Press 1 for sales. Press 2 for support.", voice="Polly.Joanna", language="en-US")
    vr.append(gather)
    vr.say("No input received. Goodbye.", voice="Polly.Joanna", language="en-US")
    vr.hangup()
    return Response(content=str(vr), media_type="text/xml")

@app.post("/voice/menu")
async def menu(request: Request):
    form = dict(await request.form())
    validate_twilio(request, form)

    digit = form.get("Digits")
    vr = VoiceResponse()
    if digit == "1":
        vr.dial("+14155550100", caller_id=os.environ["TWILIO_CALLER_ID"])
    elif digit == "2":
        vr.dial("+14155550101", caller_id=os.environ["TWILIO_CALLER_ID"])
    else:
        vr.say("Invalid choice.", voice="Polly.Joanna", language="en-US")
        vr.redirect("/voice/inbound", method="POST")
    return Response(content=str(vr), media_type="text/xml")

実行します。

export TWILIO_AUTH_TOKEN="your_auth_token"
export PUBLIC_BASE_URL="https://example.ngrok-free.app"
export TWILIO_CALLER_ID="+14155551234"
uvicorn app:app --host 0.0.0.0 --port 3000

主な機能

着信の処理(TwiML webhook)
  • Twilio がリクエストした正確な URL を使って、X-Twilio-Signature を検証してください。
  • application/x-www-form-urlencoded を解析してください。
  • 正しい Content-Type: text/xml を指定して TwiML を返してください。
  • webhook の遅延を低く保ってください。データベースのマイグレーション、サードパーティ API、時間のかかる計算の完了を待たないでください。

推奨する着信 webhook の仕様:

  • エンドポイント:POST /voice/inbound
  • 次のケースに対応する必要があります。
    • パラメーターの欠落/無効
    • リクエストの繰り返し(再試行)
    • 想定外の HTTP メソッド(405 を返す)
発信(REST API + TwiML URL)
  • REST 経由で発信通話を作成します。
    • to、from、および url(TwiML URL)またはインラインの twiml を指定します。
  • ライフサイクルイベントにはステータスコールバックを使用します。
  • 遅延の増加や誤検出を許容できる場合に限り、machineDetection を使用してください。
<Gather> による IVR(DTMF + 音声)
  • DTMF は決定論的です。音声には調整とフォールバックが必要です。
  • 必ず次を実装してください。
    • timeout の経路(入力なし)
    • 無効な入力の経路
    • 最大再試行回数
  • 音声の場合:
    • speechTimeout と language を指定し、割り込みの動作も検討してください。
会議(<Conference>)
  • 次の用途には会議を使用します。
    • エージェント + 顧客 + スーパーバイザー
    • 保留なしの転送
    • 複数参加者の通話
  • 次を制御します。
    • startConferenceOnEnter
    • endConferenceOnExit
    • beep
    • record と録音コールバック
  • Conference/Participant リソースで参加者を追跡します。
通話録音
  • 選択肢:
    • <Dial record="record-from-answer">
    • <Conference record="record-from-start">
    • ボイスメールのようなフロー向けの <Record> 動詞
  • 録音のステータスコールバックを使ってメタデータを取り込み、保持期間を適用します。
  • 録音をエクスポートする場合は、保存先で保存時暗号化を行ってください。
文字起こし
  • 次の目的では、外部の文字起こしを優先してください。
    • モデルの選択
    • マスキング
    • 言語検出
    • 話者ダイアライゼーション
  • パイプライン:
    1. 録音コールバック → ジョブをキューに追加
    2. 録音メディアを取得(認証あり)
    3. 文字起こしを実行
    4. 文字起こし結果を保存し、CallSid/RecordingSid に関連付ける
SIP トランキング/SIP ドメイン/BYOC
  • SIP ドメインは、クライアント登録と Twilio アプリへの着信 SIP 接続に使用します。
  • Elastic SIP Trunking は PBX 接続に使用します。
  • BYOC では通信事業者を制御できます。アプリのロジックと機能は引き続き Twilio が提供します。
Voice SDK(ブラウザー/iOS/Android)
  • Voice グラントを含むアクセストークンを使用します。
  • トークンの TTL は短く設定し(5~15 分)、更新します。
  • デバイス登録とプッシュ通知(モバイル)を必須にします。

コマンドリファレンス

このセクションでは、Twilio CLI と一般的な運用コマンドに焦点を当てます。Twilio CLI は任意です。本番システムでは、可能な限り REST API と IaC を使用してください。

Twilio CLI のインストール
macOS(Homebrew)
brew install twilio/brew/twilio
twilio --version
Ubuntu/Debian(npm グローバル)
sudo npm install -g [email protected]
twilio --version
Fedora(npm グローバル)
sudo npm install -g [email protected]
twilio --version
Twilio CLI の認証
twilio login
# prompts for Account SID and Auth Token

環境変数を使用した非対話型(CI)認証:

export TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Voice:通話の一覧表示
twilio api:core:calls:list \
  --limit 50 \
  --properties sid,from,to,status,startDate,endDate,duration,price,priceUnit

関連フラグ:

  • --limit <n>:最大レコード数
  • --properties <csv>:フィールドを選択します(出力を簡潔にします)
Voice:通話の取得
twilio api:core:calls:fetch --sid YOUR_CA_SID

フラグ:

  • --sid <CA...>:通話 SID(必須)
Voice:発信通話の作成
twilio api:core:calls:create \
  --from +14155551234 \
  --to +14155559876 \
  --url https://voice.example.com/twiml/outbound \
  --status-callback https://voice.example.com/webhooks/voice/status \
  --status-callback-event initiated ringing answered completed \
  --status-callback-method POST \
  --timeout 20

関連フラグ(主要なもの):

  • --from <E.164|client:...|sip:...>:発信者 ID/ID
  • --to <E.164|client:...|sip:...>:宛先
  • --url <https://...>:TwiML URL(--twiml と併用できません)
  • --twiml <xml>:インライン TwiML
  • --method <GET|POST>:url の取得に使用するメソッド(デフォルトは POST を推奨)
  • --status-callback <url>
  • --status-callback-event <events...>
  • --status-callback-method <GET|POST>
  • --timeout <seconds>:呼び出しのタイムアウト
  • --machine-detection <Enable|DetectMessageEnd|...>:AMD(遅延が増加します)
  • --record:録音を有効にします(対応している場合)
  • --recording-status-callback <url>
  • --recording-status-callback-method <GET|POST>
Voice:録音の一覧表示
twilio api:core:recordings:list --limit 20 \
  --properties sid,callSid,dateCreated,duration,price,priceUnit,status
Voice:録音メタデータの取得
twilio api:core:recordings:fetch --sid RE3f3b1b2c0d0f4a5b6c7d8e9f0a1b2c3
Voice:録音メディアのダウンロード(認証あり)

Twilio CLI にはメディアを直接ダウンロードするヘルパーが常に用意されているわけではありません。基本認証を使用して curl を実行してください。

export TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
  -L "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Recordings/RE3f3b1b2c0d0f4a5b6c7d8e9f0a1b2c3.wav" \
  -o recording.wav
file recording.wav
Voice:カンファレンスの一覧表示/取得
twilio api:core:conferences:list --status in-progress --limit 20
twilio api:core:conferences:fetch --sid YOUR_CF_SID
Voice:カンファレンス参加者の一覧表示
twilio api:core:conferences:participants:list \
  --conference-sid YOUR_CF_SID \
  --limit 50
Voice:参加者の更新(ミュート/切断)
twilio api:core:conferences:participants:update \
  --conference-sid YOUR_CF_SID \
  --sid CAbcdef0123456789abcdef0123456789 \
  --muted true

フラグ:

  • --muted <true|false>
  • --hold <true|false>(対応している場合)
  • --hold-url <url>(保留中に流す音楽/アナウンス)
Twilio CLI:API リクエスト(汎用)

CLI でラップされていないエンドポイントに使用します。

twilio api:core:accounts:fetch
twilio api:request --method GET \
  --uri "/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls.json?PageSize=20"

フラグ:

  • --method <GET|POST|PUT|DELETE>
  • --uri <path>:https://api.twilio.com からの相対パス
  • --data <k=v>:フォームフィールド(複数回指定可能)

設定リファレンス

環境変数(Linux 本番環境)

ファイル:/etc/twilio/voice.env

# Twilio auth
TWILIO_ACCOUNT_SID=AC2f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
TWILIO_AUTH_TOKEN=9f8e7d6c5b4a3a2b1c0d9e8f7a6b5c4d

# Public base URL used for signature validation
PUBLIC_BASE_URL=https://voice.prod.example.com

# Caller ID must be a Twilio number or verified caller ID
TWILIO_CALLER_ID=+14155551234

# Webhook behavior
LOG_LEVEL=info
PORT=3000

# Recording/transcription pipeline
RECORDINGS_BUCKET=s3://prod-voice-recordings-us-east-1
TRANSCRIPTION_PROVIDER=deepgram
TRANSCRIPTION_WEBHOOK_SECRET=whsec_6b1f0b2a9c3d4e5f

Systemd ユニット(Ubuntu/Fedora):

ファイル:/etc/systemd/system/twilio-voice.service

[Unit]
Description=Twilio Voice Webhook Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=twilio
Group=twilio
EnvironmentFile=/etc/twilio/voice.env
WorkingDirectory=/srv/twilio-voice
ExecStart=/usr/bin/node /srv/twilio-voice/server.js
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/twilio-voice
AmbientCapabilities=
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true

[Install]
WantedBy=multi-user.target

有効化:

sudo useradd --system --home /srv/twilio-voice --shell /usr/sbin/nologin twilio
sudo mkdir -p /srv/twilio-voice
sudo chown -R twilio:twilio /srv/twilio-voice
sudo systemctl daemon-reload
sudo systemctl enable --now twilio-voice
sudo systemctl status twilio-voice --no-pager
NGINX リバースプロキシ(TLS 終端)

ファイル:/etc/nginx/sites-available/voice.prod.example.com

server {
  listen 443 ssl http2;
  server_name voice.prod.example.com;

  ssl_certificate     /etc/letsencrypt/live/voice.prod.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/voice.prod.example.com/privkey.pem;
  ssl_protocols TLSv1.2 TLSv1.3;

  # Twilio webhooks are small; keep limits tight
  client_max_body_size 64k;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    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;
  }

  location = /healthz {
    proxy_pass http://127.0.0.1:3000/healthz;
  }
}
TwiML の例(保存済みテンプレート)

TwiML テンプレートをリポジトリ内に保存する場合は、バージョン管理の対象にしてください。

パス:/srv/twilio-voice/twiml/outbound.xml

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="Polly.Joanna" language="en-US">Connecting your call.</Say>
  <Dial callerId="+14155551234" record="record-from-answer" recordingStatusCallback="https://voice.prod.example.com/webhooks/recording">
    <Number>+14155559876</Number>
  </Dial>
</Response>

統合パターン

Twilio Messaging との組み合わせ(クラスター要件との整合)

一般的なパターン:通話に失敗した場合、オプトアウト処理を行ったうえで SMS に切り替えます。

フロー:

  1. 発信通話を試みます
  2. ステータスコールバックで completed と CallStatus=busy|no-answer|failed が示された場合
  3. Messaging Service 経由で「連絡を試みました……」という SMS を送信します
  4. STOP/START キーワードとコンプライアンス要件に従います

擬似パイプライン:

flowchart LR
  A[Create Call] --> B[Status Callback]
  B -->|answered| C[Normal completion]
  B -->|busy/no-answer/failed| D[Send SMS fallback]
  D --> E[Message Status Webhook]
Verify との組み合わせ(音声による 2FA)
  • SMS がブロックされている場合は、音声 OTP に Verify V2 を使用します。
  • 音声チャネルはコストが高くなる可能性があるため、レート制限と不正利用対策を行います。
Studio との組み合わせ
  • IVR ロジックを迅速に反復するには Studio を使用します。ただし、バージョン管理のため、重要なフローはコードで管理してください。
  • 特定の分岐(例:営業時間外のルーティング)では、REST Trigger API 経由で Studio Flow をトリガーします。
イベント駆動型の録音/文字起こしパイプライン
  • 録音ステータスコールバック → ジョブをキューに登録(SQS/PubSub/Kafka)
  • ワーカーが録音メディアをダウンロード → 文字起こし → 文字起こし結果を保存
  • 文字起こし結果を CRM チケットに添付

例(AWS SQS + ワーカー):

  • Webhook サービスが {RecordingSid, CallSid, RecordingUrl, Timestamp} メッセージを発行します
  • ワーカーが Twilio REST を使用して録音メタデータとメディアを取得します
可観測性の統合
  • CallSid、ParentCallSid、From、To、Direction を含む構造化ログを出力します。
  • メトリクス:
    • Webhook のレイテンシー(p50/p95)
    • 通話応答率
    • Gather 完了率
    • カンファレンス参加失敗数
  • トレーシング:
    • CallSid をトレース属性として伝播させます

エラー処理とトラブルシューティング

Webhook HTTP、Twilio REST API、キャリア/SIP の 3 つのレイヤーでエラーを処理します。

1) Twilio could not find a valid URL for the Voice request

症状(コンソールデバッガー):

  • “Twilio could not find a valid URL for the Voice request. Please check your TwiML App settings.”

根本原因:

  • 電話番号の Voice Webhook が設定されていないか、無効な URL が指定されています。

修正方法:

  • コンソール → 電話番号 →(番号)→ Voice の設定 → Webhook URL を設定します。
  • HTTPS を使用し、インターネットからアクセス可能であることを確認します。

2) 11200 - HTTP retrieval failure

症状:

  • Twilio Debugger に次の内容が表示されます:
    • Error - 11200
    • “HTTP retrieval failure”

根本原因:

  • Webhook エンドポイントがタイムアウトした、5xx を返した、TLS ハンドシェイクに失敗した、または DNS に問題があります。

修正方法:

  • エンドポイントが約 2~5 秒以内に応答することを確認します。
  • TLS 証明書の有効性と証明書チェーンを確認します。
  • ファイアウォールで Twilio からのインバウンド通信が許可されていることを確認します(Twilio の IP 範囲を管理していない限り、IP 許可リストは使用しないでください)。
  • /healthz を追加して監視します。

3) 12300 - Invalid Content-Type

症状:

  • Twilio Debugger に次の内容が表示されます:
    • Error - 12300
    • “Invalid Content-Type”

根本原因:

  • Webhook が TwiML を返しましたが、Content-Type が正しくありません(例:application/json)。

修正方法:

  • Content-Type: text/xml を設定します。
  • レスポンス本文が有効な XML であることを確認します。

4) 12100 - Document parse failure

症状:

  • Twilio Debugger に次の内容が表示されます:
    • Error - 12100
    • “Document parse failure”

根本原因:

  • XML の形式が正しくありません(エスケープされていない文字、無効なネスト)。

修正方法:

  • ヘルパーライブラリの TwiML ビルダーを使用します。
  • 生成された XML を検証し、& が &amp; としてエスケープされていることを確認します。

5) 21211 - The 'To' number +... is not a valid phone number

症状(REST API レスポンス):

{
  "code": 21211,
  "message": "The 'To' number +1415555 is not a valid phone number.",
  "status": 400
}

根本原因:

  • E.164 形式ではない、国番号がない、または無効な文字が含まれています。

修正方法:

  • E.164 形式(+14155551212)に正規化します。
  • Twilio を呼び出す前に libphonenumber で検証します。
  • SIP の場合は、sip:user@domain を使用します。

6) 20003 - Authenticate

症状:

  • REST API が次の内容を返します:
    • HTTP 401
    • code: 20003
    • message: “Authenticate”

根本原因:

  • Account SID/Auth Token が間違っている、トークンがローテーションされている、またはライブエンドポイントにテスト用認証情報を使用しています。

修正方法:

  • 実行環境のシークレットを確認します。
  • トークンをローテーションして再デプロイします。
  • 該当する場合は、正しいサブアカウントの認証情報を使用していることを確認します。

7) 20429 - Too Many Requests

症状:

  • REST API が次の内容を返します:
    • HTTP 429
    • code: 20429
    • message: “Too Many Requests”

根本原因:

  • レート制限を超過しています(通話の一括作成、カンファレンス操作、録音の取得など)。

修正方法:

  • ジッターを加えた指数バックオフを実装します。
  • 発信通話の作成をキューに入れ、同時実行数に上限を設けます。
  • 通話/録音のメタデータをキャッシュして、繰り返しの取得を減らします。

8) 30003 - Unreachable destination handset

症状:

  • 通話がすぐに終了し、ステータスコールバックで失敗が示されます。
  • Twilio のエラーコード 30003。

根本原因:

  • キャリアに到達できない、ルーティングが無効、端末がネットワークに接続されていない、またはキャリアによってブロックされています。

修正方法:

  • ビジネス上妥当な場合に限り、バックオフを適用して再試行します。
  • 重要なトラフィックには、代替ルート(BYOC)を検討します。
  • 発信先が音声通話に対応しており、ブロックされていないことを確認します。

9) 403 Invalid Twilio signature(アプリ側)

症状:

  • サービスが “Invalid Twilio signature” というメッセージとともに 403 を返します。

根本原因:

  • 署名の検証で誤った URL が使われています(スキーム/ホストがない、パスが違う、ngrok URL が変更された)。
  • プロキシが正しい Host/X-Forwarded-Proto を転送していません。

修正方法:

  • 外部から見える URL を使用して検証 URL を計算します。
  • PUBLIC_BASE_URL が Twilio で設定した Webhook のベース URL と一致していることを確認します。
  • NGINX で Host と X-Forwarded-Proto を転送します。

10) SIP:488 Not Acceptable Here/音声が聞こえない

症状:

  • SIP 通話は接続しますが音声が聞こえない、または SIP が 488 で拒否します。

根本原因:

  • コーデックの不一致、SRTP の不一致、NAT/ファイアウォールによる RTP の遮断。

対処方法:

  • 共通のコーデック(サポートされている場合は PCMU/PCMA/Opus)を使用してください。
  • SRTP を使用する場合は、ポリシーを一致させてください。
  • RTP ポートを開放するか、Twilio 管理のメディアを使用し、対称 RTP を確認してください。

セキュリティ強化

Webhook の真正性
  • すべての webhook で X-Twilio-Signature を検証してください。
  • Twilio がリクエストした正確な URL(スキーム/ホスト/パス)を使用して検証してください。
  • 署名のないリクエストは 403 で拒否してください。
TLS と HTTP
  • TLS 1.2 以上を必須にしてください(TLS 1.3 を推奨)。
  • 脆弱な暗号スイートを無効にしてください(最新の NGINX のデフォルト設定を使用)。
  • タイムアウトを厳格に設定してください:
    • upstream read timeout 10s
    • request body size 64k
シークレット
  • Auth Token はシークレットマネージャーに保存し、ログに記録しないでください。
  • Auth Token は定期的(四半期ごと)およびインシデント発生時にローテーションしてください。
  • 環境を分離するために、サブアカウントを使用してください(dev/stage/prod)。
最小権限(システム)
  • webhook サービスは root 以外のユーザーとして実行してください。
  • Systemd の強化設定(サンプルユニットには以下がすでに含まれています):
    • NoNewPrivileges=true
    • ProtectSystem=strict
    • ProtectHome=true
    • PrivateTmp=true
  • CIS Benchmarks に準拠してください:
    • CIS Ubuntu Linux 22.04 LTS Benchmark(サービスの強化、ファイル権限に関するセクション)
    • CIS NGINX Benchmark(TLS 設定、ヘッダー、ログ記録)
データ保護(録音/文字起こし)
  • 録音/文字起こしは機密情報として扱ってください:
    • 保存時に暗号化する(KMS)
    • IAM を使用してアクセスを制限する
    • アクセスログを監査する
  • 保持期間を定めてください:
    • 例:法的保全が必要な場合を除き、録音を 30 日後に削除する
  • 録音をエクスポートする場合は、有効期限の短い署名付き URL を使用してください。
PII の最小化
  • 必要がない限り、電話番号全体をログに記録しないでください。マスキングしてください:
    • +14155551234 → +1415****234
  • 必要がない限り、口座番号が含まれる可能性のある DTMF 入力を保存しないでください。保存が必要な場合は、トークン化してください。

パフォーマンス調整

Webhook のレイテンシ予算

目標:TwiML 生成の p95 < 200ms(ネットワークを除く)。

最適化:

  • よく使われる分岐向けの静的な TwiML テンプレートを事前に生成してください。
  • webhook 内で外部サービスを同期的に呼び出さないでください。
  • ルーティングテーブルには、定期的に更新するインメモリーキャッシュを使用してください。

期待される効果:

  • CRM の検索を webhook から非同期ジョブに移すと、p95 が約 800ms から 120ms に短縮されることがよくあります。
Twilio REST API の呼び出しを減らす
  • Calls.fetch の繰り返しを避けるため、通話メタデータを短時間(例:30~60 秒)キャッシュしてください。
  • カンファレンス参加者のポーリングでは、可能な場合、イベント駆動型のコールバックを優先してください。

期待される効果:

  • インシデントのデバッグやダッシュボードでの REST 呼び出しを 30~70% 削減できます。
カンファレンスのスケーリング
  • 小さなやり取りごとに個別のカンファレンスを作成せず、セッションごとにカンファレンスルームを再利用してください。
  • 関連付けを簡単にするため、決定的なカンファレンス名(例:support-${ticketId})を使用してください。
  • 参加者の入室/退室イベントを冪等に処理してください。
録音ダウンロードのスループット
  • 同時実行数を制限し(例:10~25 ワーカー)、録音を非同期でダウンロードしてください。
  • HTTP keep-alive とオブジェクトストレージへのストリーミングを使用してください。

期待される効果:

  • ワーカーの CPU/メモリを安定させ、429 レート制限を回避し、テールレイテンシを低減できます。

高度なトピック

webhook の冪等性

Twilio の再試行によって、処理が重複することがあります。

パターン:

  • 冪等性キーを計算します:
    • voice:{CallSid}:{EventType}:{Timestamp}
  • TTL(例:24h)付きで Redis に保存します。
  • キーが存在する場合は、すぐに 200 を返します。
<Gather> のエッジケースへの対応
  • タイムアウト時には Digits が存在しないことがあります。
  • ユーザーが複数の数字を素早く押すことがあるため、numDigits を適用してください。
  • 音声認識では、部分的な結果や信頼度にばらつきがあります。必ず DTMF のフォールバックを用意してください。
<Dial> の子通話の動作
  • 親通話と子通話では、ステータスコールバックが異なります。
  • ダイヤルされたレッグの SID が必要な場合は、コールバックまたは Twilio イベントで取得してください。
  • 転送では、レッグを関連付けるために ParentCallSid を追跡してください。
応答メッセージ判定(AMD)
  • レイテンシーが増加し、誤判定が発生することがあります。
  • ビジネスロジックで必要な場合(例:ボイスメールを残す場合)のみ使用してください。
  • フォールバックを実装してください。AMD の判定が不確かな場合は、人間に転送するか、プロンプトを再生します。
エッジロケーション
  • レイテンシーに敏感なアプリでは、REST クライアントで Twilio エッジを指定します。
    • Node ヘルパーは edge/region の設定に対応しています。
  • サービス間でエッジの選択を統一し、リージョンをまたぐメディア経路を避けてください。
コンプライアンスと同意
  • 録音に関する同意要件は、管轄区域によって異なります。
  • 必要な場合は、録音を有効にする前に明示的な同意プロンプトを実装してください。
  • 同意イベントをタイムスタンプおよび CallSid とともに保存してください。

使用例

1) インバウンド IVR → Polly 音声を使用して <Dial> でオンコール担当者に転送

Node TwiML スニペット:

const vr = new twilio.twiml.VoiceResponse();
vr.say({ voice: "Polly.Matthew", language: "en-US" }, "You have reached incident response.");
vr.dial(
  {
    callerId: process.env.TWILIO_CALLER_ID,
    timeout: 20,
    record: "record-from-answer",
    recordingStatusCallback: "https://voice.prod.example.com/webhooks/recording",
  },
  "+14155550123"
);
res.type("text/xml").send(vr.toString());

運用上の注意:

  • オンコール番号が E.164 形式であることを確認してください。
  • 録音コールバックも署名検証が必要です。

2) インライン TwiML(REST)を使用したアウトバウンド通話+ステータスコールバック

curl を使用:

export TWILIO_ACCOUNT_SID="AC2f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5"
export TWILIO_AUTH_TOKEN="9f8e7d6c5b4a3a2b1c0d9e8f7a6b5c4d"

curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls.json" \
  --data-urlencode "From=+14155551234" \
  --data-urlencode "To=+14155559876" \
  --data-urlencode "Twiml=<Response><Say voice=\"Polly.Joanna\">This is a test call.</Say><Hangup/></Response>" \
  --data-urlencode "StatusCallback=https://voice.prod.example.com/webhooks/voice/status" \
  --data-urlencode "StatusCallbackEvent=initiated ringing answered completed" \
  -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN"

注意:

  • インライン TwiML はシンプルな通知に便利です。複雑なフローには URL を使用してください。

3) カンファレンスを使用したウォーム転送

TwiML のパターン:

  • オペレーター側の通話レッグが muted=false でカンファレンスに参加する
  • 顧客側の通話レッグがカンファレンスに参加する
  • スーパーバイザーはモニタリング用に startConferenceOnEnter=false を指定して参加できる

TwiML の例:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Dial>
    <Conference
      startConferenceOnEnter="true"
      endConferenceOnExit="true"
      beep="onEnter"
      record="record-from-start"
      recordingStatusCallback="https://voice.prod.example.com/webhooks/recording"
    >support-ticket-842193</Conference>
  </Dial>
</Response>

運用上の注意:

  • チケットと関連付けられるよう、決定的なカンファレンス名を使用してください。
  • ウォーム転送中にミュート/ミュート解除するには、参加者更新 API を使用してください。

4) <Record> と文字起こしパイプラインを使用したボイスメールの録音

TwiML:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="Polly.Joanna">Please leave a message after the tone.</Say>
  <Record
    maxLength="120"
    timeout="5"
    playBeep="true"
    recordingStatusCallback="https://voice.prod.example.com/webhooks/recording"
    recordingStatusCallbackMethod="POST"
  />
  <Say voice="Polly.Joanna">Thank you. Goodbye.</Say>
  <Hangup/>
</Response>

パイプライン:

  • 録音コールバックで文字起こしジョブをキューに追加します。
  • ワーカーが .wav をダウンロードして文字起こしを行います。
  • 文字起こし結果を RecordingSid をキーとして保存します。

5) SIP インバウンド → PSTN フォールバックに転送

TwiML:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="Polly.Matthew">Routing your SIP call.</Say>
  <Dial callerId="+14155551234" timeout="15">
    <Number>+14155550199</Number>
  </Dial>
  <Say voice="Polly.Matthew">We could not connect your call.</Say>
  <Hangup/>
</Response>

注意:

  • SIP ドメインがこの TwiML アプリ/Webhook に到達するよう設定されていることを確認してください。
  • 利用可能な場合は、SIP レスポンスコードと RTP 統計を監視してください。

6) リトライ機能付き IVR ステートマシン(DTMF)

擬似実装のアプローチ:

  • Redis に状態を保存します:
    • キー:ivr:{CallSid}
    • 値:{state, retries}
  • /voice/inbound で、状態を MENU、リトライ回数を 0 に設定します。
  • /voice/menu で、入力された数字を検証します:
    • 無効で、リトライ回数が 2 未満の場合:リトライ回数を増やし、/voice/inbound にリダイレクトします
    • それ以外の場合:オペレーターに転送するか、通話を終了します

これにより無限ループを防ぎ、動作を決定的にできます。


クイックリファレンス

タスクコマンド/API主なフラグ/フィールド
アウトバウンド通話を作成twilio api:core:calls:create--from、--to、--url/--twiml、--status-callback、--timeout
通話一覧を取得twilio api:core:calls:list--limit、--properties
通話を取得twilio api:core:calls:fetch--sid
録音一覧を取得twilio api:core:recordings:list--limit、--properties
録音をダウンロードcurl -u SID:TOKEN -L .../Recordings/{RE}.wav-L、出力ファイル
カンファレンス一覧を取得twilio api:core:conferences:list--status、--limit
参加者一覧を取得twilio api:core:conferences:participants:list--conference-sid、--limit
参加者をミュートtwilio api:core:conferences:participants:update--conference-sid、--sid、--muted true
Webhook の失敗をデバッグTwilio Console Debugger11200、12100、12300 を確認
Webhook を検証ヘルパーライブラリX-Twilio-Signature、正確な URL

グラフの関係

DEPENDS_ON
  • twilio-core(アカウント認証、REST API の基礎、サブアカウント)
  • http-webhooks(署名検証、リトライ、冪等性)
  • tls-nginx(TLS 終端、リバースプロキシの適切な設定)
  • observability(構造化ログ、メトリクス、トレース)
COMPOSES
  • twilio-messaging(SMS フォールバック、ステータス Webhook、STOP 処理)
  • twilio-verify(音声 OTP、不正利用対策、レート制限)
  • sendgrid(不在着信/ボイスメールのメール通知)
  • twilio-studio(ハイブリッドフロー:迅速な反復には Studio、重要な処理にはコードを使用)
  • queue-workers(録音/文字起こしパイプライン)
類似ツール
  • plivo-voice(通話制御の概念は似ていますが、API は異なります)
  • vonage-voice(音声 Webhook + NCCO と TwiML)
  • aws-connect(コンタクトセンターのフロー。プラットフォームの抽象化がより複雑です)

コメント