外觀
通用 Webhook 給沒有現成串接的系統用,例如自家 App、官網客服視窗、自建的客服系統。 流程是非同步的:
- 你的系統把客人的訊息 POST 到 OakXgen 的接收網址,我們立刻回
200。 - AI 想好回覆之後,POST 到你設定的 Callback URL。
兩個方向都用同一把驗簽金鑰簽名。Channel agent 與 Skill agent 都能用,差別見文末。
在後台啟用
到 AI 代理的串接分頁,最下方的通用 Webhook 按啟用 Webhook,會立刻產生:
- 接收網址(給對方打)——格式是
https://webhook.oakxgen.ai/agent/<webhook_id>/, 每個 AI 只有一條。 - 驗簽金鑰——只會顯示這一次,請當下複製存好。弄丟了只能按重新產生金鑰, 舊的會立刻失效。
接著填 Callback URL(AI 回覆送到這裡) 並儲存。還沒填 Callback URL 之前, 打進來的請求會回 400:
json
{"error": "這個 webhook 還沒設定 callback URL,AI 的回覆沒有地方送"}之所以直接擋下而不是默默收下,是因為收了也送不出回覆——與其讓訊息石沉大海, 不如讓串接的工程師第一時間就看到原因。
Callback URL 必須是對外公開的 https:// 網址(callback 帶的是客人的對話內容,不接受明文的 http://)。內網 IP、localhost、雲端 metadata 位址一律不允許,會看到「callback URL 不允許」 開頭的錯誤。你的端點如果轉址,轉址目標也必須是 https,否則那次投遞算失敗。
簽章
請求與 callback 的簽章算法相同,都帶兩個 header:
X-OakXgen-Timestamp: <現在的 unix 秒數>
X-OakXgen-Signature: base64( HMAC-SHA256( 驗簽金鑰, "<timestamp>." + 原始 request body ) )時間也簽進去:跟我們的時鐘差超過 5 分鐘的請求一律拒收。這樣就算有人截到一個合法的 請求,也沒辦法事後原樣重送。你收 callback 時也請做一樣的檢查。
簽的是原始 body 的位元組,不是重新序列化過的 JSON。你收 callback 驗簽時, 請拿收到的 raw body 計算,不要先 parse 再 dump,空白或欄位順序一變簽章就對不上。
Python:
python
import base64, hashlib, hmac, json, time, requests
body = json.dumps(payload, ensure_ascii=False).encode()
timestamp = str(int(time.time()))
message = timestamp.encode() + b"." + body
signature = base64.b64encode(hmac.new(SECRET.encode(), message, hashlib.sha256).digest()).decode()
requests.post(INBOUND_URL, data=body, headers={
"Content-Type": "application/json",
"X-OakXgen-Timestamp": timestamp,
"X-OakXgen-Signature": signature,
})Node.js:
js
import crypto from 'node:crypto'
const body = JSON.stringify(payload)
const timestamp = String(Math.floor(Date.now() / 1000))
const signature = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${body}`)
.digest('base64')
await fetch(INBOUND_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-OakXgen-Timestamp': timestamp,
'X-OakXgen-Signature': signature
},
body
})驗 callback(Python):
python
def verify(raw_body: bytes, headers) -> bool:
timestamp = headers.get("X-OakXgen-Timestamp", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = base64.b64encode(
hmac.new(SECRET.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, headers.get("X-OakXgen-Signature", ""))簽章錯誤、沒帶簽章、沒帶時間或時間差超過 5 分鐘都會回 403。請確認你的伺服器有校時(NTP)。
送訊息進來
http
POST https://webhook.oakxgen.ai/agent/<webhook_id>/
Content-Type: application/json
X-OakXgen-Timestamp: 1790000000
X-OakXgen-Signature: ...
{
"user_id": "u-123",
"text": "請問出貨要幾天?",
"display_name": "王小明",
"history": [
{"role": "user", "text": "你好"},
{"role": "assistant", "text": "您好,請問需要什麼協助?"}
]
}| 欄位 | 必填 | 說明 |
|---|---|---|
user_id | ✅ | 你那邊的客人 ID(字串,最長 128 字)。同一個客人每次都要一樣,AI 靠它接續表單、情境等流程狀態。 |
text | 二擇一 | 一則文字訊息。 |
messages | 二擇一 | 一次送多則,例如文字加圖片,見下方。 |
history | ✅ | 這一則之前的對話,不含這一則。第一句給空陣列 []。 |
display_name | 客人名稱,AI 稱呼客人時會用。 | |
message_id | 你那邊的訊息 ID。逾時重送同一則時帶一樣的值,AI 還沒回覆之前收到的重複訊息會被忽略。 | |
reset | true 代表重新開始對話,見下方。 |
一次送多則:
json
{
"user_id": "u-123",
"messages": [
{"type": "text", "text": "這件有其他顏色嗎?"},
{"type": "image", "url": "https://example.com/photo.jpg"}
],
"history": []
}messages[].type 可以是 text、image、audio、video、file;非文字的一定要帶 url。
為什麼 history 每次都要帶
OakXgen 不保存 webhook 的對話內容,只保存流程狀態(表單填到哪、在哪個情境、 是否暫停中)。對話的正本在你的系統裡,理由有二:
- 轉真人之後,真人客服回的話發生在你那邊,我們看不到。AI 接回來時如果看不到 真人答應過客人什麼,就會講出矛盾的話。
- 你可以決定要給 AI 看多少歷史、要不要濾掉敏感內容。
history 每一項:
json
{"role": "user", "text": "你好"}
{"role": "assistant", "text": "您好,請問需要什麼協助?"}
{"role": "human", "text": "我是客服小美,幫您查一下訂單"}
{"role": "user", "type": "image", "url": "https://example.com/photo.jpg"}role:user是客人、assistant是 AI、human是你們的真人客服。type:預設text,也可以是image、audio、video、file(帶url)。- 最多取最後 200 則。
少帶 history 會回 400:
json
{"error": "缺少 history(對話歷史陣列,第一句就給空陣列 [])"}重新開始對話
json
{"user_id": "u-123", "reset": true}清掉這位客人的流程狀態(表單、情境、暫停),也會解除轉真人,下一則訊息 AI 重新接手。 只送 reset 不用帶 history。
回應
| 狀態 | 意思 |
|---|---|
200 {"status": "ok"} | 收到了。回覆稍後送到 callback。 |
400 {"error": "..."} | 格式錯誤,error 會寫是哪個欄位。 |
403 | 簽章錯誤,或時間差超過 5 分鐘。 |
404 | 接收網址不存在,或 webhook 已停用。 |
AI 代理被關閉時也會回 200,但不會有 callback——避免你的系統因為我們這邊關掉 AI 而一直重送。
收 callback
我們 POST 到你的 Callback URL,帶這三個 header(簽法見上方簽章):
X-OakXgen-Timestamp: <unix 秒數>
X-OakXgen-Signature: base64(HMAC-SHA256(驗簽金鑰, "<timestamp>." + raw body))
X-OakXgen-Webhook-ID: <webhook_id>你的端點回 2xx 就算送達;4xx、5xx 或 15 秒內沒回應算失敗,後台的串接卡片會顯示 最後一次失敗的原因。我們不會自動重送。
AI 的回覆
json
{
"event": "messages",
"reason": "reply",
"user_id": "u-123",
"agent_id": 1,
"messages": [
{"type": "text", "text": "一般 3 個工作天內出貨。"},
{
"type": "cards",
"alt_text": "推薦商品",
"cards": [
{
"title": "經典帆布包",
"description": "NT$ 890",
"image_url": "https://example.com/bag.jpg",
"buttons": [{"label": "看商品", "url": "https://example.com/p/1"}]
}
],
"quick_replies": [{"label": "查訂單", "data": "查訂單"}]
}
]
}messages 是一串「泡泡」,照順序顯示就是 AI 想講的樣子:
{"type": "text", "text": "..."}{"type": "image", "img_url": "...", "preview_img_url": "..."}{"type": "cards", "cards": [...], "alt_text": "..."}——alt_text是不支援卡片時的替代文字。- 最後一顆泡泡可能帶
quick_replies:[{"label": 顯示文字, "data": 按下後要當成客人訊息送回來的字}]。
怎麼畫由你決定。不支援卡片的話,顯示 alt_text 或把 title、description 轉成文字即可。
reason:
reply——回覆客人剛送的訊息。follow_up——客人一陣子沒回,AI 主動追問(只有 Channel agent 開了主動追問才會有)。
轉真人(只有 Channel agent)
在通用 Webhook 卡片勾了對方有真人客服:轉真人時通知對方,由對方的人接手, 轉真人時會收到:
json
{"event": "handoff", "user_id": "u-123", "agent_id": 1}收到之後這位客人的訊息請交給你們的真人客服,AI 會一直暫停,不會自己恢復—— 這段期間打進來的訊息我們會收下(回 200)但不處理。真人處理完要讓 AI 接回來,送 {"user_id": "u-123", "reset": true},之後客人再傳訊息就由 AI 回覆。記得把真人客服 講過的話以 role: "human" 放進 history,AI 才知道真人答應過客人什麼。
沒勾的話,轉真人時不會通知你,也不會傳任何話給客人,AI 對這位客人暫停 30 分鐘 (已讀不回),之後自動恢復。細節見轉真人怎麼觸發。
Channel agent 與 Skill agent 的差別
| Channel agent | Skill agent | |
|---|---|---|
| 表單、情境等流程 | ✅ | — |
主動追問(reason: follow_up) | ✅ | — |
轉真人(event: handoff) | ✅ | — |
| 看得懂的訊息 | 文字、圖片 | 只有文字(圖片等會被略過) |
| 回覆格式 | 泡泡 | 泡泡 |
Skill agent 要逐字串流(打字機效果)的話,webhook 做不到,請改用對話 API。