Skip to content

通用 Webhook 串接:請求格式、簽章與 callback

通用 Webhook 給沒有現成串接的系統用,例如自家 App、官網客服視窗、自建的客服系統。 流程是非同步的:

  1. 你的系統把客人的訊息 POST 到 OakXgen 的接收網址,我們立刻回 200。
  2. 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 還沒回覆之前收到的重複訊息會被忽略。
resettrue 代表重新開始對話,見下方。

一次送多則:

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 agentSkill agent
表單、情境等流程✅—
主動追問(reason: follow_up)✅—
轉真人(event: handoff)✅—
看得懂的訊息文字、圖片只有文字(圖片等會被略過)
回覆格式泡泡泡泡

Skill agent 要逐字串流(打字機效果)的話,webhook 做不到,請改用對話 API。