Twilio Studio と Functions で留守番電話検出付き一次・二次発信フローを作る

Twilio Studio と Functions で留守番電話検出付き一次・二次発信フローを作る

一次番号に自動発信後 Answering Machine Detection (AMD) で人間か留守番電話かを判定し、必要に応じて二次番号へフォールバック発信するフローを、Twilio Studio と Twilio Functions を使って構築します。 Studio の Make Outgoing Call ウィジェットと Functions を組み合わせることで、外部システムからの HTTP リクエストを入り口にしたシンプルな自動通知シナリオを実現できます。
2025.12.01

はじめに

本記事では Twilio Studio と Twilio Functions を組み合わせて、一次の電話番号に発信し、 AMD で人間が応答しなかった場合に二次の電話番号へフォールバック発信するフローを実装します。 REST API で Studio Flow を起動し、 Functions で発信制御を行う構成を、実際に動作確認したコードとあわせて紹介します。

studio-flow

Twilio とは

Twilio は音声通話やメッセージング、メールなどの機能を API として提供するクラウドコミュニケーションプラットフォームです。アプリケーション側から HTTP ベースの API を呼び出すことで、電話発信や SMS 送信などの機能をコードから利用できます。

対象読者

  • Twilio Studio と Functions を組み合わせた音声通話フローを試してみたい方
  • AMD を使って人間と留守番電話を判別し、分岐したい方
  • 通知用途の自動発信シナリオの構成例を知りたい方

参考情報

システム構成

今回実現したい要件は次のとおりです。

  1. 外部システムから HTTP リクエストが送られてきたら、一次の電話番号に発信する
  2. AMD で人間が応答したと判定された場合は、一次番号に固定メッセージを読み上げて終了する
  3. AMD で人間が応答していないと判定された場合、または通話が成立しなかった場合は、二次の電話番号に発信して別のメッセージを読み上げる

これを実現するために、次のような役割分担にしました。

  • Twilio Studio

    • 一次番号への発信
    • AMD による応答者の判定
    • 判定結果に応じた分岐と、一次側へのメッセージ再生
    • 二次発信のトリガーとして Twilio Function を呼び出す
  • Twilio Functions

    • 外部からの HTTP リクエストを受け取り、 Studio Flow の Execution を作成して一次発信を開始する
    • Studio から呼び出され、二次の電話番号への発信とメッセージの読み上げを行う

全体像を簡単なフロー図にすると次のようになります。

この構成にしておくと、 HTTP の受け口や認証、 Flow の差し替えなどは Functions 側で柔軟に制御でき、 Studio では音声フローの定義に集中できます。

実装

前提と環境変数の設定

今回の検証では次のような前提で構成しました。

  • Twilio アカウントと、発信に利用する Twilio 電話番号
  • 一次の通知先番号
  • 二次の通知先番号

Functions の Service に、次のような環境変数を設定します。

変数名
FLOW_SID 作成した Studio Flow の SID
PRIMARY_NUMBER 一次の電話番号 ( E.164 形式 )
SECONDARY_NUMBER 二次の電話番号 ( E.164 形式 )
CALLER_ID 発信元として利用する Twilio 電話番号

環境変数

この環境変数を使って、 Functions から Studio Flow の Execution を作成したり、 Calls API で発信したりします。

Studio Flow の構成

Studio 側では、次のような状態を持つ Flow を作成しました。

  • Trigger
  • call_primary (make-outgoing-call-v2)
  • branch_answered_by (split-based-on)
  • say_message_primary (say-play)
  • notify_secondary (run-function)

Studio Flows

Flow は REST API トリガーで起動します。 Trigger ウィジェットの incomingRequest を call_primary に接続し、 Functions から Execution を作成したときにこの Flow が動くようにしています。

Flow の JSON 全体
{
  "description": "A New Flow",
  "states": [
    {
      "name": "Trigger",
      "type": "trigger",
      "transitions": [
        {
          "event": "incomingMessage"
        },
        {
          "event": "incomingCall"
        },
        {
          "event": "incomingConversationMessage"
        },
        {
          "next": "call_primary",
          "event": "incomingRequest"
        },
        {
          "event": "incomingParent"
        }
      ],
      "properties": {
        "offset": {
          "x": 0,
          "y": 0
        }
      }
    },
    {
      "name": "call_primary",
      "type": "make-outgoing-call-v2",
      "transitions": [
        {
          "next": "branch_answered_by",
          "event": "answered"
        },
        {
          "next": "notify_secondary",
          "event": "busy"
        },
        {
          "next": "notify_secondary",
          "event": "noAnswer"
        },
        {
          "next": "notify_secondary",
          "event": "failed"
        }
      ],
      "properties": {
        "machine_detection_speech_threshold": "2400",
        "detect_answering_machine": true,
        "offset": {
          "x": 630,
          "y": 240
        },
        "recording_channels": "mono",
        "timeout": 60,
        "machine_detection": "Enable",
        "trim": "do-not-trim",
        "record": false,
        "machine_detection_speech_end_threshold": "1200",
        "machine_detection_timeout": "30",
        "from": "{{flow.channel.address}}",
        "to": "{{contact.channel.address}}",
        "machine_detection_silence_timeout": "5000"
      }
    },
    {
      "name": "branch_answered_by",
      "type": "split-based-on",
      "transitions": [
        {
          "next": "notify_secondary",
          "event": "noMatch"
        },
        {
          "next": "say_message_primary",
          "event": "match",
          "conditions": [
            {
              "friendly_name": "If value equal_to human",
              "arguments": [
                "{{widgets.call_primary.AnsweredBy}}"
              ],
              "type": "equal_to",
              "value": "human"
            }
          ]
        }
      ],
      "properties": {
        "input": "{{widgets.call_primary.AnsweredBy}}",
        "offset": {
          "x": 950,
          "y": 530
        }
      }
    },
    {
      "name": "say_message_primary",
      "type": "say-play",
      "transitions": [
        {
          "event": "audioComplete"
        }
      ],
      "properties": {
        "voice": "Polly.Takumi",
        "offset": {
          "x": 1190,
          "y": 790
        },
        "loop": 1,
        "say": "システムからのお知らせです。",
        "language": "ja-JP"
      }
    },
    {
      "name": "notify_secondary",
      "type": "run-function",
      "transitions": [
        {
          "event": "success"
        },
        {
          "event": "fail"
        }
      ],
      "properties": {
        "service_sid": "ZSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "environment_sid": "ZEXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "offset": {
          "x": 250,
          "y": 790
        },
        "function_sid": "ZHXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "parameters": [
          {
            "value": "{{widgets.call_primary.CallSid}}",
            "key": "callSid"
          },
          {
            "value": "{{widgets.call_primary.AnsweredBy}}",
            "key": "answeredBy"
          },
          {
            "value": "{{flow.flow_sid}}",
            "key": "executionSid"
          }
        ],
        "url": "https://{your-function-service-domain}.twil.io/call-secondary"
      }
    }
  ],
  "initial_state": "Trigger",
  "flags": {
    "allow_concurrent_calls": true
  }
}

一次番号への発信と AMD の設定

call_primary には make-outgoing-call-v2 を利用します。 AMD を有効にしAnsweredBy に判定結果を入れる設定です。 Make Outgoing Call ウィジェットは contact.channel.address などのコンテキストに基づいて発信先を決定します。

amd config

AMD の判定結果による分岐

branch_answered_bysplit-based-on ウィジェットで、 {{widgets.call_primary.AnsweredBy}} を入力にしています。

条件 遷移先
human say_message_primary
上記以外 notify_secondary

あわせて、 call_primary の busy, noAnswer, failed の各イベントも notify_secondary につなぎ、人間が応答しなかったときや通話が成立しなかったときはすべて二次発信に進むようにしています。

一次側のメッセージ再生

say_message_primary は一次番号向けのアナウンス再生です。

パラメータ名
language ja-JP
say システムからのお知らせです。

AMD で human と判定された場合のみこのウィジェットが実行され、そのまま Flow を終了します。

二次発信を起動する run-function

notify_secondaryrun-function ウィジェットで、 Functions 上の /call-secondary を呼び出します。

  • service_sid, environment_sid, function_sid で呼び出し先を指定
  • parameters で CallSid や AnsweredBy、 executionSid を渡す

AMD で human 以外となったケースや、 busy, noAnswer, failed のケースですべてこのウィジェットを通るため、二次発信の起点となります。

Twilio Function で一次発信を開始する

外部システムからの HTTP リクエストを受け取る入口として、 /start-primary-call 関数を実装しました。役割は Studio Flow の Execution を作成して一次発信を開始することです。

function start-primary-call

  • パス /start-primary-call
  • アクセス Public
/start-primary-call のコード
exports.handler = async function (context, event, callback) {
  const client = context.getTwilioClient();

  const flowSid = context.FLOW_SID;
  const to = context.PRIMARY_NUMBER;   // 一次番号
  const from = context.CALLER_ID;      // Twilio 発信元番号

  try {
    await client.studio.v2.flows(flowSid)
      .executions
      .create({
        to,
        from,
        parameters: { requestId: event.requestId || null }
      });

    const response = new Twilio.Response();
    response.setStatusCode(200);
    response.setBody({ status: 'ok' });
    callback(null, response);
  } catch (err) {
    console.error(err);
    const response = new Twilio.Response();
    response.setStatusCode(500);
    response.setBody({ error: err.message });
    callback(null, response);
  }
};

検証時は次のようなコマンドで呼び出します。

curl https://{your-function-service-domain}.twil.io/start-primary-call

これにより、 Studio Flow が起動し、一次の電話番号に発信されます。

Twilio Function で二次番号に発信する

Studio の run-function から呼び出される二次発信用 Function が /call-secondary です。この関数では Calls API を使って二次番号に発信し、 TwiML で固定メッセージを読み上げます。

function call-secondary

  • パス /call-secondary
  • アクセス Protected
/call-secondary のコード
exports.handler = async function (context, event, callback) {
  const client = context.getTwilioClient();

  const to = context.SECONDARY_NUMBER;
  const from = context.CALLER_ID;

  // AMD の結果をログ出力
  console.log('Primary AnsweredBy:', event.answeredBy);
  console.log('Primary CallSid:', event.callSid);

  // 二次側に読み上げるメッセージ
  const twiml = new Twilio.twiml.VoiceResponse();
  twiml.say(
    {
      language: 'ja-JP'
    },
    '一次の連絡先に接続できなかったため、この番号にご連絡しています。システムからのお知らせです。'
  );

  try {
    await client.calls.create({
      to,
      from,
      twiml: twiml.toString()
      // 必要に応じて二次発信にも AMD を付けられます
      // machineDetection: 'Enable'
    });

    const response = new Twilio.Response();
    response.setStatusCode(200);
    response.setBody({ status: 'secondary call started' });
    callback(null, response);
  } catch (err) {
    console.error(err);
    const response = new Twilio.Response();
    response.setStatusCode(500);
    response.setBody({ error: err.message });
    callback(null, response);
  }
};

Studio 側からは run-function 経由でこの関数が呼び出され、一次側の CallSid や AnsweredBy が event に渡されます。今回はログに出すだけですが、必要であれば二次側のメッセージ文言を変えたり、通知内容を分岐させたりできます。

動作検証

実際に動作させながら、 AMD の判定と二次発信の挙動を確認しました。

ケース1: 人間が応答し、すぐに発話する

一次の電話番号に発信し、受話後に素直に「はい、もしもし」と発話した ケースです。

  • AMD の判定結果: human
  • Flow の遷移: call_primary → branch_answered_by → say_message_primary
  • 挙動:
    1. 一次番号で通話が接続され、続けて システムからのお知らせです とアナウンスが再生される
    2. notify_secondary は呼び出されず、二次番号への発信は行われない

期待どおり、一次側にのみメッセージが流れました。

ケース2: 人間が応答するが発話しない

一次の電話番号に発信し、受話はするものの、何も話さずにしばらく放置した ケースです。

  • AMD の判定結果: human 以外 (無音が続いたため human 判定にならず、留守番電話などと同様の扱いになったと考えられます。)
  • Flow の遷移: call_primary → branch_answered_by → notify_secondary
  • 挙動:
    1. 一次番号の通話はしばらくして切断される
    2. その後、二次の電話番号に発信が行われる
    3. 二次側では 一次の連絡先に接続できなかったため、この番号にご連絡しています。システムからのお知らせです。 とアナウンスが再生される

一次に接続できてはいるものの、 AMD が人間と判定できないパターンでも、期待どおり二次へのフォールバックが動作することを確認できました。

AMD の判定はしきい値設定や発話パターンに依存するため、本番環境では実際のユースケースを想定したテストを行い、必要に応じてしきい値の調整やフロー分岐の条件追加を検討するとよさそうです。

まとめ

本記事では Twilio Studio の make-outgoing-call-v2 と AMD を使い、「人間が応答した場合は一次番号でメッセージを読み上げ、それ以外のケースでは二次番号にフォールバックする」フローを構成しました。 HTTP リクエストの入口や Flow の起動は Twilio Functions に任せることで、認証や環境変数の管理、 Flow の差し替えなどを柔軟に行えるようにしました。通知系の自動発信シナリオで、一次連絡先が留守がちな場合でも、確実にどこかの番号にメッセージを届けたいときの構成例として活用してもらえればと思います。

この記事をシェアする

FacebookHatena blogX

関連記事