Twilio Voice SDK - フロントエンドから番号を秘匿して外部電話番号へ発信する仕組みを実装する

Twilio Voice SDK - フロントエンドから番号を秘匿して外部電話番号へ発信する仕組みを実装する

Twilio Voice JavaScript SDK と Twilio Functions を用いて、ブラウザから外部電話番号へ発信する仕組みを構築しました。顧客情報はサーバーで管理し、フロントには名前と会社名だけを表示することで、番号を秘匿したまま通話が可能になります。TwiML App の設定や Function の実装など、要点を整理し、番号秘匿通話システムの基盤を制作します。

はじめに

本記事では、Twilio Voice サービスを使用した音声通話機能の実装方法について解説します。今回は例として JavaScript で実装したフロントエンドアプリケーションから外部電話番号へ発信する機能を実装します。次の画像が今回実装したアプリの UI イメージです。

動作イメージ

番号が秘匿された状態で顧客名を選択し発信することができます。配車アプリやデリバリーサービスなど、顧客とサービス提供者の間で直接的な通話が必要でありながら、お互いの電話番号を秘匿したいシステムの構築において、Twilio Voice は有力な選択肢となります。

Twilio とは

Twilio は、音声通話、SMS、ビデオ通話などの通信機能を API として提供するクラウドサービスです。開発者は複雑な通信インフラを構築することなく、REST API や SDK を通じてアプリケーションに通信機能を組み込めます。

対象読者

  • 音声通話機能の実装に興味があるアプリケーション開発者
  • Twilio サービスを初めて使用される方
  • JavaScript および Node.js の基本的な知識をお持ちの方
  • WebRTC の実用シーンについて知りたい方

参考

概要と構成図

音声発信機能では、フロントエンドから Twilio Voice JavaScript SDK を使用して、一般の電話番号に通話を発信します。フロントエンドの実装で電話番号をマスキングし、UI では顧客名のみが表示されるようにします。

構成図

  1. フロントエンドがバックエンドから顧客の名前:電話番号リストを取得 (電話番号は UI 表示しない)
  2. フロントエンドが Twilio Function /token から Access Token を取得
  3. ユーザーが顧客を選択して発信ボタンをクリック
  4. フロントエンドが選択された顧客の電話番号を使用し Twilio Voice SDK で発信リクエストを実行
  5. Twilio が TwiML を Twilio Function /voice から取得し内容を解釈
  6. Twilio が PSTN 経由で外部電話番号に通話を転送

実装

TwiML App の作成

  • Develop > Phone Numbers > Manage > TwiML Apps
    TwiML App の作成
  • Friendly name: 任意 (例: voice-poc-app)
  • Voice Configuration の Request URL は後ほど Twilio Functions の URL を設定
    TwiML App の設定
  • TwiML App の SID を控えておきます

API Key の取得

  • Account Dashboard > Account Info > API Keys & Tokens
    API Key の取得
  • Friendly name: 任意 (例: voice-poc-app-key)
  • Key type: Standard
    API Key の設定
  • Key SID と Secret を控えておきます

Twilio Function: Access Token 生成 (/token)

  • Develop > Functions and Assets > Services でサービスを作成
    create service
  • Service Name: 任意 (例: voice-poc-service)
  • Add Function で /token という名前の Function を作成
    add function
/token の実装
exports.handler = function(context, event, callback) {
  const res = new Twilio.Response();
  res.appendHeader('Content-Type', 'application/json');
  res.appendHeader('Access-Control-Allow-Origin', '*');
  res.appendHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

  if (event.httpMethod === 'OPTIONS') {
    res.setStatusCode(200); res.setBody(''); return callback(null, res);
  }

  const AccessToken = Twilio.jwt.AccessToken;
  const VoiceGrant = AccessToken.VoiceGrant;

  const identity = 'user001';
  const token = new AccessToken(context.ACCOUNT_SID, context.API_KEY_SID, context.API_KEY_SECRET, { identity });
  token.addGrant(new VoiceGrant({ outgoingApplicationSid: context.TWIML_APP_SID, incomingAllow: true }));

  res.setBody({ identity, token: token.toJwt() });
  callback(null, res);
};
  • Visibility を Public に変更
    visbility 設定
  • 変更後 Save と Deploy All を忘れずに押下します

Twilio Function: TwiML 生成 (/voice)

  • Add Function で /voice という名前の Function を作成
/voice の実装
exports.handler = function (context, event, callback) {
  const twiml = new Twilio.twiml.VoiceResponse();
  const to = event.To;
  const from = context.CALLER_ID; // 例: +81XXXXXXXXXX(発信元として認証済み番号 or 購入番号)

  if (!to) {
    twiml.say({ voice: 'Polly.Joanna', language: 'en-US' }, 'No phone number provided');
    return callback(null, twiml);
  }
  if (!from) {
    twiml.say({ voice: 'Polly.Joanna', language: 'en-US' }, 'Caller ID is not configured');
    return callback(null, twiml);
  }

  const dial = twiml.dial({ callerId: from, answerOnBridge: true, timeLimit: 3600 });
  dial.number(to);
  callback(null, twiml);
};

環境変数の設定

  • Environment Variables で下記の値を設定します。ACCOUNT_SID は Twilio コンソールアカウントダッシュボードから取得できます。また、 CALLER_ID には Twilio で購入済みの番号を指定してください。
ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API_KEY_SID=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API_KEY_SECRET=your_api_key_secret
TWIML_APP_SID=APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CALLER_ID=+81xxxxxxxxxx

Environment Variables

TwiML App への Functions URL 設定

Function を Deploy 完了後、TwiML App の Voice Request URL に /voice Function の URL を設定します。発信時はその TwiML App 経由で実際の PSTN 発信が行われます。

  • Console > TwiML Apps > 作成したアプリを選択
  • Voice Request URL: https://voice-poc-service-xxxx-dev.twil.io/voice

TwiML App への Functions URL 設定

バックエンド実装

顧客情報管理 (電話番号含む) を担当する部分を、今回は仮で作成します。本実装では、DB 接続やフロントエンドのセッション管理などがここで実装される想定です。

mkdir twilio-voice-poc
cd twilio-voice-poc
npm init
npm install express
server.js の実装
const express = require('express');
const app = express();
app.use(express.static('public'));

app.get('/customers', (_req, res) => {
  const customers = [
    { id: 'c1', name: '越井 琢巳', company: 'クラスメソッド', phoneNumber: '+819012345678' },
    { id: 'c2', name: '佐藤 花子', company: 'XYZ株式会社',     phoneNumber: '+819087654321' },
    { id: 'c3', name: '山田 次郎', company: 'サンプル商店',     phoneNumber: '+819011111111' },
  ];
  res.json({ customers, count: customers.length });
});

app.listen(3000, () => {
  console.log('http://localhost:3000');
});

フロントエンド実装

エンドユーザーが顧客と通話を行うために必要な機能や、番号秘匿 UI を提供します。今回の記事の核となる部分です。

Twilio Voice JavaScript SDK Releases から twilio.min.js をダウンロードして public フォルダに配置します。

twilio-voice-poc/
├── server.js
├── public/
│   ├── index.html
│   ├── app.js
│   └── twilio.min.js  ← GitHub からダウンロードしたファイル
public/index.html の実装
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Twilio Voice PoC</title>
  <style>
    .customer-item{border:1px solid #ccc;margin:5px 0;padding:10px;border-radius:5px;cursor:pointer}
    .customer-item:hover{background:#f0f0f0}
    .customer-item.selected{background:#e3f2fd;border-color:#2196f3}
  </style>
</head>
<body>
  <h1>音声発信テスト (番号マスキング)</h1>
  <div id="initSection">
    <p>音声通話を使うにはマイク許可が必要です。</p>
    <button id="initButton">音声機能を初期化</button>
    <div id="initStatus">初期化待ち</div>
  </div>
  <div id="callSection" style="display:none">
    <h3>お客様を選択してください</h3>
    <div id="customerList">読み込み中...</div>
    <div style="margin:20px 0">
      <button id="callButton" disabled>選択した顧客に発信</button>
      <button id="hangupButton" disabled>切断</button>
    </div>
    <div id="callStatus">準備完了</div>
    <div id="selectedCustomer"></div>
  </div>
  <script src="twilio.min.js"></script>
  <script src="app.js"></script>
</body>
</html>
public/app.js の実装
const Device = Twilio.Device;
const FUNCTIONS_BASE_URL = 'https://voice-poc-service-xxxx.twil.io';  // Twilio Function の URL を指定

let device;
let activeCall = null;
let selectedCustomer = null;

async function initializeTwilio() {
  // マイク許可
  await navigator.mediaDevices.getUserMedia({ audio: true, video: false });

  // トークン取得
  const { token } = await (await fetch(`${FUNCTIONS_BASE_URL}/token`)).json();

  // Device 作成 → register
  device = new Device(token);
  await device.register();

  // UI 切り替え
  document.getElementById('initSection').style.display = 'none';
  document.getElementById('callSection').style.display = 'block';

  // 以後の通話イベントは device で一元管理
  device.on('connect', (call) => {
    activeCall = call;
    document.getElementById('callStatus').textContent = '通話中...';
    document.getElementById('callButton').disabled = true;
    document.getElementById('hangupButton').disabled = false;

    call.on('disconnect', () => {
      activeCall = null;
      document.getElementById('callStatus').textContent = '通話終了';
      document.getElementById('callButton').disabled = false;
      document.getElementById('hangupButton').disabled = true;
    });
  });

  device.on('tokenWillExpire', async () => {
    const { token } = await (await fetch(`${FUNCTIONS_BASE_URL}/token`)).json();
    await device.updateToken(token);
  });
}

// 顧客一覧のロード&表示(電話番号はUI表示しない)
async function loadCustomers() {
  const { customers } = await (await fetch('/customers')).json();
  const list = document.getElementById('customerList');
  list.innerHTML = customers.map(c => `
    <div class="customer-item" data-customer='${JSON.stringify(c)}'>
      <strong>${c.name}</strong><br><small>${c.company}</small>
    </div>`).join('');
  [...document.querySelectorAll('.customer-item')].forEach(el => {
    el.addEventListener('click', () => {
      [...document.querySelectorAll('.customer-item')].forEach(i => i.classList.remove('selected'));
      el.classList.add('selected');
      selectedCustomer = JSON.parse(el.dataset.customer);
      document.getElementById('callButton').disabled = false;
      document.getElementById('selectedCustomer').textContent =
        `選択中: ${selectedCustomer.name}${selectedCustomer.company}`;
    });
  });
}

// 初期化ボタン
document.getElementById('initButton').addEventListener('click', async () => {
  await initializeTwilio();
  await loadCustomers();
});

// 発信
document.getElementById('callButton').addEventListener('click', () => {
  if (!selectedCustomer) return;
  document.getElementById('callStatus').textContent = '発信中...';
  device.connect({ params: { To: selectedCustomer.phoneNumber } }); // ✅
});

// 切断
document.getElementById('hangupButton').addEventListener('click', () => {
  activeCall?.disconnect();
});

動作検証

Twilio Functions の Deploy 確認

Functions Console で両方の Function が正常に Deploy されていることを確認します。

Function Deploy 確認

ローカルサーバー起動

node server.js

動作テスト

  1. http://localhost:3000 をブラウザで開く
    動作1
  2. 「音声機能を初期化」ボタンをクリックしマイク使用を承認
    動作2
  3. 顧客リストが表示されることを確認
    動作3
  4. 顧客を選択して「選択した顧客に発信」ボタンをクリック (電話番号はフロントエンドのUIに表示されない)
    動作4
  5. アプリ⇔外部電話番号間で問題なく通話が行えることを確認

まとめ

本記事では、Twilio Voice JavaScript SDK と Twilio Functions を組み合わせて、ブラウザから外部電話番号へ発信する仕組みを構築しました。顧客情報はサーバー側で管理しつつ、フロントエンドでは名前と会社名だけを表示することで、利用者同士の電話番号を秘匿したまま通話を成立させることができます。配車アプリやデリバリーサービスのようなユースケースに限らず、電話番号を共有せずにユーザー間で通話を実現したいあらゆる場面で活用できます。

次回の記事では、発信処理の次のステップとして着信処理の実装を紹介する予定です。双方向での通信を可能とすることで、より実運用に近い番号秘匿通話システムを構築できることが期待されます。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.