有些範本根本不是人傳的。潮汐洗衣店希望預約系統一確認時段,收件確認就立刻發出去 —— 不該有人為了這個去開收件匣。為此有一個端點:
POST https://api.omniocto.com/api/wa-templates/{id}/send
它把一個已核准的範本傳給一個電話號碼。從 webhook 處理程式或腳本裡呼叫它 —— 「訂單出貨時,傳送收件範本」。因為是範本,24 小時視窗不適用 (原因)。
取得 API 金鑰
金鑰在 /settings/api-keys —— 齒輪選單裡的 管理與系統 → API 金鑰。
- 按下 建立金鑰。對話框 建立 API 金鑰 會要你填一個 名稱(選填) —— 例如 「例如 CI 流程」;用將要使用它的系統來命名。
- 已建立 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 |
components | Meta 自己的 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 接受的;回應本文會列出問題 |
| 402 | billing_not_eligible | 工作區還不能傳送外送訊息 |
| 404 | template_not_found | 這個工作區裡沒有這個 ID 的範本 |
| 409 | blocked_dnc | 該號碼已取消訂閱。訊息會以已攔截狀態保留在對話裡;不會有任何東西到達 Meta |
| 422 | template_not_approved | Meta 還沒有核准這個範本;status 說明它目前的狀態 |
| 422 | template_components_mismatch | components 和範本對不上。error 會指出問題 |
| 422 | channel_unavailable | 範本所屬的 WhatsApp 號碼已被移除或停用 |
| 422 | channel_pending_verification, workspace_suspended, route_blocked | 針對號碼或工作區的傳送檢查拒絕了它。訊息會以已攔截或失敗狀態保留在對話裡 |
| 429 | workspace_rate_limited | 你的工作區在這個端點上超出了速率。不會寫入任何內容 |
| 429 | rate_limited | 這個 WhatsApp 號碼的突發視窗已滿 —— 等幾秒到幾分鐘 |
| 429 | daily_limit_reached | 這個號碼的每日範本上限已用完 —— 可能要等幾個小時 |
| 502 | send_failed | Meta 拒絕了這次傳送;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 拒絕投遞 —— 請見原因。
接下來讀什麼
- 把 WhatsApp 範本匯入 Omni Octo —— 範本必須先獲得核准並完成同步。
- 一個號碼能傳多快
——
rate_limited和daily_limit_reached背後的依號碼額度。