為排隊回覆實作 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,應先用原始 request 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 託管流程時,應選擇官方 WhatsApp Business Platform。官方平台亦是判斷官方送達是否收費的事實來源。
非官方接口不能審批範本、延長 Meta 時段、提供 Meta Pricing Analytics 或改變 WhatsApp 政策。它的價值是為一般訊息帳號提供另一條接入路徑,並統一多平台事件。兩條路徑都需要伺服器端冪等、並行控制及審計記錄。
費率及產品規則具時效性。10 月 1 日切換前請重新核對 Meta 官方文件,也不要把規劃費率寫死在狀態轉移邏輯內。
常見問題
發送護欄應使用哪個時間戳?
使用已驗證並被系統接受的最新用戶訊息時間;不要使用 webhook 接收時間、工作建立時間或客服畫面的倒數計時。
亂序 webhook 會縮短時段嗎?
不應。用穩定事件識別碼去重,只在入站訊息時間晚於目前記錄時更新到期點及 state_version。
排隊回覆在發送前過期怎麼辦?
停止自由文字發送並重新分流。只有業務意圖確實符合已審批範本時才可改走範本,否則交回人工。
可以自動把過期自由文字轉成範本嗎?
不可靜默轉換。範本類別、變數及審批是獨立契約,必須明確選擇並驗證。
如何避免重複事件觸發兩次發送?
持久化訊息或事件識別碼,並讓 worker 在交易或鎖內檢查冪等鍵及 state_version 後才發送。
下一步
先完成正確的入站邊界:閱讀webhook 投遞及簽署文件,再測試重複或亂序事件不會錯誤延長發送時段。狀態轉移可靠後,再接上訊息分類。
來源
官方來源核實日期:2026 年 7 月 27 日。