Twilio Verifyとは|SMS・音声認証の実装手順と日本向け料金
Twilio Verifyは、利用者の電話番号あてにワンタイムコードを送り、その入力をもってアカウントの持ち主を確かめるAPIです。コードの生成と保管、再送の制御、不正送信の検知までTwilio側が受け持つため、二要素認証の実装コストを大きく下げられます。一方で認証1件ごとに課金が発生し、有効期限のような主要な挙動は変更できません。ここではVerifyの守備範囲、Python実装、日本宛の料金内訳、Webhookの署名検証までを公式ドキュメントの数値とともに整理します。
まとめ
Twilio Verifyは電話番号の到達性を確かめるサービスで、声を照合する生体認証ではありません。channelパラメータに指定できるのはsms、call、email、whatsapp、sna、autoの6つで、声紋を扱うチャネルは用意されていません。
料金は認証が成功した場合に$0.05、加えてチャネルごとの料金がかかります。日本宛SMSはlong codeで$0.0890のため、1回で成功したときの実費は1件あたりおよそ$0.139。音声チャネルは公式の料金表で「検証成功ごとに$0.05」とだけ記載されています。検証コードの有効期限は10分で固定、コード長は既定4桁・4〜10桁の範囲で変更できます。
実装で見落とされやすいのが、着信通話やステータス通知を受けるWebhook側の署名検証です。X-Twilio-SignatureヘッダーをAuthTokenで検証しないと、認証フローの入口を誰でも叩ける状態になります。以下、それぞれの根拠と実装を順に説明します。
Twilio Verifyが担う本人確認の範囲
電話番号の到達性確認と声紋照合の区別
「音声認証」という語は二つの異なる技術を指します。ひとつは話者の声の特徴量を登録済みデータと照合する声紋認証、もうひとつは音声通話でワンタイムコードを読み上げて入力させる電話認証です。Twilio Verifyが提供するのは後者だけです。
根拠は公式のVerification APIリファレンスにあります。channelに指定できる値はsms、call、email、whatsapp、sna、autoの6種で、生体情報を受け取るチャネルは定義されていません。callチャネルは合成音声でコードを読み上げるもので、利用者の声を録音して照合する機能ではありません。声紋認証が要件なら、Twilioとは別に話者認識のエンジンを調達することになります。
Verifyが実際に保証するのは「その電話番号にコードが届き、それを入力できる人物である」という一点です。SIMスワップや転送設定を突破された場合は成立しません。金融取引の承認のように被害額が大きい操作では、Verifyだけに依存せずデバイス側の要素と組み合わせる設計が必要になります。Twilioの音声データそのものの扱いについてはTwilio音声データとAI+API連携の可能性を解き明かすで別途取り上げています。
チャネル6種の選択基準
チャネル選択は、到達率、単価、利用者の環境の3点で決まります。SMSは最も広く使われますが、日本では格安SIMの一部やデータ専用回線で受信できません。callは固定電話しか持たない利用者や、SMSが届かなかったときの再試行手段として機能します。Verify ServiceのSkipSmsToLandlinesを有効にすると、固定電話番号へのSMS送信を自動的に見送れます(後述のとおりlookup_enabledとの併用が前提です)。
sna(Silent Network Auth)はモバイル回線の情報を使ってコード入力なしで認証する方式で、利用者の操作が不要なぶん離脱を減らせますが、対応キャリアに依存します。autoはTwilio側がチャネルを選びます。
混同されやすいのがRCSとTOTP、Pushの扱いです。RCSはchannelの選択肢ではなく、公式ドキュメントに「SMSチャネルの自動アップグレード」と説明されているとおり、smsを指定したときにTwilioが可能な範囲でRCS配信へ切り替える動作を指します。認証アプリで6桁コードを生成するTOTPと、公開鍵暗号を使うPushは、channelパラメータの値ではなくVerify内の別APIです。「7つのチャネルから選ぶ」と書かれた解説を見かけますが、APIリファレンス上の選択肢は6つです。
Verify APIの実装手順
Verify Serviceの作成とコード長の設定
Twilioアカウントを作成しコンソールでAccount SIDとAuth Tokenを取得したら、最初に行うのがVerify Serviceの作成です。Serviceは認証の設定単位で、ここで決めたコード長やテンプレートが以降のすべての検証に適用されます。SMS送信用の電話番号を自分で購入する必要はありません。friendly_nameは32文字以内、code_lengthは既定が4で、4から10の範囲で指定できます。4桁は総当たりに弱いため、6桁以上を選んでください。
import os
from twilio.rest import Client
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
service = client.verify.v2.services.create(
friendly_name="MyApp Login",
code_length=6,
do_not_share_warning_enabled=True,
lookup_enabled=True, # skip_sms_to_landlines の前提条件
skip_sms_to_landlines=True,
)
print(service.sid) # VA で始まる Service SID
do_not_share_warning_enabledを有効にすると、SMS本文に「このコードを他人に教えないでください」という警告文が追加されます。フィッシングでコードを聞き出す手口への基本的な防御になるため、既定で入れておきたい設定です。
skip_sms_to_landlinesは単独では機能しません。公式のService APIリファレンスに「Requires lookup_enabled」と明記されており、両方を有効にして初めて固定電話へのSMS送信が抑止されます。lookup_enabledは検証を開始するたびに電話番号のLookupを実行する設定なので、有効化する前にLookupの課金条件を料金ページで確認してください。
名前空間がclient.verify.v2.servicesである点にも注意が必要です。client.verify.servicesと書いている古い記事が残っていますが、現行のtwilio-python 9.10.9で実行すると「services is deprecated. Use v2.services instead.」という警告が出ます。動作はするものの非推奨の書き方です。
認証コードの送信と確認のPython実装
認証は、コードを送るverificationsと、入力されたコードを照合するverification_checksの2回のAPI呼び出しで完結します。コードそのものをアプリケーション側で生成・保持する必要はありません。
# 前掲の client をそのまま使う
SERVICE_SID = os.environ["TWILIO_VERIFY_SERVICE_SID"]
# 1. コードを送信する
verification = client.verify.v2.services(SERVICE_SID).verifications.create(
to="+819012345678", channel="sms"
)
print(verification.status) # pending
# 2. 利用者が入力したコードを照合する
check = client.verify.v2.services(SERVICE_SID).verification_checks.create(
to="+819012345678", code="123456"
)
print(check.status) # approved なら本人確認成立
電話番号はE.164形式で渡します。日本の番号なら先頭の0を外し+81を付けた+819012345678の形です。国内向けの表記である090-1234-5678のまま送るとエラーになるため、入力欄で受け取った番号は保存前に正規化しておきます。
有効期限10分と失敗ステータスの扱い
検証コードの有効期限は10分です。この値はService側の設定項目として公開されておらず、短くも長くもできません。「セッションの有効期限に合わせて30分にしたい」といった要件がある場合は、Verifyの外側でアプリケーション側のセッション管理と突き合わせる形になります。
statusが返す値はpending、approved、canceled、max_attempts_reached、deleted、failed、expiredです。実装で分岐が必要なのは、コード誤りが続いたmax_attempts_reachedと期限切れのexpiredで、いずれもVerification SIDが破棄されるため、利用者には新しいコードの再送を促す導線が要ります。ここをapproved以外まとめて「認証に失敗しました」と表示すると、期限切れなのか番号違いなのか利用者が判断できず、問い合わせが増える箇所です。
日本向けの料金内訳とSMS・音声のコスト差
成功課金$0.05とチャネル料金の内訳
Verifyの課金は二階建てです。公式の料金ページは各チャネルの料金を「認証成功ごとに$0.05 + チャネルごとの料金」という形で示しており、認証が成功しなければ$0.05は発生しません。
| チャネル | 認証成功ごと | チャネル料金(米国) |
|---|---|---|
| SMS | $0.05 | $0.0083 / 通 |
| $0.05 | $0.0034 / 通 | |
| Voice | $0.05 | 料金ページに記載なし |
| $0.05 | 料金ページに記載なし | |
| Silent Network Auth | $0.05 | 国ごとに異なる |
チャネルではありませんが、TOTPとPushはチャネル料金が認証料金に含まれると記載されています。注意すべきは、SMSの$0.0083が米国宛の単価である点です。日本宛は国際SMS料金が別に適用されるため、この数字をそのまま予算に使うと実費と大きくずれます。
日本宛SMSと音声通話の単価比較
日本宛のSMS料金は、short codeが$0.0800、long codeが$0.0890、alphanumeric sender IDが$0.089です。ただしこの3つは選べません。TwilioのJapan SMS Guidelinesでは、short codeは非対応、alphanumeric sender IDは国際経由が非対応で国内ゲートウェイのみ(事前登録が必須で所要5週間)、long codeは国内非対応・国際対応と定められています。通常の国際ゲートウェイ経由なら、実質的にlong codeの$0.0890が唯一の選択肢です。
課金はメッセージ単位ではなく1セグメント単位で、日本語を含む本文は70文字を超えると2セグメントになります。認証コードの通知程度なら1セグメントに収まりますが、案内文を長くすると単価が倍になります。なおsms指定時にRCSへ自動アップグレードされて届いた場合も、公式ドキュメントによればSMSとして課金されるため、この見積もりは変わりません。
1回で成功した場合の実費は、成功課金$0.05にSMS $0.0890を足しておよそ$0.139。月1万件の認証成功で約$1,390という水準です。
音声はどうか。料金ページの音声チャネル欄には「検証成功ごとに$0.05」とあるだけで、通話料の記載がありません。日本向けの音声料金は別の表で定義されており、固定電話・VoIP宛が$0.0746/分、携帯宛が$0.1850/分、0570番号が$0.1200/分です。通話料が別途乗る前提で携帯宛を見積もれば1件あたり$0.235前後、乗らなければ$0.05となり、両者の差は5倍近くになります。この一点で総額が変わるため、音声チャネルを主軸に据えるなら少量で試験送信し、請求明細の内訳で確定させてください。
読み取れる範囲でも判断材料にはなります。日本宛SMSは$0.139が確定値で、音声は最悪でも$0.235前後。ボリュームが小さいうちは差は月数十ドルにとどまるため、単価より到達率(SMSが届かない回線の存在)で選ぶほうが合理的です。
もっとも、総額を左右するのは単価より失敗の回数です。番号の入力ミスで再送が2回発生すれば、成功課金は1件でもSMS料金は3通ぶん請求されます。入力欄でのE.164正規化と再送ボタンのクールダウン設定は、そのまま通信費の削減になると考えてください。CPaaS各社の料金体系の違いはビジネスに最適なCPaaSはどれ?VonageとTwilioを徹底比較で整理しています。
Webhookの署名検証(X-Twilio-Signature)の実装
署名文字列の生成規則とJSONボディの扱い
Verifyのコード送信自体はWebhookを必要としませんが、音声チャネルの着信処理やメッセージのステータス通知を受け取るなら、公開エンドポイントを用意することになります。このエンドポイントを保護するのがX-Twilio-Signatureヘッダーの検証です。
署名は、AuthTokenを鍵としたHMAC-SHA1で作られます。対象の文字列は、スキームからクエリ文字列までを含む完全なURLに、POSTパラメータを名前の昇順に並べて「名前と値を区切りなしで連結」したものを足したもので、その結果をBase64エンコードしたものが署名値です。並び順はUnix形式の大文字小文字を区別するソートなので、Toとtoは別のキーになります。Content-Typeがapplication/jsonの場合はPOSTパラメータの代わりに、本文のSHA-256を16進表記したbodySHA256がクエリパラメータとしてURLに付与されます。
公式ドキュメントにはAuthTokenと期待される署名値を含む計算例が掲載されているため、自前実装の検算はその値を再現できるかで行えます。ただし実装ミスがそのまま認証バイパスになる箇所なので、特段の理由がなければヘルパーライブラリのRequestValidatorに任せてください。
Flaskでの検証とプロキシ配下での落とし穴
import os
from functools import wraps
from urllib.parse import urlsplit, urlunsplit
from flask import Flask, request, abort
from twilio.request_validator import RequestValidator
app = Flask(__name__)
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
def validate_twilio(f):
@wraps(f)
def wrapper(*args, **kwargs):
signature = request.headers.get("X-Twilio-Signature", "")
proto = request.headers.get("X-Forwarded-Proto", request.scheme)
url = urlunsplit((proto,) + tuple(urlsplit(request.url))[1:])
# JSON は本文そのものを渡す。dict を渡すと bodySHA256 が照合されない
if request.is_json:
params = request.get_data(as_text=True)
else:
params = request.form
if not validator.validate(url, params, signature):
abort(403)
return f(*args, **kwargs)
return wrapper
@app.route("/voice", methods=["POST"])
@validate_twilio
def voice():
from twilio.twiml.voice_response import VoiceResponse
response = VoiceResponse()
response.say("お電話ありがとうございます", language="ja-JP")
return str(response), 200, {"Content-Type": "text/xml"}
この実装で最も踏みやすい失敗が、リバースプロキシやロードバランサ配下での署名不一致です。TwilioはHTTPSのURLで署名を計算しますが、TLS終端より内側のアプリケーションにはhttp://としてリクエストが届くため、request.urlをそのまま渡すと署名が必ず食い違います。上のコードでX-Forwarded-Protoを見てスキームを組み直しているのはこのためです。ngrokやCloud Runなど、TLSを手前で終端する構成では原則として該当します(FlaskのProxyFixを挟んでいる場合はrequest.url側が既にhttpsになるため、二重に補正しないよう確認してください)。
もうひとつは、コンソールに登録したWebhook URLと実際のリクエストURLの差異です。末尾のスラッシュの有無やクエリ文字列が1文字でも違えば署名は一致しません。403が返るときは、まず受信側でTwilioが送ってきたURLをログに出して突き合わせてください。Webhookという仕組み自体の前提はIncoming Webhookとは?仕組みとSlack・Discordでの使い方を解説で解説しています。
SMS Pumping対策としてのFraud Guard
認証SMSを狙った代表的な不正がSMS Pumpingです。攻撃者が高額な着信料を得られる番号帯を大量に登録フォームへ投入し、認証SMSを送らせて通信費を吸い上げます。被害はセキュリティ侵害ではなく請求額として現れるため、月次の明細を見るまで気づきにくいのが厄介な点です。
Twilioはこれに対しFraud Guardを用意しています。公式ドキュメントの記載は「Fraud Guard is on by default for all Verify customers」で、申し込みなしに既定で有効です。判定材料は、SMS Pumpingが確認されている国や地域あての検証、そのアカウントがこれまで一度もSMSを送ったことのない国あての検証、人間の操作とは考えにくい特徴を持つ検証といった観点で、これらを既知の不正パターンと突き合わせます。ブロックした送信は節約額としてコンソールに集計されます。防御の強度はBasic、Standard、Maxの3段階から選べ、既定はStandardです。
重要な制約が一つあります。公式ドキュメントは「Fraud Guard protects only the SMS channel. The Voice channel is not supported」と明記しており、音声チャネルは保護対象外です。前章のコスト比較で音声を選ぶ判断をする場合、単価だけでなくこの防御差も勘定に入れてください。音声を使うなら、送信可能な国を絞るGeo Permissionsの設定と、アプリケーション側での試行回数制限が代替の防波堤になります。
Verifyを使わずMessaging APIで認証コードを自作する場合、この防御はすべて自前で用意することになります。海外展開の予定がないサービスなら、Geo Permissionsで送信先を日本だけに制限するのが最も確実な対策です。
Verifyを採用すべきでない条件
Verifyは実装工数を下げますが、認証1件ごとの課金と固定された仕様を引き受ける契約でもあります。次の3つに当てはまるなら、Messaging APIでの自作や他の認証方式を検討してください。
第一に、TOTPだけが要件のケースです。認証アプリで6桁コードを生成する方式は仕様が公開されており、Pythonならpyotpのようなライブラリで完結します。シークレットの保管さえ自前で設計できるなら、認証成功ごとの$0.05を払う理由は薄くなります。
第二に、認証件数が月10万件規模を超え、社内にSMS配信の運用ノウハウがあるケースです。成功課金だけで月$5,000に達するため、コード生成・レート制限・不正検知を内製する開発コストと比較する価値が出てきます。逆にこの規模に届かないうちは、Fraud Guardと再送制御を自作するほうが割高です。
第三に、有効期限や再送間隔をサービス固有の要件に合わせて細かく制御したいケースです。前述のとおり検証コードの有効期限は10分固定で、Serviceの設定項目として公開されていません。規程で期限が定められている業務システムでは、この1点が採用の可否を分けます。
なお、通話品質そのものに要件がある音声チャネル中心の構成では、事前診断のしくみもあわせて検討してください。Twilio Preflightとは何か?高品質通話を支援する事前診断ツールの全貌から役割まで徹底解説で扱っています。
よくある質問
Twilio Verifyとは何ですか?
利用者の電話番号やメールアドレスあてにワンタイムコードを送り、その入力を照合して本人確認を行うAPIです。コードの生成、保管、有効期限の管理、不正送信の検知をTwilio側が担うため、アプリケーションはコードを送るAPIと照合するAPIの2つを呼ぶだけで二要素認証を組み込めます。channelとして指定できるのはSMS、音声通話、メール、WhatsApp、Silent Network Auth、自動選択の6つです。
Twilio Verifyの料金はいくらですか?
認証が成功するごとに$0.05、これにチャネルごとの料金が加算されます。SMSのチャネル料金は米国宛で$0.0083ですが、日本宛は国際SMS料金が適用されlong codeで$0.0890です。したがって日本でSMS認証を行う場合、1回で成功したときの実費は1件あたりおよそ$0.139になります。再送が発生すると成功課金は1件のままSMS料金だけが積み上がります。音声チャネルは料金ページ上「検証成功ごとに$0.05」と記載されているだけで通話料の扱いが読み取れないため、実額は試験送信と請求明細で確認してください。
Twilio Verifyは声紋による音声認証に使えますか?
使えません。Verifyのchannelに指定できる値はsms、call、email、whatsapp、sna、autoの6つで、声の特徴量を照合するチャネルは公式リファレンス上に定義がないためです。callチャネルが担うのは合成音声による認証コードの読み上げまでで、話者本人の声を判定する処理は含みません。声紋認証が要件なら、話者認識に対応した別のサービスを組み合わせることになります。
認証コードの有効期限は変更できますか?
できません。検証コードの有効期限は10分で、Verify Serviceの設定項目としては公開されていません。変更できるのはコード長で、既定の4桁から4〜10桁の範囲で指定できます。期限が切れるとステータスがexpiredになりVerification SIDが破棄されるため、利用者にはコードの再送を促す導線を用意してください。
Webhookの署名検証で403が返るのはなぜですか?
最も多い原因は、リバースプロキシ配下でリクエストURLのスキームがhttpになっていることです。TwilioはHTTPSのURLで署名を計算するため、X-Forwarded-Protoを見てURLを組み直す必要があります。次に多いのが、コンソールに登録したURLと実際のURLの不一致で、末尾のスラッシュやクエリ文字列が1文字でも異なると署名は一致しません。受信したURLをログ出力して突き合わせるのが最短の切り分けです。