キュー済み返信を守るWhatsApp 24時間送信ガードの実装
キューに入ったWhatsApp返信は、承認待ち、再試行、workerの取得待ちの間に24時間ウィンドウを越えることがあります。安全な送信ガードは、実際の送信直前にサーバー状態を再読込し、検証済みの最新ユーザーメッセージから期限を計算し、期限切れの自由文を止めます。カテゴリーと料金はサービスメッセージとユーティリティメッセージの判定表で確認し、ここでは誤送信を防ぐ実装境界に集中します。
要点
- 2つの期限と単調増加する
state_versionを保存し、真偽値だけにしないでください。 - 署名検証と重複排除を通ったユーザー受信メッセージだけが24時間ウィンドウをリセットします。
- workerはジョブ取得後、同じトランザクションまたはロック内で状態を再読込します。
- 期限切れの下書きは停止して再ルーティングし、黙ってテンプレートへ変換しません。
- 重複、順序逆転、worker競合、秒単位の境界を固定時計でテストします。
キュー済み返信のためのWhatsApp送信ガードを構築する
WhatsApp公式のBusiness Platform料金ページによると、ユーザーメッセージが24時間ウィンドウを開くかリセットします。実装では、この許可境界だけを最終送信条件にします。2026年10月のカテゴリーと料金変更は既存の判定ガイドで扱い、ここでは繰り返しません。
送信器はキュー内の各ジョブについて、次の3点を確認します。
- 最新の状態バージョンを読んでいるか。 古いworkerが更新後の期限を上書きしてはいけません。
- 実送信時にもウィンドウが開いているか。 下書き作成時や承認時の判定は再利用できません。
- 選択した経路がまだ許可されているか。 閉じた後は意図に合う承認済みテンプレートだけを使い、それ以外は停止して人へ戻します。
1つの状態ではなく2つの時計を保存する
再試行、遅延ジョブ、担当者の引き継ぎでも再計算できるよう、明示的な期限を保存します。
| 保存する値 | 開始・リセット条件 | 制御すること |
|---|---|---|
service_window_expires_at | ユーザーからの各受信メッセージ | 非テンプレートのサービス返信を許可するか |
free_entry_expires_at | 対象となるClick-to-WhatsApp広告またはFacebookページのCTAからの流入 | 72時間の配信料金免除が有効か |
last_user_message_id | 受理した各ユーザーメッセージ | 冪等性と監査の根拠 |
state_version | 状態を変える受信イベント | キュー送信処理が新しい状態を上書きしないための版番号 |
72時間の無料エントリーポイントは、72時間の送信許可ウィンドウではありません。無料期間内に新しいユーザーメッセージが届けば24時間の時計はリセットされますが、無料期間の期限は独立したままです。
送信ガードを実装する
次のTypeScriptはアプリケーション内の例であり、Metaのpayloadスキーマではありません。送信許可と予想課金を分けて返します。
type WindowState = {
serviceWindowExpiresAt: Date | null;
freeEntryExpiresAt: Date | null;
};
function evaluateWhatsAppSend(state: WindowState, now: Date) {
const serviceWindowOpen =
state.serviceWindowExpiresAt !== null && now < state.serviceWindowExpiresAt;
const freeEntryActive =
state.freeEntryExpiresAt !== null && now < state.freeEntryExpiresAt;
return {
maySendNonTemplate: serviceWindowOpen,
expectedDeliveryCharge: serviceWindowOpen && !freeEntryActive,
requiredPath: serviceWindowOpen ? "service" : "approved_template",
} as const;
}
これはUI表示ではなく、最終的な送信ガードとして使います。10時に作成した返信が承認キューに残り、期限後に実行される場合があるからです。ジョブを取得した時点で最新状態を読み直し、次のいずれかを選びます。
- ウィンドウが開いていれば、非テンプレートのサービス返信を送る。
- 閉じていれば、意図に合う承認済みテンプレートへ切り替える。
- どちらも有効でなければ送信を止め、人の担当者へ戻す。
自由文を黙ってテンプレートへ変換したり、テンプレートに合わせて切り詰めたりしないでください。カテゴリー、変数、審査はそれぞれ別の契約です。
誤ったイベントでウィンドウを延長しない
ウィンドウをリセットできるのは、ユーザーが送ったメッセージだけです。配信確認、ステータス更新、担当者の下書き、内部メモ、再試行、企業側メッセージのechoでは延長しません。
処理順は次のようにします。
- providerのwebhookを検証してから状態を変更する。
- 安定したメッセージIDまたはイベントIDで重複を排除する。
- ユーザー送信の受信メッセージであり、receiptやechoではないことを確認する。
service_window_expires_atを受理時刻の24時間後に設定する。- 条件を満たすreferralだけに72時間の無料期限を設定する。
state_versionを増やし、元イベントを監査用に保存する。- 実際の送信直前に2つの時計を再評価する。
古い期限へ24時間を足し続けてはいけません。ユーザーが09:00と12:00に送った場合、新しい期限は翌日の12:00です。
キュー済み返信とイベント順序の境界をテストする
固定時計を使い、1秒の境界も再現可能にします。
| シナリオ | 期待結果 |
|---|---|
| 10:00に受信し、翌日09:59:59に返信 | 非テンプレート返信を許可 |
| 同じ受信に翌日ちょうど10:00に返信 | ウィンドウは閉じたと判定 |
| 翌日09:50にユーザーが再送信 | 期限はその翌日09:50へリセット |
| 期限前に承認、期限後にジョブ取得 | 非テンプレート送信を止めて再ルーティング |
| 同じユーザーメッセージを2回受信 | 重複排除し、状態遷移は1回だけ |
| 古いユーザーメッセージが新しいものより後に到着 | 新しい期限を維持し、状態を巻き戻さない |
| 2つのworkerが同じ返信を同時取得 | 1つだけがバージョン検査を通って送信 |
本番では予想結果を公式のステータスと料金記録に照合します。サービスメッセージ料金の計測ガイドは、配信数と市場別レートを分離する方法を説明しています。Meta Business Agentと独自AIの選択は、料金と構成の比較で判断してください。
UnifyPortが適合する場所
上のステートマシンは公式WhatsApp Business Platform用です。UnifyPortの非公式インターフェースは、通常のメッセージングアカウント向けの別経路であり、MetaのカスタマーサービスウィンドウやPricing Analyticsの状態を提供しません。
この別経路では、WhatsApp受信メッセージが正規化された message.received イベントとして届きます。webhook endpointに signing_secret がある場合、raw bodyに対する X-Device-Timestamp と X-Device-Signature を検証してから処理します。標準の対応返信は POST /v1/messages を使います。詳細はwebhook配信と署名リファレンスとmessage.receivedイベントリファレンスにあります。
UnifyPortイベントを公式Cloud APIの課金状態へ入れ、Metaが分類したと扱ってはいけません。両方を運用する場合は、transport または control_plane を保存し、台帳を分けます。
制限とトレードオフ
承認済みテンプレート、キャンペーン、Click-to-WhatsAppのアトリビューション、Meta公式分析、BSPのサポートが必要なら公式Platformが適しています。公式配信の課金結果も公式記録が正です。
非公式インターフェースはテンプレートを承認せず、Metaのウィンドウを延長せず、Pricing Analyticsを提供せず、WhatsAppのポリシーを変更しません。通常のメッセージングアカウントを接続し、複数プラットフォームのイベントを統一する別経路です。どちらの経路にも冪等性、同時実行制御、監査ログが必要です。
料金とルールには時効性があります。10月1日の切り替え前に公式資料を再確認し、計画用レートを状態遷移へ固定しないでください。
FAQ
送信ガードはどのタイムスタンプを使いますか?
検証後に受理した最新ユーザーメッセージの時刻です。webhook受信時刻、ジョブ作成時刻、管理画面のカウントダウンは使いません。
順序が逆のwebhookでウィンドウが短くなりますか?
短くしないでください。安定したイベントIDで重複排除し、受信時刻が現在の記録より新しい場合だけ期限と state_version を更新します。
キュー済み返信が送信前に期限切れになったら?
自由文の送信を止めて再ルーティングします。意図と承認済みテンプレートが一致する場合だけテンプレート経路を使い、それ以外は人へ戻します。
期限切れの自由文を自動でテンプレートにできますか?
黙って変換してはいけません。テンプレートのカテゴリー、変数、承認は別の契約であり、明示的な選択と検証が必要です。
重複イベントによる二重送信を防ぐには?
メッセージまたはイベントIDを永続化し、workerがトランザクションまたはロック内で冪等キーと state_version を検査してから送信します。
次のステップ
まず受信境界を実装してください。webhook配信と署名リファレンスを確認し、重複または順序が入れ替わったイベントで送信ウィンドウが誤って延長されないことをテストします。
情報源
公式情報の確認日:2026年7月27日。