> For the complete documentation index, see [llms.txt](https://docs.maiagent.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.maiagent.ai/api/preparation/message-webhook.md).

# 訊息 Webhook

對話平台每收到一則客戶訊息、或每送出一則回覆，MaiAgent 都可以把該則訊息推送到你指定的網址。本篇說明 Incoming／Outgoing 的差別、後台與 API 設定方式、推送內容、來源驗證（HMAC 簽章），以及暫時性失敗時的自動重送

在對話平台（Inbox）設定訊息 Webhook 後，每一則訊息都會由 MaiAgent 主動推送到你的系統，不需要輪詢。常見用途：在自己的系統顯示「有新訊息」、把客戶留言同步到 CRM、把 AI 回覆記錄到資料倉儲。

一個對話平台可以設定多組訊息 Webhook，每組各自選擇類型、網址與格式，彼此獨立推送。

{% hint style="info" %}
只想在**轉真人**時收到通知，請用 [轉真人 Webhook 事件](/api/preparation/handoff-webhook.md)：它只在轉真人流程的狀態改變時推送，不會每則訊息都推送。兩者是各自獨立的設定，可以同時啟用、由同一支接收端處理。
{% endhint %}

## 類型：Incoming 與 Outgoing <a href="#directions" id="directions"></a>

| 類型         | 何時推送                                 | payload 的 `type` |
| ---------- | ------------------------------------ | ---------------- |
| `Incoming` | 對話平台每收到一則客戶（聯絡人）訊息                   | `incoming`       |
| `Outgoing` | 每送出一則回覆到客戶所在的對話平台，包括 AI 自動回覆與真人客服的回覆 | `outgoing`       |

* 後台 `Incoming` 選項的說明文字是「接收外部請求」，但實際行為是**把每則客戶訊息推送到你設定的網址**。
* 同一個選單中的 `Receive` 用途相反：由外部平台把訊息送進 MaiAgent，不屬於本篇的推送範圍。經由 `Receive` 送進來的訊息不會觸發同一對話平台的 `Incoming`／`Outgoing` 推送，避免訊息在兩個系統之間來回循環。
* 需要同時知道客戶訊息與回覆時，請各設一組 `Incoming` 與 `Outgoing`，可以指向同一個網址，再以 payload 的 `type` 區分。

## 設定方式 <a href="#setup" id="setup"></a>

### 在後台設定 <a href="#setup-admin" id="setup-admin"></a>

1. 後台 → <mark style="color:blue;">客服對話</mark> → <mark style="color:blue;">對話平台</mark> → 選擇對話平台 → <mark style="color:blue;">Webhook 設定</mark> → <mark style="color:blue;">新增 Webhook</mark>
2. 填寫 <mark style="color:blue;">名稱</mark>，<mark style="color:blue;">類型</mark>選 <mark style="color:blue;">Incoming</mark> 或 <mark style="color:blue;">Outgoing</mark>，填入 <mark style="color:blue;">Webhook URL</mark>，選擇 <mark style="color:blue;">HTTP Method</mark>（預設 `POST`）。
3. （建議）在 <mark style="color:blue;">Headers</mark> 新增一組驗證用的標頭，例如 `Authorization: Bearer <你產生的隨機字串>`，接收端據此確認請求來自 MaiAgent（見[驗證推送來源](#verify-source)）。
4. <mark style="color:blue;">Payload 格式</mark>選 <mark style="color:blue;">MaiAgent Standard</mark>（見[推送內容](#payload)）；需要自訂欄位時選 <mark style="color:blue;">自訂範本</mark>。
5. 確認 <mark style="color:blue;">啟用狀態</mark> 已開啟後儲存。關閉啟用狀態即暫停推送，設定會保留。

WhatsApp 對話平台的設定頁目前沒有 <mark style="color:blue;">Webhook 設定</mark> 分頁，請改用下方的 API 設定。

### 以 API 設定 <a href="#setup-api" id="setup-api"></a>

訊息 Webhook 掛在對話平台（Inbox）之下：

| 動作   | 方法與路徑                                                      |
| ---- | ---------------------------------------------------------- |
| 列出   | `GET /api/v1/inboxes/{inbox_id}/webhooks/`                 |
| 新增   | `POST /api/v1/inboxes/{inbox_id}/webhooks/`                |
| 查詢單筆 | `GET /api/v1/inboxes/{inbox_id}/webhooks/{id}/`            |
| 修改   | `PATCH /api/v1/inboxes/{inbox_id}/webhooks/{id}/`（只送要改的欄位） |
| 刪除   | `DELETE /api/v1/inboxes/{inbox_id}/webhooks/{id}/`         |

API 金鑰擁有者需能管理該對話平台，詳見 [API 認證 → API 金鑰的權限範圍](/api/preparation/authentication.md#api-key-permissions)。

| 欄位               | 必填           | 說明                                                                          |
| ---------------- | ------------ | --------------------------------------------------------------------------- |
| `name`           | 是            | 名稱，方便辨識                                                                     |
| `direction`      | 是            | `incoming` 或 `outgoing`                                                     |
| `url`            | 是            | 接收推送的網址，必須可從網際網路連線（建議 `https://`）；指向內部網段或本機位址的網址會被拒送                        |
| `method`         | 否            | `POST`（預設）、`PUT` 或 `PATCH`                                                  |
| `headers`        | 否            | 每次推送都會帶上的自訂標頭，JSON 物件，例如 `{"Authorization": "Bearer ..."}`                  |
| `templatePreset` | 否            | `maiagent_standard`（預設）、`custom`，或串接 Genesys Cloud 用的 `genesys_cloud`       |
| `customTemplate` | `custom` 時必填 | Jinja2 範本，渲染結果必須是 JSON；儲存時會先試算一次，格式錯誤會回 400                                 |
| `secretToken`    | 否            | 簽章密鑰；設定後每次推送都帶 HMAC-SHA256 簽章與時間戳標頭（見[驗證推送來源](#verify-source)）。只能寫入，查詢時不會回傳 |
| `isEnabled`      | 否            | 是否啟用，預設 `true`                                                              |

```bash
curl -X POST "https://api.maiagent.ai/api/v1/inboxes/YOUR_INBOX_ID/webhooks/" \
  -H "Authorization: Api-Key YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "新訊息通知",
    "direction": "incoming",
    "url": "https://example.com/webhooks/maiagent-message",
    "secretToken": "YOUR_RANDOM_SECRET"
  }'
```

成功回 `201` 與該筆設定（不含 `secretToken`）。要再收回覆，以 `"direction": "outgoing"` 再新增一筆。

## 推送內容 <a href="#payload" id="payload"></a>

每則訊息以一個 HTTP 請求推送：

* 標頭：`Content-Type: application/json`、你設定的自訂標頭、`X-MaiAgent-Delivery-Attempt`（第幾次嘗試，第一次為 `1`），以及有設定 `secretToken` 時的 `X-MaiAgent-Timestamp`、`X-MaiAgent-Signature-256`。自訂標頭與這三個標頭同名（不分大小寫）時，送出前會被移除。
* 內容：依 <mark style="color:blue;">Payload 格式</mark> 產生的 JSON。

### MaiAgent Standard 格式 <a href="#standard-format" id="standard-format"></a>

欄位一律為 snake\_case（與 API 回應的 camelCase 不同）。以下是一則 `Outgoing`（AI 回覆）的推送內容：

```json
{
  "id": "a0c49ae2-013c-4310-b00f-b4554bc9cd83",
  "type": "outgoing",
  "content": "您好，已收到您的訂單問題，請提供訂單編號。",
  "created_at": "2026-10-03T04:59:01.512340+00:00",
  "conversation": {
    "id": "e6b27044-4990-4129-ad8e-ac7649984e7d",
    "title": "訂單地址修改",
    "type": "individual",
    "status": "open",
    "progress_status": "ai_processing",
    "auto_reply_enabled": true,
    "assignee": null
  },
  "inbox": { "id": "6571672b-2fae-4200-a4d0-cf12e86216c3", "name": "官網客服", "channel_type": "web" },
  "contact": { "id": "350691e0-66a5-48ea-855c-d8bb90a7e24a", "name": "王小明", "email": null, "phone_number": null },
  "sender": { "id": "8a214fc3-75ad-43ba-93f6-567e9df97b76", "name": "客服助理", "type": "chatbot" },
  "attachments": [],
  "metadata": {}
}
```

| 欄位             | 說明                                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------------------- |
| `id`           | 訊息 id，可用來去重，也可用於[訊息 API](/api/api-reference/dui-hua-he-xun-xi.md) 查詢                                    |
| `type`         | `incoming`（客戶訊息）或 `outgoing`（回覆）；判斷方向請用這個欄位                                                             |
| `content`      | 訊息文字                                                                                                    |
| `created_at`   | 訊息建立時間（ISO 8601，UTC）                                                                                    |
| `conversation` | 所屬對話。`progress_status` 為 `human_serving` 表示目前由真人客服處理；`assignee` 是被指派的客服 `{ "id", "name" }`，未指派時為 `null` |
| `inbox`        | 對話平台 `{ "id", "name", "channel_type" }`，`channel_type` 例如 `web`、`line`、`messenger`                      |
| `contact`      | 客戶（聯絡人）；`email`、`phone_number` 沒有資料時為 `null`                                                            |
| `sender`       | 發話者，`type` 為 `contact`（客戶）、`chatbot`（Agent）或 `user`（組織成員，例如真人客服；以 API 金鑰代客戶送出的訊息也會是 `user`）             |
| `attachments`  | 附件陣列，每筆含 `id`、`type`（`image`／`video`／`audio`／`sticker`／`other`）、`filename`、`source_url`                 |
| `metadata`     | 訊息的附加資訊，內容依對話平台而異                                                                                       |

### 自訂範本 <a href="#custom-template" id="custom-template"></a>

<mark style="color:blue;">自訂範本</mark>（API 為 `templatePreset: "custom"`）以 Jinja2 語法組出你要的 JSON。可用變數：`message`（`id`、`type`、`content`、`created_at`、`metadata`）、`conversation`、`inbox`、`contact`、`sender`、`sender_type`（`contact`／`chatbot`／`user`）、`attachments`、`now`。字串請用 `tojson` 過濾器輸出，避免引號或換行破壞 JSON：

```
{
  "text": {{ message.content | tojson }},
  "direction": "{{ message.type }}",
  "conversation_id": "{{ conversation.id }}",
  "customer": {{ (contact.name if contact else none) | tojson }}
}
```

推送當下範本渲染失敗時，該則訊息不會送出。

## 驗證推送來源 <a href="#verify-source" id="verify-source"></a>

設定 `secretToken`（API 欄位，或後台 Webhook 表單的 <mark style="color:blue;">Secret Token</mark>；Payload 格式為 Genesys Cloud 時後台不顯示此欄位）後，每次推送都會帶以下標頭：

| 標頭                         | 內容                                                                       |
| -------------------------- | ------------------------------------------------------------------------ |
| `X-MaiAgent-Timestamp`     | 送出當下的 Unix 時間（秒）                                                         |
| `X-MaiAgent-Signature-256` | `sha256=` 加上 `HMAC-SHA256(secretToken, "{timestamp}." + 原始 body)` 的十六進位值 |

接收端驗證步驟：

1. 用收到的**原始 body 位元組**計算簽章，不要先解析成 JSON 再重新序列化（位元組會不同，簽章對不上）。
2. 以固定時間比較（例如 Python 的 `hmac.compare_digest`）與 `X-MaiAgent-Signature-256` 比對。
3. `X-MaiAgent-Timestamp` 與接收端目前時間相差超過容許範圍（例如 5 分鐘）就拒絕，避免重放。每次嘗試（含重送）都依送出當下重新計算時間戳與簽章。

完整說明見技術手冊的[驗證簽章與時間戳](https://docs.maiagent.ai/tech/serve/webhook#webhook-signature)。

* 不使用簽章時，也可以在 `headers` 自訂驗證標頭（例如 `Authorization: Bearer <隨機字串>`）。`headers` 的值在查詢 Webhook 設定時會原樣回傳，能查看該對話平台設定的組織成員都看得到；`secretToken` 只能寫入、不會回傳。
* 設定 `secretToken` 時仍會另外送出舊的 `X-Webhook-Secret` 標頭（值為 `secretToken` 原文），供既有接收端相容；新串接請改驗簽章。
* 驗證用的密鑰請用夠長的隨機值，並透過 HTTPS 接收。

## 回應與重試 <a href="#response-and-retry" id="response-and-retry"></a>

* 訊息寫入後才在背景推送，通常數秒內送達；不同訊息各自送出，**不保證到達順序**，請以 `created_at` 排序、以 `id` 去重。
* 接收端請在 **30 秒**內回應 2xx。逾時、連線失敗或非 2xx 都視為失敗。
* **暫時性失敗會自動重送**：逾時、連線失敗、`5xx`、`429` 會在約 1、5、15 分鐘後各重送一次，最多重送 3 次（含第一次共 4 次嘗試）。其他 `4xx`、目的地被安全防護封鎖、自訂範本渲染失敗都不重送。重送的 body 與第一次完全相同，`X-MaiAgent-Delivery-Attempt` 依序為 `2`、`3`、`4`；間隔固定，不讀取回應的 `Retry-After`。重送前 Webhook 已停用或刪除、或訊息已刪除，就不再重送。細節見技術手冊的[自動重送](https://docs.maiagent.ai/tech/serve/webhook#retry)。
* 有了重送，推送是「至少送達一次」：接收端其實已處理、但回應逾時或回了 `5xx`，同一則訊息會再收到一次，請以訊息 `id` 去重。建議先把內容落地再回 2xx、耗時處理放到背景；確定永遠無法處理才回 `4xx`，暫時無法處理（例如下游服務故障）請回 `5xx` 或 `429`。
* 3 次重送都失敗就不再補送，訊息 Webhook **不保證送達**。[偵測新訊息](/api/preparation/detect-new-messages.md#polling)的輪詢做法可以低頻補查**客戶訊息**（Incoming）；AI 或客服的回覆（Outgoing）常在客戶訊息之後才產生，輪詢範例不一定會再讀到，不能靠它補回推送失敗的回覆。要找出推送失敗的訊息，請查下方的傳送紀錄：`statusCode` 不是 2xx 或為空（逾時、連線失敗）的就是失敗的推送，`requestBody` 是當時推送的內容（MaiAgent Standard 格式內有訊息 `id` 與 `conversation.id`），再讀該對話訊息列表的第一頁補上。
* 每次嘗試（含重送）的網址、內容、回應狀態碼都會各留一筆傳送紀錄，可用 `GET /api/v1/chatbots/{chatbot_id}/webhook-records/` 查詢（需具備該 Agent 的編輯權限；`chatbot_id` 為對話平台綁定的 Agent）。注意此 API 的回應同樣轉為 camelCase，實際推送的內容是 snake\_case。
* 推送失敗不影響訊息本身的收發與 AI 回覆。

## 接收端最小範例 <a href="#receiver-example" id="receiver-example"></a>

以下是一支可直接啟動的 Python（Flask）接收端：驗證簽章與時間戳、以訊息 `id` 去重（重送時同一則會再收到）、依 `type` 分流，先回 2xx 再處理。把 `MESSAGE_WEBHOOK_SECRET` 換成你設定的 `secretToken`；若改用自訂標頭驗證，改讀對應的標頭即可。

{% code title="receiver.py" %}

```python
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["MESSAGE_WEBHOOK_SECRET"]
TOLERANCE_SECONDS = 300
processed_ids: set[str] = set()  # 正式環境請改用資料庫或 Redis，並設定保存期限


def is_valid_signature(timestamp: str, signature: str, body: bytes) -> bool:
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    expected = "sha256=" + hmac.new(SECRET.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)


@app.post("/webhooks/maiagent-message")
def message_webhook():
    if not is_valid_signature(
        request.headers.get("X-MaiAgent-Timestamp", ""),
        request.headers.get("X-MaiAgent-Signature-256", ""),
        request.get_data(),
    ):
        abort(401)

    message = request.get_json(force=True)
    if message["id"] in processed_ids:
        return "", 204
    processed_ids.add(message["id"])

    conversation_id = message["conversation"]["id"]
    if message["type"] == "incoming":
        notify_new_message(conversation_id, message["content"])  # 例如顯示「有新訊息」
    elif message["type"] == "outgoing":
        archive_reply(conversation_id, message["content"])

    return "", 204  # 30 秒內回 2xx；逾時或 5xx 會觸發重送，耗時工作請丟到背景


def notify_new_message(conversation_id, content):
    ...


def archive_reply(conversation_id, content):
    ...
```

{% endcode %}

```bash
MESSAGE_WEBHOOK_SECRET=你的密鑰 flask --app receiver run --port 8080
```

本機測試可用下列指令模擬一筆簽章正確的推送：

```bash
BODY='{"id":"acadbd9c-0000-0000-0000-000000000001","type":"incoming","content":"我要改訂單地址","created_at":"2026-10-03T04:58:55.262828+00:00","conversation":{"id":"e6b27044-0000-0000-0000-000000000002","title":"訂單地址修改","type":"individual","status":"open","progress_status":"ai_processing","auto_reply_enabled":true,"assignee":null},"inbox":{"id":"6571672b-0000-0000-0000-000000000003","name":"官網客服","channel_type":"web"},"contact":{"id":"350691e0-0000-0000-0000-000000000004","name":"王小明","email":null,"phone_number":null},"sender":{"id":"350691e0-0000-0000-0000-000000000004","name":"王小明","type":"contact"},"attachments":[],"metadata":{}}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "你的密鑰" | sed 's/^.* //')
curl -X POST http://localhost:8080/webhooks/maiagent-message \
  -H "Content-Type: application/json" \
  -H "X-MaiAgent-Timestamp: $TS" \
  -H "X-MaiAgent-Signature-256: sha256=$SIG" \
  --data-raw "$BODY"
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.maiagent.ai/api/preparation/message-webhook.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
