Asterisk の音声データを Incoming Webhook で MiiTel に連携する

🚧

Developer サイト内の URL 表記について

MiiTel のドメイン変更に伴い、Developer サイト内の一部ページに旧ドメインの URL が記載されている可能性があります。
現在、該当箇所の確認および修正を順次進めております。

この記事では、Asterisk PBX と MiiTel の Incoming Webhook を連携して、音声データ付きの通話履歴を作成する方法について説明します。

手順概要

  1. 必要な環境の準備
  2. Asterisk のインストール
  3. Asterisk の設定 (PJSIP とダイヤルプラン)
  4. MixMonitor による通話録音の有効化
  5. Webhook スクリプトの作成
  6. 動作確認

全体構成

+----------------------+     +------------------+     +------------------+     +--------+
|      SIP Client      |     |     Asterisk     |     |    SIP Trunk     |     |  PSTN  |
|   (Softphone/Phone)  |<--->|                  |<--->|   (e.g. Twilio)  |<--->|        |
+----------------------+     +--------+---------+     +------------------+     +--------+
                                      |
                                 (AGI on hangup)
                                      |
                                      v
                             +------------------+
                             |      MiiTel      |
                             | Incoming Webhook |
                             |       API        |
                             +------------------+

通話フロー

発信 (エージェントから顧客への発信):

SIP クライアント --> Asterisk [from-internal] --> SIP トランク --> PSTN --> 顧客

着信 (顧客からエージェントへの着信):

顧客 --> PSTN --> SIP トランク --> Asterisk [from-siptrunk] --> SIP クライアント

通話終了後、Asterisk のハングアップハンドラーが AGI スクリプトを実行し、通話データと録音ファイルを Incoming Webhook 経由で MiiTel に送信します。

1. 必要な環境の準備

開発者が用意するもの

  • Linux サーバー (例: Amazon EC2 またはオンプレミスサーバー)
  • SIP トランクプロバイダーからの SIP トランクと電話番号 (例: Twilio)
  • Incoming Webhook が有効化された MiiTel 環境
  • MiiTel Admin から取得した MiiTel ユーザー ID と Webhook ID

SIP トランクの設定

SIP トランクプロバイダーで SIP トランクを作成し、Asterisk サーバーのパブリック IP またはドメインを指す Origination URI を設定します。以下の例では Twilio を使用していますが、他の SIP トランクプロバイダーでも同様の手順で設定できます。

サーバーの準備

  1. サーバーに Elastic IP アドレス (またはパブリック IP) を取得します。
  2. 以下のポートを開放します (信頼できるソースのみに制限してください):
    • 5060/UDP — SIP シグナリング
    • 10000-20000/UDP — RTP メディア
  3. SIP トランクプロバイダー (例: Twilio) の IP 範囲を開放します。

2. Asterisk のインストール

サーバーに Asterisk の安定版をインストールします。以下の手順は Ubuntu 24 向けです。

ダウンロード先: https://www.asterisk.org/downloads/

# リポジトリの更新
apt-get update

# 必要なパッケージのインストール
apt-get install bison wget openssl libssl-dev libasound2-dev libc6-dev \
  libxml2-dev libsqlite3-dev libnewt-dev libncurses5-dev zlib1g-dev \
  gcc g++ make perl uuid-dev git subversion libjansson-dev \
  unixodbc-dev unixodbc autoconf libedit-dev bzip2 pkg-config

cd /usr/src

wget http://downloads.asterisk.org/pub/telephony/asterisk/old-releases/asterisk-<version>.tar.gz
tar -xzvf asterisk-<version>.tar.gz

# ビルドとインストール
cd asterisk-<version>
./configure
make && make install && make config && make samples

インストールの確認:

sudo systemctl enable asterisk
sudo systemctl start asterisk
asterisk -r

3. Asterisk の設定

PJSIP の設定

元の設定ファイルをバックアップし、新しいファイルを作成します:

mv /etc/asterisk/pjsip.conf /etc/asterisk/pjsip.conf.bak
vi /etc/asterisk/pjsip.conf
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060
external_media_address=<YOUR_PUBLIC_IP>
external_signaling_address=<YOUR_PUBLIC_IP>
local_net=<YOUR_PRIVATE_SUBNET>/255.255.255.0

;================================================================================
; SIP クライアント
;================================================================================
[6000]
type=endpoint
transport=transport-udp
context=from-internal
disallow=all
allow=ulaw
aors=6000
auth=6000

[6000]
type=auth
auth_type=userpass
username=6000
password=<YOUR_PASSWORD>

[6000]
type=aor
max_contacts=2

;================================================================================
; SIP トランクテンプレート (Twilio の例 — プロバイダーに合わせて調整してください)
;================================================================================
[twilio-endpoint](!)
type=endpoint
transport=transport-udp
context=from-siptrunk
disallow=all
allow=ulaw
force_rport=yes
rtp_symmetric=yes
rtp_keepalive=10
rtp_timeout=30
rtp_timeout_hold=60
from_domain=<YOUR_TWILIO_PSTN>.pstn.twilio.com

[twilio-identify](!)
type=identify
endpoint=twilio-trunk
; お使いのリージョンの Twilio IP 範囲を追加してください。
; 参照: https://www.twilio.com/docs/sip-trunking/ip-addresses
match=<TWILIO_IP_RANGE>

;================================================================================
; 電話番号ごとの SIP トランク (Twilio の例 — プロバイダーに合わせて調整してください)
;================================================================================
[twilio<YOUR_PHONE_NUMBER>](twilio-endpoint)
aors=twilio<YOUR_PHONE_NUMBER>
outbound_auth=twilio<YOUR_PHONE_NUMBER>

[twilio<YOUR_PHONE_NUMBER>]
type=auth
auth_type=userpass
username=twilio_user
password=<YOUR_PASSWORD>

[twilio<YOUR_PHONE_NUMBER>]
type=aor
max_contacts=10
contact=sip:<YOUR_TWILIO_PSTN>.pstn.twilio.com

[twilio<YOUR_PHONE_NUMBER>](twilio-identify)
endpoint=twilio<YOUR_PHONE_NUMBER>

設定を反映します:

asterisk -r
CLI> pjsip reload

ダイヤルプランの設定

元のファイルをバックアップし、ダイヤルプランを作成します:

📘

以下のダイヤルプランは日本 (+81) での使用を想定しています。お使いのリージョンに合わせて COUNTRY_CODE と電話番号のフォーマットを調整してください。

mv /etc/asterisk/extensions.conf /etc/asterisk/extensions.conf.bak
vi /etc/asterisk/extensions.conf
[globals]
COUNTRY_CODE=81
CALLERID_MAP_6000=<YOUR_PHONE_NUMBER>

[from-internal]
; 外部への発信
exten => _[0-9+].,1,Noop(from-internal)
 same =>   n,Ringing()
 same =>   n,Progress()
 same =>   n,Set(VOLUME(TX)=1)
 same =>   n,Set(VOLUME(RX)=1)
 same =>   n,Set(E164_NUMBER=${EXTEN})
 same =>   n,ExecIf($["${E164_NUMBER:0:1}"="+"]?Set(E164_NUMBER=${E164_NUMBER}):Set(E164_NUMBER=+${GLOBAL(COUNTRY_CODE)}${E164_NUMBER}))
 same =>   n,ExecIf($["${E164_NUMBER:0:1}"="0"]?Set(E164_NUMBER=+${GLOBAL(COUNTRY_CODE)}${E164_NUMBER:1}):Set(E164_NUMBER=${E164_NUMBER}))
 same =>   n,Set(CALLERID_NUM=${CALLERID_MAP_${CALLERID(num)}})
 same =>   n,Set(CALLERID(name)=+${CALLERID_NUM})
 same =>   n,Set(CALLERID(num)=+${CALLERID_NUM})
 same =>   n,Set(RECORDING_UUID=${UUID()})
 same =>   n,MixMonitor(${RECORDING_UUID}.raw,D)
 same =>   n,Set(CHANNEL(hangup_handler_push)=hangup_handler,s,1(${RECORDING_UUID},${CALLERID(num)},${E164_NUMBER},OUTGOING_CALL))
 same =>   n,Dial(PJSIP/${E164_NUMBER}@twilio${CALLERID_NUM})
 same =>   n,Return()

; 外部からの着信
[from-siptrunk]
exten => +<YOUR_PHONE_NUMBER>,1,GoSub(dial-extension,s,1(6000,${EXTEN}))

[dial-extension]
exten => s,1,Noop(from-siptrunk)
 same =>   n,Set(VOLUME(TX)=1)
 same =>   n,Set(VOLUME(RX)=1)
 same =>   n,Set(RECORDING_UUID=${UUID()})
 same =>   n,MixMonitor(${RECORDING_UUID}.raw,D)
 same =>   n,Set(CHANNEL(hangup_handler_push)=hangup_handler,s,1(${RECORDING_UUID},${CALLERID(num)},${ARG2},INCOMING_CALL))
 same =>   n,Dial(PJSIP/${ARG1})
 same =>   n,Return()

[hangup_handler]
exten => s,1,NoOp(Hangup handler: UUID=${ARG1} FROM=${ARG2} TO=${ARG3} DIR=${ARG4})
 same =>   n,AGI(call_webhooks.py,${ARG1},${ARG2},${ARG3},${ARG4})
 same =>   n,Return()

ダイヤルプランを反映します:

CLI> dialplan reload

4. MixMonitor による通話録音の有効化

MixMonitor は通話の両チャネルの音声を録音します。MiiTel の音声解析 (Talk:Listen 比率、話者識別、AI スコアリングなど) を正しく動作させるには、ステレオ音声 (話者別のチャンネル) が必要です。

ドキュメント: https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/MixMonitor/

ステレオ出力のための録音オプション

デフォルトの MixMonitor(filename.wav) は両方の話者を 単一のモノラルチャンネル にミックスします。これでは MiiTel が話者を区別できません。ステレオ音声を生成するには、以下のいずれかの方法を使用してください:

オプション A: D オプション

D オプションを使用すると、インターリーブされた 2 チャンネル (ステレオ) の raw ストリームが出力されます。ファイル拡張子は 必ず .raw を使用してください — 他の拡張子ではファイルが破損します。

same => n,MixMonitor(${RECORDING_UUID}.raw,D)

AGI スクリプトで Python 標準ライブラリの wave モジュールを使用して .raw.wav に変換します (外部依存なし):

import wave

with open(raw_file, "rb") as f:
    raw_data = f.read()
with wave.open(wav_file, "wb") as w:
    w.setnchannels(2)       # ステレオ
    w.setsampwidth(2)       # 16-bit
    w.setframerate(8000)    # 8kHz (電話音声の標準)
    w.writeframes(raw_data)

オプション B: r() と t() オプション

r (受信) と t (送信) オプションを使用して各方向を別ファイルに録音し、sox でステレオファイルにマージします:

same => n,MixMonitor(${RECORDING_UUID}.wav,r(${RECORDING_UUID}-in.wav)t(${RECORDING_UUID}-out.wav))

通話後に sox でマージ:

sox -M ${RECORDING_UUID}-in.wav ${RECORDING_UUID}-out.wav ${RECORDING_UUID}-stereo.wav

注意: オプション B はサーバーに sox のインストールが必要です (apt install sox)。

デフォルトの録音保存先

デフォルトでは、録音ファイルは以下のディレクトリに保存されます:

/var/spool/asterisk/monitor/

テスト通話後に録音ファイルが保存されていることを確認してください:

ls -la /var/spool/asterisk/monitor/

5. Webhook スクリプトの作成

通話メタデータの送信と録音ファイルのアップロードを行う AGI スクリプトを作成します。このスクリプトはダイヤルプランのハングアップハンドラーから RECORDING_UUIDFROM_NUMBERTO_NUMBERDIRECTION (OUTGOING_CALL または INCOMING_CALL) を受け取ります。

Webhook の作成方法については、以下を参照してください:

vi /var/lib/asterisk/agi-bin/call_webhooks.py
#!/usr/bin/python3

import sys
import os
import time
import wave
import requests
from datetime import datetime, timezone

# === Read AGI environment ===
agi_env = {}
while True:
    line = sys.stdin.readline().strip()
    if not line:
        break
    if ': ' in line:
        key, _, value = line.partition(': ')
        agi_env[key] = value

def agi_command(cmd):
    sys.stdout.write(f"{cmd}\n")
    sys.stdout.flush()
    return sys.stdin.readline().strip()

def get_variable(name):
    result = agi_command(f"GET VARIABLE {name}")
    if "1 (" in result:
        return result.split("(", 1)[1].rstrip(")")
    return ""

def to_iso(asterisk_time):
    if not asterisk_time:
        return None
    try:
        dt = datetime.strptime(asterisk_time, "%Y-%m-%d %H:%M:%S")
        return dt.replace(tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    except ValueError:
        return None

# === AGI arguments from dialplan ===
RECORDING_UUID = agi_env.get('agi_arg_1', '')
FROM_NUMBER = agi_env.get('agi_arg_2', '')
TO_NUMBER = agi_env.get('agi_arg_3', '')
DIRECTION = agi_env.get('agi_arg_4', 'OUTGOING_CALL')

# === Get call timestamps from CDR ===
call_starts_at = to_iso(get_variable("CDR(start)"))
call_answered_at = to_iso(get_variable("CDR(answer)"))
call_ends_at = to_iso(get_variable("CDR(end)"))

if not call_ends_at:
    call_ends_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
if not call_starts_at:
    call_starts_at = call_ends_at

# === Config ===
TENANT_CODE = "<YOUR_TENANT_CODE>"
WEBHOOK_ID = "<YOUR_WEBHOOK_ID>"
RECORDING_DIR = "/var/spool/asterisk/monitor"
BASE_URL = f"https://{TENANT_CODE}.miitel.jp/api/call/v2/webhook_pbx/{WEBHOOK_ID}"
YOUR_MIITEL_USER_UUID = "<YOUR_MIITEL_USER_UUID>"

RAW_FILE = f"{RECORDING_DIR}/{RECORDING_UUID}.raw"
WAV_FILE = f"{RECORDING_DIR}/{RECORDING_UUID}-stereo.wav"

# === Convert raw interleaved stereo to wav ===
agi_command(f'VERBOSE "Converting {RAW_FILE} to stereo wav" 1')

if not os.path.exists(RAW_FILE):
    agi_command(f'VERBOSE "Raw file not found: {RAW_FILE}" 1')
    sys.exit(0)

try:
    with open(RAW_FILE, "rb") as raw:
        raw_data = raw.read()
    with wave.open(WAV_FILE, "wb") as w:
        w.setnchannels(2)
        w.setsampwidth(2)
        w.setframerate(8000)
        w.writeframes(raw_data)
except Exception as e:
    agi_command(f'VERBOSE "Wav conversion failed: {e}" 1')
    sys.exit(0)

# === Build participants ===
if DIRECTION == "OUTGOING_CALL":
    from_user_id = YOUR_MIITEL_USER_UUID
    to_user_id = None
else:
    from_user_id = None
    to_user_id = YOUR_MIITEL_USER_UUID

# === Step 1: POST call metadata ===
payload = {
    "call_data": [
        {
            "call_data_id": f"call-{int(time.time())}",
            "audio_file_type": "wav",
            "audio_channel_type": "STEREO",
            "event_type": DIRECTION,
            "call_starts_at": call_starts_at,
            "call_answered_at": call_answered_at,
            "call_ends_at": call_ends_at,
            "circuit_number": FROM_NUMBER,
            "group_name": None,
            "queue_name": None,
            "participants": [
                {
                    "type": "FROM",
                    "stereo_lr": "left",
                    "miitel_user_id": from_user_id,
                    "number": FROM_NUMBER,
                    "name": "Agent" if DIRECTION == "OUTGOING_CALL" else "Customer",
                },
                {
                    "type": "TO",
                    "stereo_lr": "right",
                    "miitel_user_id": to_user_id,
                    "number": TO_NUMBER,
                    "name": "Customer" if DIRECTION == "OUTGOING_CALL" else "Agent",
                },
            ],
            "tags": [{"value": "Record from asterisk"}],
        }
    ]
}

agi_command(f'VERBOSE "Webhook: {DIRECTION} from={FROM_NUMBER} to={TO_NUMBER}" 1')

resp = requests.post(BASE_URL, json=payload, timeout=30)
agi_command(f'VERBOSE "Webhook status: {resp.status_code}" 1')

if resp.status_code != 201:
    agi_command(f'VERBOSE "Webhook failed: {resp.text}" 1')
    sys.exit(0)

data = resp.json()
upload_info = list(data["audio_upload_urls"][0].values())[0]
presigned_url = upload_info["url"]

# === Step 2: Upload stereo .wav ===
agi_command(f'VERBOSE "Uploading {WAV_FILE}" 1')

with open(WAV_FILE, "rb") as f:
    upload_resp = requests.put(presigned_url, data=f, timeout=60)

agi_command(f'VERBOSE "Upload status: {upload_resp.status_code}" 1')

# === Cleanup raw file ===
try:
    os.remove(RAW_FILE)
except OSError:
    pass

sys.exit(0)

スクリプトに実行権限を付与します:

chmod +x /var/lib/asterisk/agi-bin/call_webhooks.py

6. 動作確認

SIP クライアントの設定

SIP 登録に対応した SIP クライアント (ソフトフォンまたは SIP 電話) を使用してください。例えば Zoiper が利用できます: https://www.zoiper.com/en/voip-softphone/download/current

Asterisk の pjsip.conf に設定した認証情報 (ドメイン、ユーザー名、パスワード) で SIP クライアントを設定してください。

テスト通話の実行

  1. SIP クライアントから通話を発信します。
  2. Asterisk CLI でデバッグ出力を確認します:
sudo asterisk -vvvvvr
  1. 通話終了後、以下を確認してください:
    • /var/spool/asterisk/monitor/ に録音ファイルが存在すること
    • ハングアップハンドラーで AGI スクリプトが実行されたこと
    • MiiTel Analytics にアップロードされた音声付きの通話履歴が表示されること

CLI 出力例

以下は、MixMonitor による録音と AGI Webhook の実行が成功した発信通話の例です:

$ sudo asterisk -vvvvvr
Asterisk 22.9.0, Copyright (C) 1999 - 2025, Sangoma Technologies Corporation and others.
=========================================================================
Connected to Asterisk 22.9.0 currently running on localhost (pid = 124669)
    -- Executing [+81xxxxxxxxxxx@from-internal:1] NoOp("PJSIP/6000-00000032", "from-internal") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:2] Ringing("PJSIP/6000-00000032", "") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:3] Progress("PJSIP/6000-00000032", "") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:4] Set("PJSIP/6000-00000032", "VOLUME(TX)=1") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:5] Set("PJSIP/6000-00000032", "VOLUME(RX)=1") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:6] Set("PJSIP/6000-00000032", "E164_NUMBER=+81xxxxxxxxxxx") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:7] ExecIf("PJSIP/6000-00000032", "1?Set(...)") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:8] ExecIf("PJSIP/6000-00000032", "0?Set(...)") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:9] Set("PJSIP/6000-00000032", "CALLERID_NUM=81xxxxxxxxxx") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:10] Set("PJSIP/6000-00000032", "CALLERID(name)=+81xxxxxxxxxx") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:11] Set("PJSIP/6000-00000032", "CALLERID(num)=+81xxxxxxxxxx") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:12] Set("PJSIP/6000-00000032", "RECORDING_UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") in new stack

    -- Executing [+81xxxxxxxxxxx@from-internal:13] MixMonitor("PJSIP/6000-00000032", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.raw,D") in new stack  ◄── [MixMonitor] ステレオ録音開始
  == Begin MixMonitor Recording PJSIP/6000-00000032                        ◄── [MixMonitor] 音声キャプチャ中

    -- Executing [+81xxxxxxxxxxx@from-internal:14] Set("PJSIP/6000-00000032", "CHANNEL(hangup_handler_push)=hangup_handler,s,1(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)") in new stack
    -- Executing [+81xxxxxxxxxxx@from-internal:15] Dial("PJSIP/6000-00000032", "PJSIP/+81xxxxxxxxxxx@twilio81xxxxxxxxxx") in new stack
    -- Called PJSIP/+81xxxxxxxxxxx@twilio81xxxxxxxxxx
    -- PJSIP/twilio81xxxxxxxxxx-00000033 is making progress passing it to PJSIP/6000-00000032
    -- PJSIP/twilio81xxxxxxxxxx-00000033 answered PJSIP/6000-00000032      ◄── SIP トランク経由で通話接続
    -- Channel PJSIP/twilio81xxxxxxxxxx-00000033 joined 'simple_bridge' basic-bridge <...>
    -- Channel PJSIP/6000-00000032 joined 'simple_bridge' basic-bridge <...>
    -- Channel PJSIP/6000-00000032 left 'simple_bridge' basic-bridge <...>
    -- Channel PJSIP/twilio81xxxxxxxxxx-00000033 left 'simple_bridge' basic-bridge <...>
  == Spawn extension (from-internal, +81xxxxxxxxxxx, 15) exited non-zero on 'PJSIP/6000-00000032'

    -- PJSIP/6000-00000032 Internal Gosub(hangup_handler,s,1(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)) start  ◄── ハングアップハンドラー起動
    -- Executing [s@hangup_handler:1] NoOp("PJSIP/6000-00000032", "Hangup handler running for xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") in new stack
    -- Executing [s@hangup_handler:2] AGI("PJSIP/6000-00000032", "call_webhooks.py,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") in new stack
    -- Launched AGI Script /var/lib/asterisk/agi-bin/call_webhooks.py       ◄── [AGI] Webhook スクリプト開始
    -- <PJSIP/6000-00000032>AGI Script call_webhooks.py completed, returning 0  ◄── [AGI] スクリプト成功 (return 0)
    -- Executing [s@hangup_handler:3] Return("PJSIP/6000-00000032", "") in new stack
  == Spawn extension (from-internal, +81xxxxxxxxxxx, 15) exited non-zero on 'PJSIP/6000-00000032'
    -- PJSIP/6000-00000032 Internal Gosub(hangup_handler,s,1(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)) complete GOSUB_RETVAL=

  == MixMonitor close filestream (mixed)                                    ◄── [MixMonitor] 録音ファイル保存
  == End MixMonitor Recording PJSIP/6000-00000032                          ◄── [MixMonitor] 録音完了

フローのまとめ:

  1. MixMonitor が通話開始時に録音を開始します (Begin MixMonitor Recording)。
  2. SIP トランク経由で通話が接続され、両者がブリッジされます。
  3. 通話終了後、Asterisk がハングアップハンドラーを起動し、AGI スクリプト (call_webhooks.py) を実行します。
  4. AGI スクリプトが通話メタデータを MiiTel の Incoming Webhook API に POST し、署名付き URL 経由で録音された .wav ファイルをアップロードします。
  5. MixMonitor が録音ファイルを確定します (End MixMonitor Recording)。

お問い合わせ

ご質問がある場合は、フォーラム からお問い合わせください。