← 所有文章
教學

為佇列回覆實作 WhatsApp 24 小時發送護欄

佇列中的 WhatsApp 回覆可能在等待審核、重試或 worker 取得工作期間跨過 24 小時視窗。可靠的發送護欄必須在實際發送前重新讀取伺服器端狀態,以最近一則已驗證的使用者訊息計算期限,並阻擋已過期的自由文字。訊息類別與計費規則可先查閱服務與實用訊息決策樹;本文只處理避免誤發的執行邊界。

重點摘要

  • 保存兩個到期時間與單調遞增的 state_version,不要只保存布林狀態。
  • 只有已驗證、已去重的使用者入站訊息可以重設 24 小時視窗。
  • worker 取得工作後,應在同一交易或鎖內重新讀取狀態並判斷發送路徑。
  • 已過期的佇列草稿應停止並重新路由,不能靜默轉成範本。
  • 以重複事件、亂序事件、並行 worker 和秒級邊界測試驗證護欄。

圍繞佇列回覆建立 WhatsApp 發送護欄

WhatsApp 官方商業平台計費頁面說明,使用者訊息會開啟或重設 24 小時客服視窗。實作層只需把這條許可邊界設為最終發送條件;2026 年 10 月的訊息分類與計費變更由既有決策指南獨立說明,本文不再重複。

發送器執行每個佇列工作時必須回答三個實作問題:

  1. 讀到的是最新狀態版本嗎? 舊 worker 不得覆蓋更新後的視窗。
  2. 實際發送時視窗仍開啟嗎? 草稿建立或核准時的判斷不能沿用。
  3. 選擇的發送路徑仍獲授權嗎? 視窗關閉後只能使用真正符合且已核准的範本,否則停止並交回人工。

使用兩個時鐘,而不是單一狀態

請保存明確時間戳,讓重試、延遲工作或客服交接都能重新判斷。

保存欄位何時開啟或重設控制內容
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 寫好的回覆可能在審核佇列中待到視窗關閉。發送器應在領取工作時重新讀取狀態,再選擇:

  1. 視窗仍開啟時傳送非範本服務回覆。
  2. 視窗關閉後,在業務意圖符合已核准範本時改走範本。
  3. 兩條路徑都不合法時停止並交回人工處理。

不要靜默把自由文字改成範本,也不要為了符合範本而截斷內容。範本類別、變數與審核是不同契約。

處理入站事件時不要誤延長視窗

只有使用者傳來的訊息才能重設客服視窗。送達回執、訊息狀態、客服草稿、內部註記、重試與企業端訊息 echo 都不能延長它。

建議依序處理:

  1. 先驗證 provider webhook,再變更狀態。
  2. 用穩定的訊息或事件識別碼去重。
  3. 確認事件是使用者傳送的入站訊息,而非回執或 echo。
  4. service_window_expires_at 設為已接受訊息時間加 24 小時。
  5. 只有符合資格的 referral 才建立獨立 72 小時免費入口。
  6. 增加 state_version,保存來源事件供稽核。
  7. 每次真正發送前重新計算兩個時鐘。

不要在舊到期時間上繼續加 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-TimestampX-Device-Signature;標準支援範圍內的回覆使用 POST /v1/messages。詳見webhook 投遞與簽章文件message.received 事件文件

不要把 UnifyPort 事件送入官方 Cloud API 計費狀態機後聲稱已由 Meta 分類。若團隊同時使用兩條路徑,請保存明確的 transportcontrol_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 日。