はじめに

こんにちは。and factory でバックエンドエンジニアをしている江州です。

Twilioを使ってGoで通話機能を実装する方法を調べたとき、公式ドキュメントは充実しているもののNode.jsやPythonの例が多く、Goでの実装例はまだ少ないと感じました。

この記事では、Twilioの公式Go SDK(twilio-go)を使い、Conference(会議室)ベースの通話システムを構築するパターンを紹介します。単純な1対1の電話だけでなく、通話中のアナウンス・DTMF認証・VoIP/PSTN両対応・録音といった実用的な要素をカバーします。

対象読者

  • GoでTwilioを使った通話機能を構築しようとしている方
  • TwilioのConference / TwiML / VoIPの組み合わせを知りたい方
  • twilio-go SDKの具体的な使い方を知りたい方

動作確認環境

種別バージョン
Go1.24
twilio-gov1 系

1. twilio-go SDK のセットアップ

Twilioの公式Go SDK twilio-go をインストールします。

1
go get github.com/twilio/twilio-go

REST APIクライアントの作成は次のとおりです。

1
2
3
4
5
6
import "github.com/twilio/twilio-go"

client := twilio.NewRestClientWithParams(twilio.ClientParams{
    Username: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // Account SID
    Password: "your_auth_token",                    // Auth Token
})

環境変数 TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN を設定しておけば、引数なしの twilio.NewRestClient() でもクライアントを作成できます。


2. なぜ Conference(会議室)ベースなのか

Twilioで2者間の通話を実現するには、主に2つのアプローチがあります。

方式概要特徴
<Dial> 直接接続発信者から直接相手に接続シンプルだが、通話中の制御が限定的
<Conference>仮想会議室に参加者を合流通話中アナウンス・録音・参加者管理が柔軟

単純な転送や直接通話なら <Dial> で十分です。しかし通話中のアナウンスや会議室単位の録音、参加者の入退室トラッキングが必要であればConference一択です。

Conference の基本フロー

sequenceDiagram
    participant A as 参加者A
    participant T as Twilio
    participant S as サーバー
    participant B as 参加者B

    A->>T: 電話をかける
    T->>S: Webhook(着信)
    S->>T: TwiML: 会議室Xに入室(保留音で待機)
    S->>T: MakeCall(参加者Bに発信)
    T->>B: 着信
    B->>T: 応答
    T->>S: Webhook(応答)
    S->>T: TwiML: 会議室Xに入室(録音開始)
    Note over A,B: 会議室Xで通話開始

参加者Aが先に会議室へ入って保留音を聞きながら待ち、参加者Bが入室した時点で通話が始まります。


3. TwiML生成をGoで型安全に書く

TwiML(Twilio Markup Language)は、Twilioに通話の制御方法を指示するXMLです。twilio-go SDKには twiml パッケージが含まれており、XMLを文字列で組み立てる必要がありません。

基本: VoiceSay

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import "github.com/twilio/twilio-go/twiml"

func sayXML(message string) (string, error) {
    say := &twiml.VoiceSay{
        Language: "ja-JP",
        Voice:    "Polly.Mizuki",  // Amazon Polly の日本語音声
        Message:  message,
    }
    return twiml.Voice([]twiml.Element{say})
}

twiml.Voice()twiml.Element のスライスを渡すと、TwiMLのXML文字列が生成されます。上の例では次のようなXMLになります。

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say language="ja-JP" voice="Polly.Mizuki">こんにちは</Say>
</Response>

Conference に入室させるTwiML

参加者を会議室に入室させるTwiMLの例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func joinConferenceXML(roomName, statusCallbackURL string) (string, error) {
    say := &twiml.VoiceSay{
        Language: "ja-JP",
        Voice:    "Polly.Mizuki",
        Message:  "担当者にお繋ぎしますので、少々お待ちください。",
    }

    conference := &twiml.VoiceConference{
        Name:                   roomName,
        StartConferenceOnEnter: "false",
        EndConferenceOnExit:    "true",
        StatusCallbackEvent:    "start end join leave",
        StatusCallback:         statusCallbackURL,
        StatusCallbackMethod:   "POST",
        WaitUrl:                "http://com.twilio.music.classical.s3.amazonaws.com/ith_brahms-116-4.mp3",
        WaitMethod:             "GET",
    }

    dial := &twiml.VoiceDial{
        InnerElements: []twiml.Element{conference},
    }

    return twiml.Voice([]twiml.Element{say, dial})
}

重要なパラメータの解説です。

パラメータ説明
StartConferenceOnEnter"false"この参加者の入室では会議を開始しない(待機側に設定)
EndConferenceOnExit"true"この参加者が退室したら会議終了
StatusCallbackEvent"start end join leave"受け取りたいイベントを指定
WaitUrl(Twilioの保留音URL)会議開始まで再生される保留音

もう一方の参加者(会議を開始する側)では StartConferenceOnEnter"true" にします。

1
2
3
4
5
6
7
conference := &twiml.VoiceConference{
    Name:                   roomName,
    StartConferenceOnEnter: "true",
    EndConferenceOnExit:    "true",
    Record:                 "record-from-start",
    Trim:                   "do-not-trim",
}

TwiML生成のベストプラクティス

TwiMLは通話のフェーズごとに異なるXMLが必要になります。1つの関数で条件分岐するのではなく、フェーズごとに独立した関数を作ると見通しがよくなります。

1
2
3
4
5
func waitInConferenceXML(roomName string) (string, error) { ... }    // 待機側入室
func startConferenceXML(roomName string) (string, error) { ... }     // 開始側入室(録音付き)
func gatherPasswordXML(actionURL string) (string, error) { ... }     // パスワード入力要求
func callEndReminderXML(minutes int) (string, error) { ... }         // 残り時間アナウンス
func hangupWithMessageXML(message string) (string, error) { ... }    // メッセージ後切断

4. 発信と通話更新

MakeCall: 発信

twilio-goでの発信は Api.CreateCall を使います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import api "github.com/twilio/twilio-go/rest/api/v2010"

params := &api.CreateCallParams{}
params.SetTo("+819012345678")
params.SetFrom("+815012345678")
params.SetUrl("https://example.com/twiml/answer")  // 応答時のTwiML URL
params.SetStatusCallback("https://example.com/callback/status")
params.SetStatusCallbackEvent([]string{"answered", "completed"})
params.SetTimeout(45) // 着信タイムアウト(秒)

resp, err := client.Api.CreateCall(params)
if err != nil {
    log.Fatal(err)
}
fmt.Println("CallSid:", *resp.Sid)

Url に指定したエンドポイントは、相手が応答したときにTwilioがGETまたはPOSTするWebhookです。ここでConference入室用のTwiMLを返せば、自動的に会議室に参加させられます。

TwiML パラメータを使えば、URLの代わりにTwiML文字列を直接渡すこともできます。

1
2
xml, _ := joinConferenceXML("room-123", callbackURL)
params.SetTwiml(xml)  // Urlの代わりにTwiMLを直接指定

UpdateCall: 通話中の制御

進行中の通話に対して、TwiMLの差し替えや切断指示を送れます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
params := &api.UpdateCallParams{}

// パターン1: 新しいTwiMLを適用(アナウンス後に切断など)
xml, _ := hangupWithMessageXML("まもなく通話が終了します。")
params.SetTwiml(xml)

// パターン2: 強制切断
// params.SetStatus("completed")

resp, err := client.Api.UpdateCall("CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", params)

SetTwiml でTwiMLを差し替えると、現在再生中の音声やConferenceが中断され、新しいTwiMLの実行に切り替わります。終了案内を流してから切断したい場合に便利です。

UpdateConference: 会議室アナウンス

Conference参加者全員にアナウンスを流すには UpdateConference を使います。

1
2
3
4
5
params := &api.UpdateConferenceParams{}
params.SetAnnounceUrl("https://example.com/twiml/reminder")
params.SetAnnounceMethod("POST")

_, err := client.Api.UpdateConference("CFxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", params)

AnnounceUrl に指定したエンドポイントが返すTwiMLの <Say> が全参加者に再生されます。通話を中断せずにアナウンスだけ割り込ませられるのがポイントです。


5. DTMF によるパスワード認証

通話参加前にDTMFトーン(プッシュボタン音)で認証する仕組みは <Gather> で実現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func gatherPasswordXML(actionURL string) (string, error) {
    say := &twiml.VoiceSay{
        Language: "ja-JP",
        Voice:    "Polly.Mizuki",
        Message:  "4桁のパスワードを入力してください。",
    }

    gather := &twiml.VoiceGather{
        Input:         "dtmf",
        Timeout:       "30",
        NumDigits:     "4",
        Action:        actionURL,  // 入力結果の送信先
        Method:        "POST",
        InnerElements: []twiml.Element{say},
    }

    noInputSay := &twiml.VoiceSay{
        Language: "ja-JP",
        Voice:    "Polly.Mizuki",
        Message:  "入力がありませんでした。通話を終了します。",
    }
    hangup := &twiml.VoiceHangup{}

    return twiml.Voice([]twiml.Element{gather, noInputSay, hangup})
}

<Gather> の動作は次のとおりです。

  1. InnerElements のガイダンスを再生しながらDTMF入力を待つ
  2. 指定桁数の入力があると Action URLにPOST(入力値は Digits パラメータ)
  3. Timeout 秒間入力がなければ <Gather> の後の要素に進む

Action URLのハンドラ側で入力値を検証し、正しければConference入室用のTwiMLを返し、間違っていればリトライ用のTwiMLを返します。パスワードの比較には crypto/subtleConstantTimeCompare を使い、タイミング攻撃を防ぎます。また、試行回数の上限を設けて総当たりを防ぐことが重要です(4桁PINは10,000通りしかないため、制限なしだと1回の通話で全パターンを試せてしまいます)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "crypto/subtle"

const maxPasswordAttempts = 3

func handlePasswordCallback(w http.ResponseWriter, r *http.Request) {
    digits := r.FormValue("Digits")
    attempt := getAttemptCount(r) // 1始まり。クエリパラメータやセッションで管理

    var xml string
    if len(expectedPassword) == 0 {
        xml, _ = hangupWithMessageXML("システムエラーが発生しました。")
    } else if subtle.ConstantTimeCompare([]byte(digits), []byte(expectedPassword)) == 1 {
        xml, _ = startConferenceXML("room-123")
    } else if attempt >= maxPasswordAttempts {
        xml, _ = hangupWithMessageXML("認証に失敗しました。通話を終了します。")
    } else {
        xml, _ = gatherPasswordRetryXML(actionURL, attempt+1)
    }

    w.Header().Set("Content-Type", "application/xml")
    w.Write([]byte(xml))
}

6. VoIP と PSTN の両対応

Twilioは電話回線(PSTN)だけでなく、モバイルアプリからのVoIP通話もサポートしています。

PSTN(電話番号)への発信

通常の電話番号にかける場合は、To にE.164形式の電話番号を指定します。

1
params.SetTo("+819012345678")

PSTN向けには**AMD(Answering Machine Detection)**の有効化を検討しましょう。留守番電話への接続を検知して無駄な通話料を抑えられます。

1
params.SetMachineDetection("Enable")

ただしAMDにはトレードオフがあります。AMD自体が有料オプションで、応答した通話1件ごとに追加課金が発生します。また既定の同期モード(AsyncAmd=false)では判定完了までTwilioが通話をブロックするため、接続にラグが乗ります。コストと体験のバランスを考慮して導入を判断してください。

VoIP(アプリ)への発信

モバイルアプリのTwilio Voice SDKに向けて発信する場合は、Toclient: プレフィックス付きのidentityを指定します。

1
params.SetTo("client:user-12345")

アプリ側がVoIP通話を受けるためには、Access Token(JWT)が必要です。twilio-goのJWTパッケージで発行できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import twilioJwt "github.com/twilio/twilio-go/client/jwt"

accessToken := twilioJwt.CreateAccessToken(twilioJwt.AccessTokenParams{
    AccountSid:    accountSid,
    SigningKeySid: apiKeySid,
    Secret:        apiSecret,
    Identity:      "user-12345",
    Ttl:           3600, // トークン有効期間(秒)
})

accessToken.AddGrant(&twilioJwt.VoiceGrant{
    Outgoing: twilioJwt.Outgoing{
        ApplicationSid: twimlAppSid,
    },
    Incoming: twilioJwt.Incoming{
        Allow: true,
    },
    PushCredentialSid: pushCredentialSid, // APNs or FCM
})

jwtToken, err := accessToken.ToJwt()
パラメータ説明
SigningKeySid / SecretTwilio ConsoleのAPI Keysで発行
ApplicationSidTwiML Appの SID(発信時のWebhook設定)
PushCredentialSidプッシュ通知用の認証情報SID(iOS APNs / Android FCM)

iOSとAndroidでプッシュ通知の仕組みが異なるため、PushCredentialSid はプラットフォームごとに切り替える必要があります。

VoIP vs PSTN の判定

発信先がVoIPかPSTNかで処理を分けたい場合、client: プレフィックスの有無で判定できます。

1
2
3
4
5
6
if strings.HasPrefix(to, "client:") {
    // VoIP: AMDは不要
} else {
    // PSTN: AMD有効化を検討
    params.SetMachineDetection("Enable")
}

電話番号のE.164変換

Twilio APIはE.164形式(+81 + 番号)を要求します。日本の国内番号からの変換は単純な文字列操作で実現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var (
    reNonDigit = regexp.MustCompile(`\D`)
    reE164     = regexp.MustCompile(`^\+[1-9]\d{1,14}$`)
)

func toE164Japan(phoneNumber string) (string, error) {
    // 既にE.164形式なら検証して素通し
    if strings.HasPrefix(phoneNumber, "+") {
        if !reE164.MatchString(phoneNumber) {
            return "", fmt.Errorf("無効なE.164番号: %s", phoneNumber)
        }
        return phoneNumber, nil
    }

    // ハイフンなどの非数字を除去
    cleaned := reNonDigit.ReplaceAllString(phoneNumber, "")

    // 国内番号の先頭が0であることを確認
    if len(cleaned) == 0 || cleaned[0] != '0' {
        return "", fmt.Errorf("無効な国内電話番号: %s", phoneNumber)
    }

    // 国内番号は10桁(固定)または11桁(携帯)
    if len(cleaned) < 10 || len(cleaned) > 11 {
        return "", fmt.Errorf("無効な桁数の電話番号: %s", phoneNumber)
    }

    // 先頭の0を+81に置換
    return "+81" + cleaned[1:], nil
}

// "090-1234-5678" → "+819012345678"
// "+819012345678" → "+819012345678"(そのまま返す)

ユーザーへの表示時にはE.164を国内形式に戻します。

1
2
3
4
5
6
7
8
func toLocalFormat(e164 string) string {
    if strings.HasPrefix(e164, "+81") {
        return "0" + e164[3:]
    }
    return e164
}

// "+819012345678" → "09012345678"

7. 通話録音

Conferenceの録音機能を使えば、通話内容を自動的に録音できます。

録音の設定

Conferenceに入室するTwiMLで録音パラメータを指定します。

1
2
3
4
5
6
7
8
9
conference := &twiml.VoiceConference{
    Name:                          roomName,
    StartConferenceOnEnter:        "true",
    Record:                        "record-from-start",
    Trim:                          "do-not-trim",
    RecordingStatusCallback:       "https://example.com/callback/recording",
    RecordingStatusCallbackMethod: "POST",
    RecordingStatusCallbackEvent:  "completed",
}
パラメータ推奨値理由
Recordrecord-from-start会議開始と同時に録音開始
Trimdo-not-trim無音部分も含めて完全に録音(証跡用途の場合)
RecordingStatusCallbackEventcompleted録音完了時にのみコールバック

Record は待機側(StartConferenceOnEnter: "false")ではなく、会議を開始する側のConference要素に設定します。

録音ファイルのダウンロード

録音が完了すると、RecordingStatusCallback に録音の情報がPOSTされます。RecordingUrl パラメータに録音ファイルのURLが含まれるので、そこからダウンロードします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func downloadRecording(recordingURL, accountSid, authToken string) (io.ReadCloser, error) {
    // SSRF対策: Twilio APIのURLであることを検証
    if err := validateRecordingURL(recordingURL); err != nil {
        return nil, err
    }

    // MP3形式でデュアルチャンネル(話者分離)ダウンロード
    downloadURL := recordingURL + ".mp3?RequestedChannels=2"

    req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
    if err != nil {
        return nil, err
    }
    req.SetBasicAuth(accountSid, authToken)

    client := &http.Client{
        Timeout: 5 * time.Minute,
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            if len(via) >= 10 {
                return fmt.Errorf("リダイレクトが多すぎます")
            }
            return validateRecordingURL(req.URL.String())
        },
    }
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    if resp.StatusCode != http.StatusOK {
        resp.Body.Close()
        return nil, fmt.Errorf("録音ダウンロード失敗: status=%d", resp.StatusCode)
    }

    return resp.Body, nil
}
  • 拡張子(.mp3)を付けることでフォーマットを指定(デフォルトはWAV)
  • RequestedChannels=2 でデュアルチャンネル(左右で話者が分離)を取得できる。ただしConference録音ではTwilio ConsoleのVoice Recording settingsで事前にDual-channel設定を有効化しておくこと。未有効化で指定すると400が返る
  • Twilio APIへのリクエストにはBasic認証が必要
  • CheckRedirect でリダイレクト先のホストも検証し、SSRF対策をリダイレクトチェーンにも適用
  • 長時間録音はサイズが大きくなるため、タイムアウトを長めに設定

SSRF対策

録音のコールバックで受け取った RecordingUrl をそのままHTTPリクエストに使う場合、SSRF(Server-Side Request Forgery) に注意が必要です。万が一コールバックが改ざんされた場合、内部ネットワークへのアクセスに悪用される可能性があります。

対策として、URLが https://api.twilio.com/ で始まることを検証します。

1
2
3
4
5
6
7
8
const twilioAPIBaseURL = "https://api.twilio.com/"

func validateRecordingURL(url string) error {
    if !strings.HasPrefix(url, twilioAPIBaseURL) {
        return fmt.Errorf("不正なRecordingURL: Twilio APIのURLではありません")
    }
    return nil
}

8. ステータスコールバック

Twilioは通話やConferenceの状態が変化するたびにWebhookを送信します。これを使ってバックエンド側で通話状態をトラッキングできます。

通話ステータス

MakeCall 時に StatusCallbackStatusCallbackEvent を設定すると、指定したイベントでコールバックが届きます。

イベントタイミング
initiated発信開始
ringing相手が着信中
answered相手が応答
completed通話終了

すべてのイベントを受け取ることもできますが、実際の業務ロジックで必要なのは answeredcompleted だけというケースが多いです。不要なコールバックを減らすことで、Webhookエンドポイントの負荷を軽減できます。

Conference イベント

Conferenceのステータスコールバックでは、通話ステータスとは別のイベントが使われます。

イベント用途例
start会議開始 → 通話開始時刻の記録
end会議終了 → 通話終了処理、課金計算
join参加者入室 → もう一方の参加者への発信トリガー
leave参加者退室 → もう一方の参加者への通知

特に join イベントは有用です。PSTNユーザーの場合、ユーザーが実際に電話を取ってConferenceに入室(join)してからもう一方の参加者に発信することで、通話確立前に相手を呼び出してしまう問題を回避できます。

1
2
3
4
5
// StatusCallbackEventに "join" を含めると、参加者入室時にコールバックが届く
conference := &twiml.VoiceConference{
    StatusCallbackEvent: "start end join leave",
    StatusCallback:      callbackURL,
}

9. Webhookの署名検証

SSRF対策と並んで重要なのが、Webhookリクエスト自体の正当性検証です。TwilioはすべてのWebhookリクエストに X-Twilio-Signature ヘッダを付与しています。twilio-goの RequestValidator を使えば、Auth Tokenでリクエストの正当性を検証できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import "github.com/twilio/twilio-go/client"

func validateTwilioRequest(r *http.Request, baseURL, authToken string) bool {
    requestValidator := client.NewRequestValidator(authToken)

    fullURL := baseURL + r.URL.RequestURI()
    params := make(map[string]string)
    if err := r.ParseForm(); err != nil {
        return false
    }
    for key, values := range r.PostForm {
        params[key] = values[0]
    }
    signature := r.Header.Get("X-Twilio-Signature")

    return requestValidator.Validate(fullURL, params, signature)
}

baseURL はアプリケーションの設定値(例: https://api.example.com)から渡します。Twilioが署名を生成する際のURLと完全一致する必要があるため、ハードコードではなくTwilio Consoleに設定したものと同じ値を使ってください。なお r.URL.Path ではなく r.URL.RequestURI() を使っているのは、クエリ文字列やpercent-encodingを保持するためです。Path はクエリを含まずURLデコードも行うため、署名が一致しなくなります。

この検証をミドルウェアに組み込めば、Twilio以外からの偽リクエストを排除できます。ステータスコールバック・録音コールバック・DTMF入力コールバックなど、すべてのTwilio Webhookエンドポイントに適用しましょう。


10. 認証情報の管理

Twilioの認証情報(Account SID / Auth Token / API Key)は厳重に管理する必要があります。

環境別の設定

開発・ステージング・本番で異なるTwilioアカウントや電話番号を使う場合の管理方法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type TwilioConfig struct {
    AccountSID            string
    AuthToken             string
    PhoneNumber           string
    TwiMLAppSID           string
    APIKeySID             string
    APISecret             string
    PushCredentialSIDiOS  string
    PushCredentialSIDFCM  string
}
  • ローカル開発: 環境変数または .env ファイル
  • ステージング/本番: AWS SSM Parameter StoreやGCP Secret Managerなどのシークレット管理サービス

いずれの場合も、AccountSIDやAuthTokenをソースコードや設定ファイルにハードコードしないことが鉄則です。

Twilio テスト用のクレデンシャル

Twilioはテスト用のクレデンシャルを提供しています。テスト用のAccount SIDとAuth Tokenを使えば、実際の通話は発生せず、課金もされません。自動テストではこれを活用しましょう。


11. 実装上のTips

タイムアウトの設定

MakeCall のデフォルトタイムアウトは60秒ですが、ユースケースに応じて短くすることを検討しましょう。

1
params.SetTimeout(45)

60秒だと留守電が応答してしまうケースがあります。45秒程度に設定すれば、ほとんどのキャリアの留守電よりも先にタイムアウトします。

TwiMLの前後に Pause を入れる

音声ガイダンスの前後に短いPauseを入れると、聞き取りやすくなります。

1
2
3
4
5
6
7
func voiceWithPause(elements ...twiml.Element) (string, error) {
    all := make([]twiml.Element, 0, len(elements)+2)
    all = append(all, &twiml.VoicePause{Length: "1"})
    all = append(all, elements...)
    all = append(all, &twiml.VoicePause{Length: "1"})
    return twiml.Voice(all)
}

電話が繋がった直後や切断直前は回線のノイズが入りやすいため、1秒のPauseがあるだけでガイダンスの頭切れを防げます。

Conference の Trim 設定に注意

1
Trim: "do-not-trim"

Twilioのデフォルトでは録音の先頭・末尾の無音がトリムされます。通常の用途では問題ありませんが、証跡として完全な録音が必要な場合は do-not-trim を明示的に指定してください。

HTTP クライアントのタイムアウト

録音ダウンロードなど、レスポンスに時間がかかるTwilio APIコールでは、GoのHTTPクライアントのタイムアウトをデフォルト(無制限)から適切な値に設定しましょう。

1
client := &http.Client{Timeout: 5 * time.Minute}

長時間通話の録音ファイルは数十MBになることもあるため、短すぎるタイムアウトではダウンロードが途中で切れます。


まとめ

GoとTwilioで通話システムを構築する際のポイントを振り返ります。

  • Conference ベースで構築すると、通話中アナウンス・録音・参加者イベント管理が柔軟にできる
  • twilio-goのtwimlパッケージを使えば、TwiMLを型安全に生成できる。用途別に関数を分離すると見通しがよい
  • VoIPとPSTNは発信方式(client: vs E.164)、AMD、プッシュ通知など差異があるため、それぞれの特性を理解して実装する
  • 録音ダウンロード時のSSRF対策はURLプレフィックス検証で行う
  • Webhookの署名検証X-Twilio-Signature)で偽リクエストを排除する
  • ステータスコールバックは必要なイベントだけ選択し、特にConferenceの join イベントはフロー制御に活用できる
  • 認証情報はシークレット管理サービスで管理し、コードにハードコードしない

twilio-go SDKはTwilioのREST APIをほぼ完全にカバーしており、Goからでも不自由なくTwilioの機能を活用できます。この記事がGoでの通話機能開発の参考になれば幸いです。

関連リンク