為佇列回覆實作 WhatsApp 24 小時發送護欄
佇列中的 WhatsApp 回覆可能在等待審核、重試或 worker 取得工作期間跨過 24 小時視窗。可靠的發送護欄必須在實際發送前重新讀取伺服器端狀態,以最近一則已驗證的使用者訊息計算期限,並阻擋已過期的自由文字。訊息類別與計費規則可先查閱服務與實用訊息決策樹;本文只處理避免誤發的執行邊界。
重點摘要
- 保存兩個到期時間與單調遞增的
state_version,不要只保存布林狀態。 - 只有已驗證、已去重的使用者入站訊息可以重設 24 小時視窗。
- worker 取得工作後,應在同一交易或鎖內重新讀取狀態並判斷發送路徑。
- 已過期的佇列草稿應停止並重新路由,不能靜默轉成範本。
- 以重複事件、亂序事件、並行 worker 和秒級邊界測試驗證護欄。
圍繞佇列回覆建立 WhatsApp 發送護欄
WhatsApp 官方商業平台計費頁面說明,使用者訊息會開啟或重設 24 小時客服視窗。實作層只需把這條許可邊界設為最終發送條件;2026 年 10 月的訊息分類與計費變更由既有決策指南獨立說明,本文不再重複。
發送器執行每個佇列工作時必須回答三個實作問題:
- 讀到的是最新狀態版本嗎? 舊 worker 不得覆蓋更新後的視窗。
- 實際發送時視窗仍開啟嗎? 草稿建立或核准時的判斷不能沿用。
- 選擇的發送路徑仍獲授權嗎? 視窗關閉後只能使用真正符合且已核准的範本,否則停止並交回人工。
使用兩個時鐘,而不是單一狀態
請保存明確時間戳,讓重試、延遲工作或客服交接都能重新判斷。
| 保存欄位 | 何時開啟或重設 | 控制內容 |
|---|---|---|
service_window_expires_at | 每一則使用者入站訊息 | 是否可傳送非範本服務回覆 |
free_entry_expires_at | 符合資格的 Click-to-WhatsApp 廣告或 Facebook 粉絲專頁行動按鈕入口 | 72 小時內是否免除訊息送達費 |
last_user_message_id | 每一則已接受的使用者入站訊息 | 目前視窗的冪等與稽核依據 |
state_version | 每個改變狀態的入站事件 | 避免排程發送器覆蓋較新狀態 |
72 小時免費入口不是延長版客服視窗。使用者在這段期間再次傳訊息,可以重設 24 小時許可時鐘,但原本的免費入口到期點仍然獨立。
實作發送護欄
以下是應用層 TypeScript 範例,不是 Meta webhook 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;
}
這個判斷必須是最終發送護欄,不能只做介面提示。10:00 寫好的回覆可能在審核佇列中待到視窗關閉。發送器應在領取工作時重新讀取狀態,再選擇:
- 視窗仍開啟時傳送非範本服務回覆。
- 視窗關閉後,在業務意圖符合已核准範本時改走範本。
- 兩條路徑都不合法時停止並交回人工處理。
不要靜默把自由文字改成範本,也不要為了符合範本而截斷內容。範本類別、變數與審核是不同契約。
處理入站事件時不要誤延長視窗
只有使用者傳來的訊息才能重設客服視窗。送達回執、訊息狀態、客服草稿、內部註記、重試與企業端訊息 echo 都不能延長它。
建議依序處理:
- 先驗證 provider webhook,再變更狀態。
- 用穩定的訊息或事件識別碼去重。
- 確認事件是使用者傳送的入站訊息,而非回執或 echo。
- 將
service_window_expires_at設為已接受訊息時間加 24 小時。 - 只有符合資格的 referral 才建立獨立 72 小時免費入口。
- 增加
state_version,保存來源事件供稽核。 - 每次真正發送前重新計算兩個時鐘。
不要在舊到期時間上繼續加 24 小時。使用者 09:00 傳訊息、12:00 再傳一則時,新到期點應為隔日 12:00。
測試佇列回覆與事件順序邊界
以固定時鐘做驗收測試,讓一秒鐘的邊界也能重現。
| 情境 | 預期結果 |
|---|---|
| 使用者 10:00 傳訊息;隔日 09:59:59 回覆 | 允許非範本服務回覆 |
| 同一訊息;隔日正好 10:00 回覆 | 視窗視為已關閉 |
| 隔日 09:50 使用者又傳訊息 | 到期點重設為再隔日 09:50 |
| 到期前核准、到期後才領取的排程回覆 | 阻擋非範本發送並重新路由 |
| 同一使用者訊息重複送達兩次 | 去重後只產生一次狀態轉移 |
| 較舊的使用者訊息晚於新訊息抵達 | 保留較新的到期點,不讓狀態倒退 |
| 兩個 worker 同時取得同一佇列回覆 | 只有一個 worker 通過版本檢查並發送 |
上線後,應將系統預判與官方狀態和計費紀錄對帳。服務訊息費用追蹤指南說明如何拆分送達量與市場費率。若正在評估 Meta Business Agent 或自建 AI 流程,請閱讀計費與架構比較,不要把 AI 選項混進此狀態機。
UnifyPort 的位置
上述狀態機適用於官方 WhatsApp Business Platform。UnifyPort 的非官方介面是一般訊息帳號的另一條接入路徑,不提供 Meta 客服視窗或 Pricing Analytics 狀態。
在這條獨立路徑上,WhatsApp 入站訊息會以標準 message.received 事件送達。若 webhook 端點設定了 signing_secret,應先以原始請求內容驗證 X-Device-Timestamp 與 X-Device-Signature;標準支援範圍內的回覆使用 POST /v1/messages。詳見webhook 投遞與簽章文件及 message.received 事件文件。
不要把 UnifyPort 事件送入官方 Cloud API 計費狀態機後聲稱已由 Meta 分類。若團隊同時使用兩條路徑,請保存明確的 transport 或 control_plane 欄位並拆分帳本。
限制與取捨
需要已核准範本、行銷活動工具、Click-to-WhatsApp 歸因、Meta 原生分析或 BSP 代管流程時,應選擇官方 WhatsApp Business Platform。官方平台也是判斷官方送達是否計費的事實來源。
非官方介面不能核准範本、延長 Meta 視窗、提供 Meta Pricing Analytics 或改變 WhatsApp 政策。它的價值是為一般訊息帳號提供另一條接入路徑,並統一多平台事件。兩條路徑都需要伺服器端冪等、並行控制與稽核紀錄。
費率和產品規則具時效性。10 月 1 日切換前請重新檢查 Meta 官方文件,也不要把規劃費率寫死在狀態轉移邏輯中。
常見問題
發送護欄應使用哪個時間戳?
使用已驗證並被系統接受的最新使用者訊息時間;不要使用 webhook 接收時間、工作建立時間或客服介面的倒數計時。
亂序 webhook 會縮短視窗嗎?
不應。用穩定事件識別碼去重,且只在入站訊息時間晚於目前紀錄時更新到期點與 state_version。
佇列回覆在發送前過期怎麼辦?
停止自由文字發送並重新路由。只有業務意圖確實符合已核准範本時才能改走範本,否則交回人工。
可以自動把過期自由文字轉成範本嗎?
不能靜默轉換。範本類別、變數與核准是獨立契約,必須明確選擇並驗證。
如何避免重複事件觸發兩次發送?
持久化訊息或事件識別碼,並讓 worker 在交易或鎖內檢查冪等鍵與 state_version 後才發送。
下一步
先完成正確的入站邊界:閱讀webhook 投遞與簽章文件,再測試重複或亂序事件不會錯誤延長發送視窗。狀態轉移可靠後,再接上訊息分類。
來源
官方來源查核日期:2026 年 7 月 27 日。