
Twilio × AWS サーバレス構成によるリマインド通知基盤の実践:SMS・音声通話の既読/応答判定と運用設計ガイド
概要
ユーザーへのリマインド通知(リマインダー送信)は、日常業務やサービス運営で非常にニーズが高い機能です。しかし、「相手が本当にメッセージを見たか」 「電話に出たかどうか」を自動で検知・再送制御したい場合、実現方法は通信手段によって大きく異なります。
本記事では、Twilio を使ったリマインド通知の実装において、「SMS」 「音声通話(電話)」それぞれで開封・応答状況をどこまで判定できるのか、またその際のベストプラクティスや API 活用例について、現場のユースケースに即して解説します。
対象読者
- 業務システムや Web サービスに「リマインド通知」を組み込みたいエンジニア
- 顧客への重要連絡(支払い催促、予約確認、緊急通知等)を確実に届けたいプロダクト担当者
- Twilio API(SMS / Voice)の運用・実装経験があり、通知の「未読・未応答」検出ロジックに悩んでいる方
- 法人や自治体など、通知の開封/応答率を KPI 管理したい方
想定ユースケース
- リマインダー(例:医療/美容予約・請求・納期通知)を送信し、「未読者のみ再送」したい
- SMS 経由で「開封確認」を自動化したいが、LINE 等の既読機能が使えない
- 自動音声通話(IVR)で「誰が出たか・出なかったか」に応じた再発信処理をしたい
- セキュリティ通知(例:ワンタイムパスワード)などで「未着・未読」を検知したい
SMS の「既読・未読」判定はできるのか?
結論:Twilio × AWSサーバレスで「返信=既読」を実現できる
SMS には「既読確認」や「開封通知」のAPIはありません。そのため「既読/未読の自動判定」や「未読者だけ再送」 といった業務的な制御はアプリケーション側で実現する必要があります。 本記事ではその方法として、Twilio Webhook の着信を AWS のサーバレス(API Gateway+Lambda+DynamoDB)で受け、メッセージの既読/未読判定や再送判定をサーバ管理なしで自動化・永続化する実践例を紹介します。
この構成なら、インフラ管理不要で低コストかつスケーラブルに既読データを蓄積できるため、業務で使いやすい「未読者への自動再送」や「開封率の集計」などにも即応できます。
ここでは Twilio Webhook+API Gateway+Lambda+DynamoDB の最小構成で、リマインド SMS の既読判定&記録を検証する方法を紹介します。
1. システム全体の流れ
[ユーザー]
↑ SMS返信
[Twilio]
──(Webhook/HTTP POST)→ [API Gateway] → [Lambda] → [DynamoDB]
- Twilio:SMS の返信を Webhook(HTTP POST)で API Gateway へ転送
- API Gateway:外部からの POST リクエストを Lambda に中継
- Lambda:Webhook の内容をパースし、「既読」かどうか判定
- DynamoDB:既読(返信済み)・未読(返信なし)を記録
2. 必要な事前準備
-
TwilioのSMS送受信が可能な電話番号(管理画面で取得・確認)
-
AWSアカウント
- API Gateway(REST API / HTTP API どちらでもOK)
- Lambda関数(Node.js / Python 等)
- DynamoDBテーブル(スケーラビリティ・運用コスト・サーバレス性を重視し採用。RDB 等他の DB でも構いませんが、検証・PoC には最小構成の DynamoDB が最適です)
3. 構築&検証手順
3.1 DynamoDB テーブル作成
AWS マネジメントコンソールから DynamoDB を開き、テーブルを作成してください。
- テーブル名:
RemindStatus
- パーティションキー:
phoneNumber
(文字列型。例:+819012345678 など E.164 形式推奨)
※テーブル作成時は「ソートキー(range key)」は不要です。
3.2 Lambda関数を用意
Node.js 22.x での例です。まず、ローカル環境で node 環境を作成します。
# プロジェクト用ディレクトリを作成
mkdir reminder-webhook
cd reminder-webhook
# npm 初期化
npm init -y
# AWS SDK のインストール
npm install aws-sdk
次に、 index.js
を作成します。
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();
const qs = require('querystring');
exports.handler = async (event) => {
// Twilioからは x-www-form-urlencoded 形式で届く
const body = qs.parse(event.body);
const from = body.From;
const msg = body.Body;
// 「はい」と返信されたら既読
let statusUpdate;
if (msg && msg.trim().toLowerCase() === 'はい') {
statusUpdate = {
UpdateExpression: 'SET readAt = :now, latestReply = :msg, #st = :s',
ExpressionAttributeNames: {
'#st': 'status'
},
ExpressionAttributeValues: {
':now': new Date().toISOString(),
':msg': msg,
':s': 'read'
}
};
} else {
statusUpdate = {
UpdateExpression: 'SET latestReply = :msg',
ExpressionAttributeValues: { ':msg': msg }
};
}
await dynamo.update({
TableName: 'RemindStatus',
Key: { phoneNumber: from },
...statusUpdate
}).promise();
// TwilioへXMLで空レスポンス
return {
statusCode: 200,
headers: { "Content-Type": "text/xml" },
body: "<Response></Response>"
};
};
zip ファイルにまとめ、 Lambda コンソールにアップロードできる形にします。
zip -r function.zip .
Lambda関数の作成画面で「.zip ファイルをアップロード」オプションを選択し、function.zip をアップロードしてください。
次に、Lambda 関数の環境変数で DynamoDB 権限を付与します。Lambda関数の「設定 > アクセス権限 > 実行ロール」から、関連付けられているIAMロールを開きます。以下のようなカスタムポリシーをアタッチします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:ap-northeast-1:YOUR_ACCOUNT_ID:table/RemindStatus"
}
]
}
補足: Lambda コンソールを用いたテスト方法について
詳細はこちら
Lambda 関数が正しく動作するかどうかは、AWS コンソールの「テスト」タブを使って、Twilio から届く Webhook イベントを模擬して検証できます。
- Lambda 管理画面で関数を選択し、「テスト」タブを開く
- 新しいテストイベントを作成
- 「テスト」タブで「テストイベントを作成」ボタンをクリック
- 「イベント名」を任意で入力(例:
TwilioReply
)
- テスト用イベント JSON を下記のように入力
Twilio Webhook はx-www-form-urlencoded
形式ですが、API Gateway 経由ではevent.body
に文字列として格納されます。そのため、下記のような JSON で body を模擬します:{ "body": "From=%2B819012345678&Body=%E3%81%AF%E3%81%84" }
From=%2B819012345678
:送信元番号(+819012345678 の URL エンコード)Body=%E3%81%AF%E3%81%84
:本文( 「はい」の URL エンコード)(参考:「はい」は UTF-8 で%E3%81%AF%E3%81%84
となります)
- テストイベントを保存し、「テスト」ボタンを実行
- 正常に完了すれば、DynamoDB テーブル「RemindStatus」 で、該当
phoneNumber
のreadAt
、latestReply
、status
が更新されていることを確認できます。 - DynamoDBの管理画面から、該当レコードを検索し、値が更新されているかチェックしてください。
- 正常に完了すれば、DynamoDB テーブル「RemindStatus」 で、該当
3.3 API Gateway を設定
- REST API(またはHTTP API)で POST エンドポイントを作成
- Lambda 統合で上記関数を割り当て
- デプロイして URL を発行(例:
https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/default/reminder-webhook
)
補足: curl を用いたテスト方法について
詳細はこちら
「はい」と返信した場合のサンプルです。
curl -X POST \
https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/webhook \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "From=%2B819012345678&Body=%E3%81%AF%E3%81%84"
- 正常な場合
statusCode: 200
と<Response></Response>
が返ります - DynamoDB
テーブル「RemindStatus」に該当のphoneNumber
レコードが新規追加・更新されているか確認します
3.4 Twilio 管理画面の Webhook 設定
電話番号設定画面で 「A MESSAGE COMES IN」 の Webhook URL 欄に、上記 API Gateway のエンドポイントを貼り付けます。
下記の手順で動作を検証します。
- サービス側から対象ユーザーにリマインド SMS を送信(本文に「このSMSを読んだら『はい』と返信してください」等を記載)
サンプルコード
const twilio = require('twilio'); require('dotenv').config(); const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const messagingServiceSid = process.env.MESSAGING_SERVICE_SID; const client = twilio(accountSid, authToken); // 送信先電話番号(E.164形式、例:+819012345678) const to = '+819012345678'; // 送信する本文 const body = 'このSMSを読んだら「はい」と返信してください。'; client.messages .create({ to, body, messagingServiceSid }) .then(message => { console.log(`Message SID: ${message.sid}`); console.log('送信完了'); }) .catch(error => { console.error('送信失敗:', error); });
- ユーザーが返信
- Twilio → API Gateway → Lambda → DynamoDB で既読記録
- DynamoDB コンソールで
phoneNumber
ごとにreadAt
やstatus
が更新されているか確認
音声通話(Voice)の「応答状況」はどこまで判定できるか?
結論:Twilio Voice API なら応答状況を API 経由で詳細に取得できる
音声通話(自動発信・IVR)では、Twilio Voice API が 「誰が電話に出たか」 「留守電だったか」 「応答しなかったか」などの発信先情報をプログラムで取得可能です。これにより、「電話に出ていない相手だけ再発信」 「人間以外(留守電)への通話は除外」など、より精度の高いリマインド運用が期待できます。
1. 判定できる項目例
Twilio の「Call Resource」 API で以下の情報が取得できます。
項目 | 意味 | 運用ポイント例 |
---|---|---|
status | completed / no-answer / busy等 | 「no-answer」で再送判定 |
answeredBy | human / machine_start / unknown等 | 「human」のみ応答済とみなす |
duration | 通話時間(秒) | 短すぎる場合は再送検討 |
2. サンプルコード(Node.js)
発信後に記録される「Call SID」をパラメータとして発信した結果の情報を取得できます。
const twilio = require('twilio');
require('dotenv').config();
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = twilio(accountSid, authToken);
// Call SID(発信後、callback等で記録しておく)
const callSid = 'CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
client.calls(callSid)
.fetch()
.then(call => {
console.log(`Status: ${call.status}`);
console.log(`Answered by: ${call.answeredBy}`);
console.log(`Duration: ${call.duration}s`);
// 必要なロジック(例: answeredBy !== 'human' の場合に再発信など)
})
.catch(error => {
console.error('取得失敗:', error);
});
3. 運用のヒント
- 通話ごとに応答状況を記録し、「未応答」や「留守電」だけ再発信するロジックを構築可能
- DynamoDB や RDS に「通話結果」もあわせて記録すれば、SMS と同じく開封・応答状況を一元管理できる
- 留守電ガイダンス対策や、短時間だけつながった場合の運用も柔軟にカスタマイズ可能
まとめ:Twilio × AWS でリマインド通知運用を最大化しよう
-
SMS は「返信=既読」だけ自動判定可能
→ Twilio Webhook+API Gateway+Lambda+DynamoDB のサーバレス最小構成が便利 -
音声通話は「応答状況」まで API で取得可能
→ Voice API で「未応答」 「留守電」判定を取り込み、柔軟な再送や通知精度アップを実現 -
どちらも運用コストを最小化しつつ、業務要件に応じた「本当に見た/聞いた」人だけに自動再送ができる
→ KPI 管理、開封率・応答率向上にも寄与
運用・発展例
- 未読者・未応答者一覧の自動出力や、通知履歴のダッシュボード化も DynamoDB+Lambda で実現可能
- Twilio Functions や SaaS 連携を併用すれば、さらにノーコード寄りの小規模構成も実現できる
- セキュリティ要件(IP 制限、署名検証など)にも本番環境では十分に配慮を
参考・関連ドキュメント
おわりに
Twilio と AWS のサーバレス構成を組み合わせることで、「通知を送っただけ」から 「誰に届き、誰が読んだ/応答したかを可視化・自動集計できる通知基盤」 が、最小限の構成とコストで実現できます。自社サービスの通知業務に「本当に見た人」だけへの再送や、KPI 管理を導入したい方に特におすすめです。