從你自己的系統傳送範本

一次 API 呼叫,把一個已核准的 WhatsApp 範本傳給一個號碼 —— 從 webhook、排程工作或腳本發出。金鑰、範本 ID、請求、每一種拒絕,以及設計時要考慮的兩件事。

更新於

有些範本根本不是人傳的。潮汐洗衣店希望預約系統一確認時段,收件確認就立刻發出去 —— 不該有人為了這個去開收件匣。為此有一個端點:

POST https://api.omniocto.com/api/wa-templates/{id}/send

它把一個已核准的範本傳給一個電話號碼。從 webhook 處理程式或腳本裡呼叫它 —— 「訂單出貨時,傳送收件範本」。因為是範本,24 小時視窗不適用 (原因)。

取得 API 金鑰

金鑰在 /settings/api-keys —— 齒輪選單裡的 管理與系統 → API 金鑰。

  1. 按下 建立金鑰。對話框 建立 API 金鑰 會要你填一個 名稱(選填) —— 例如 「例如 CI 流程」;用將要使用它的系統來命名。
  2. 已建立 API 金鑰 只會顯示一次金鑰,並提示 「請立即複製此金鑰 — 之後將無法再次 查看。」 把它存到你的系統存放機密的地方。

清單會顯示每把金鑰的 上次使用 日期,撤銷金鑰 會立即停用它:「任何使用它的 整合都會失敗。」 每個整合用一把金鑰,撤銷才安全。

每次呼叫都以 Authorization: Bearer <key> 的形式帶上金鑰。

找到範本 ID

開啟 /templates/whatsapp,點選範本。抽屜面板開啟,頁面網址會多出 &template=wt-… —— 這個 wt- 短 ID 就是要放進路徑裡的。(範本的 UUID 也可以。)

同一個面板會告訴你這次呼叫需要填什麼:

  • 變數 —— 內文的變數:要嘛是 「2 個位置變數」({{1}}、{{2}}),要嘛是 名稱: 後面接著變數名稱。
  • 頁首 —— 它的文字,或是媒體頁首對應的 [IMAGE]、[VIDEO] 或 [DOCUMENT]。
  • 按鈕 —— 依順序列出,每個都附類型([URL]、[COPY_CODE]、[QUICK_REPLY])。 第一個的索引是 0。

在腳本裡,GET /api/wa-templates?status=approved 會列出已核准的範本,附 shortId、name、language 和 category。

只有已核准的範本才能傳送;其他任何狀態都會被拒絕。

請求

請求本文有兩個欄位:

欄位意思
to電話號碼,E.164 格式 —— +14255550123
componentsMeta 自己的 template.components 陣列,原樣傳遞。沒有變數的範本可以省略

components 就是你傳給 Meta Cloud API 的那個陣列,所以 Meta 關於它的文件同樣適用。 簡單說:

  • 頁首 —— 一個參數:帶變數的文字頁首用 text,或是用帶 https:// link 的 image、video 或 document。
  • 內文 —— text 參數。依位置的範本({{1}}、{{2}})依順序傳送,不帶名稱。 具名範本({{customer_name}})要替每個參數加上 parameter_name。兩種寫法不要 混用。Octo 建立的範本在這裡是依位置的,不管你當初替變數取了什麼名稱。
  • 按鈕 —— 每個需要值的按鈕一個元件,附 sub_type 和 index(它在範本按鈕清單 裡的位置):
    • url —— 一個 text 參數:連結裡變動的那部分。
    • copy_code —— 代碼,可以用 Meta 的 { "type": "coupon_code", "coupon_code": "FRESH20" } 形式,也可以用 { "type": "text" }。最多 15 個字符。
    • quick_reply —— 選填;如果你希望按鈕被點按時收到一個 payload,就加一個 { "type": "payload" } 參數。

完整範例

潮汐洗衣店的收件範本 pickup_confirmation,內文是 Hi {{1}}, your pickup is confirmed for {{2}}. Reply here to change it.,還有一個 Track pickup 按鈕,連結以訂單編號結尾:

curl -X POST "https://api.omniocto.com/api/wa-templates/wt-a1b2c3d4e5/send" \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14255550123",
    "components": [
      { "type": "body", "parameters": [
        { "type": "text", "text": "Ana" },
        { "type": "text", "text": "Tuesday, 9–11am" }
      ] },
      { "type": "button", "sub_type": "url", "index": "0",
        "parameters": [{ "type": "text", "text": "TW-20931" }] }
    ]
  }'

如果這個範本用的是具名變數,內文會替每個值加上名稱:

{ "type": "body", "parameters": [
  { "type": "text", "parameter_name": "customer_name", "text": "Ana" },
  { "type": "text", "parameter_name": "pickup_slot", "text": "Tuesday, 9–11am" }
] }

成功回傳 201:

{
  "messageId": "6f1c…",
  "conversationId": "3a9e…",
  "conversationShortId": "cv-9f8e7d6c5b",
  "externalId": "wamid.HBgL…",
  "isNewConversation": false,
  "template": { "id": "…", "shortId": "wt-a1b2c3d4e5", "name": "pickup_confirmation", "language": "en_US" }
}

externalId 是 Meta 回報送達和已讀回條時使用的 ID;這些回條稍後會到達對話。 201 表示 Meta 接受了這次傳送,不代表手機已經收到。 把 conversationShortId 存下來 —— 下文會用到。

訊息落在哪裡

在 /conversations 裡,範本所屬 WhatsApp 號碼上與該號碼的對話 —— 也就是顧客的 訊息所到達的同一個對話:

  • 如果與這個號碼的對話已經存在,範本會加入其中 —— 如果它是已關閉的,會被重新 開啟。isNewConversation 為 false。
  • 如果不存在,會新開一個對話(對陌生號碼還會新建一位聯絡人)。isNewConversation 為 true。
  • 泡泡顯示填入了你的值的內文,並附有 範本 · 和範本名稱的標籤。

顧客回覆時,回覆會落到同一個對話裡 —— 而且透過 API 傳送並不會把對話交給某個人。原本 就存在的對話,仍由原本處理它的一方繼續處理;已關閉的或新的對話,會在接聽這個 WhatsApp 號碼的 AI 智慧體名下變為活躍。所以只要這個號碼有智慧體,就由它來回答這則 回覆,除非有人在 /conversations 裡 接手了這個對話。

傳送被拒絕時

狀態code意思
400—to 不是 E.164 格式,或是 components 的結構不是 Meta 接受的;回應本文會列出問題
402billing_not_eligible工作區還不能傳送外送訊息
404template_not_found這個工作區裡沒有這個 ID 的範本
409blocked_dnc該號碼已取消訂閱。訊息會以已攔截狀態保留在對話裡;不會有任何東西到達 Meta
422template_not_approvedMeta 還沒有核准這個範本;status 說明它目前的狀態
422template_components_mismatchcomponents 和範本對不上。error 會指出問題
422channel_unavailable範本所屬的 WhatsApp 號碼已被移除或停用
422channel_pending_verification, workspace_suspended, route_blocked針對號碼或工作區的傳送檢查拒絕了它。訊息會以已攔截或失敗狀態保留在對話裡
429workspace_rate_limited你的工作區在這個端點上超出了速率。不會寫入任何內容
429rate_limited這個 WhatsApp 號碼的突發視窗已滿 —— 等幾秒到幾分鐘
429daily_limit_reached這個號碼的每日範本上限已用完 —— 可能要等幾個小時
502send_failedMeta 拒絕了這次傳送;error 帶著 Meta 的訊息。訊息會以失敗狀態留在對話裡

每個 429 都帶一個 Retry-After 標頭,回應本文裡還有對應的 retryAfterSec。等待這麼 久之後,重新發出同一個呼叫:第一次並沒有傳送、儲存或計費任何訊息。(傳給一個新號碼 的第一則訊息如果被號碼的額度拒絕,對話本身仍然已經建立,是空的;回應本文裡帶著它的 conversationShortId。)

兩類 429 是不同的限制。你的工作區 預設每分鐘可以呼叫這個端點 300 次,突發時每秒 最多 10 次;這個速率只計算 API 呼叫,由 Omni Octo 支援團隊依工作區設定。WhatsApp 號碼 有它自己的傳送額度,與收件匣、Octo 和行銷活動共用 —— 一個號碼能傳多快 有詳細說明。

不相符時什麼都不會傳

呼叫會在寫入任何內容之前,拿 components 和範本做比對。缺一個值、多一個值、未知 的名稱、頁首類型不對、給不接受參數的按鈕傳了參數 —— 每一種都是 422 template_components_mismatch,不儲存訊息,也不傳送任何東西。error 會準確說出哪裡 不對:

body expects 2 parameters, got 1
body is missing variable "pickup_slot"
button 0 is a QUICK_REPLY button, got url
button 0 (a URL button) is missing its url parameter

所以不相符的情況可以放心修正後重新傳送。

這個呼叫不是冪等的

每一次成功的呼叫都會傳送 —— 並計費 —— 一個範本。沒有冪等金鑰。如果一次呼叫逾時或 連線中斷,你無法知道它是否已經送出,盲目重試可能會傳兩次。

逾時後重試之前,先檢查對話 ——

GET /api/conversations/{conversationShortId}

用之前傳給這個號碼時拿到的 ID,或是在 /conversations 裡開啟這個對話。只有範本 不在裡面時才重新傳送。

上面那些拒絕不一樣:4xx 明確告訴你這次傳送沒有發生。

這個端點做不到的事

  • 傳送自由格式文字。 它只傳已核准的範本 —— 要在視窗內回覆,請用 收件匣。
  • 傳給一份名單。 一次呼叫對應一個號碼;名單由 行銷活動處理。
  • 推翻 Meta 的決定。 即使回傳了 201,行銷類範本仍可能被 Meta 拒絕投遞 —— 請見原因。

接下來讀什麼