> 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/tech/serve/webhook.md).

# Webhook

對話平台 Webhook：把訪客訊息與 AI／真人回覆即時推送到您的系統，支援 MaiAgent 標準格式、Genesys Cloud 與自訂 Jinja2 範本

Webhook 設定在\*\*對話平台（Inbox）\*\*上：每當該平台的對話有新訊息建立，MaiAgent 就會把訊息以 HTTP 請求推送到您指定的端點。一個對話平台可以設定多個 Webhook，各自決定要推送哪個方向的訊息、用什麼格式。

設定位置：<mark style="color:blue;">客服對話</mark> → <mark style="color:blue;">對話平台</mark> → 編輯平台 → <mark style="color:blue;">Webhook 設定</mark> → <mark style="color:blue;">新增 Webhook</mark>。API 平台的 <mark style="color:blue;">Webhook 設定</mark> 在左側「串接與登入」群組；LINE 等通訊平台在上方分頁列；網頁與內部平台沒有這個分頁。後台操作見使用者手冊的 [Webhook](https://docs.maiagent.ai/build/conversation-settings/webhook)。

{% hint style="info" %}
透過 [建立對話與訊息](/tech/api-integration/conversations-and-messages.md) API 送出訊息時，回應只代表訊息已建立；**Agent 的回覆是非同步產生的，會以 Outgoing Webhook 推送給您**。這是 API 串接取得回覆的標準方式。
{% endhint %}

## 運作方式 <a href="#how-it-works" id="how-it-works"></a>

```mermaid
sequenceDiagram
    participant C as 您的系統
    participant M as MaiAgent
    participant AI as Agent

    C->>M: POST /api/v1/messages/（訪客訊息）
    M-->>C: 201 已建立（不含回覆）
    M-)C: Incoming Webhook（訪客訊息）
    M->>AI: 產生回覆
    AI-->>M: 回覆訊息建立
    M-)C: Outgoing Webhook（AI 回覆）
```

真人客服在後台回覆時，該則回覆同樣是一則 Outgoing 訊息，也會推送到 Outgoing Webhook；payload 的 `sender.type` 可分辨是 AI 還是真人。

## Webhook 類型 <a href="#directions" id="directions"></a>

| 類型           | 觸發時機                               | 典型用途                                             |
| ------------ | ---------------------------------- | ------------------------------------------------ |
| **Outgoing** | Agent 或真人客服的回覆建立時                  | 取得 AI 回覆、把回覆轉發到您自己的前台或通訊軟體                       |
| **Incoming** | 訪客訊息建立時（含 API、Web Chat、LINE 等所有通道） | 同步對話紀錄到客服系統、資料倉儲、稽核                              |
| **Receive**  | 由外部平台呼叫 MaiAgent                   | 接收外部客服平台送回的訊息（目前支援 Genesys Cloud Open Messaging） |

本頁說明 Outgoing 與 Incoming；Genesys Cloud 的 Receive 與 Genesys Cloud 格式見 [Genesys Cloud 整合](/tech/serve/customer-service-system-integration/genesys-cloud-integration.md)。

## 設定欄位 <a href="#fields" id="fields"></a>

| 欄位                                                                  | 說明                                                                                                                                                                  |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <mark style="color:blue;">名稱</mark>                                 | 自訂識別名稱                                                                                                                                                              |
| <mark style="color:blue;">類型</mark>                                 | Outgoing／Incoming／Receive                                                                                                                                           |
| <mark style="color:blue;">Webhook URL</mark>                        | 接收推送的網址                                                                                                                                                             |
| <mark style="color:blue;">HTTP Method</mark>                        | `POST`（預設）／`PUT`／`PATCH`                                                                                                                                            |
| <mark style="color:blue;">Headers</mark>                            | 附加在每次請求的自訂標頭（例如您系統的 API 金鑰）                                                                                                                                         |
| <mark style="color:blue;">Payload 格式</mark>                         | **MaiAgent 標準格式**／**Genesys Cloud**／**自訂範本**（Jinja2）                                                                                                                |
| <mark style="color:blue;">啟用狀態</mark>                               | 關閉時不推送，設定保留                                                                                                                                                         |
| <mark style="color:blue;">Secret Token</mark>（API 欄位 `secretToken`） | 簽章密鑰。設定後，每次推送都帶 HMAC-SHA256 簽章與時間戳標頭，見[驗證簽章與時間戳](#webhook-signature)。只能寫入，查詢時不回傳；後台編輯時留空代表維持原值。<mark style="color:blue;">Payload 格式</mark>為 Genesys Cloud 時後台不顯示此欄位 |

{% hint style="info" %} <mark style="color:blue;">Headers</mark> 裡若有與 `X-MaiAgent-Signature-256`、`X-MaiAgent-Timestamp`、`X-MaiAgent-Delivery-Attempt` 同名（不分大小寫）的自訂標頭，送出前會被移除，一律以 MaiAgent 產生的值為準。
{% endhint %}

## MaiAgent 標準格式 <a href="#standard-payload" id="standard-payload"></a>

`Content-Type: application/json`，內容如下（Outgoing 與 Incoming 結構相同，以 `type` 區分方向）：

```json
{
  "id": "2491074a-6d23-4289-af45-caa12531420e",
  "type": "outgoing",
  "content": "您好，請問需要什麼協助？",
  "created_at": "2026-09-09T10:15:30.123456+00:00",
  "conversation": {
    "id": "8e44604a-8278-4c96-96b5-3e5a0548d738",
    "title": "詢問退貨流程",
    "type": "individual",
    "status": "open",
    "progress_status": "ai_processing",
    "auto_reply_enabled": true,
    "assignee": null
  },
  "inbox": {
    "id": "1f9c1c1e-4d1a-4b1e-9d0e-7b2f4a5c6d7e",
    "name": "官網客服",
    "channel_type": "web"
  },
  "contact": {
    "id": "c0ffee00-1111-2222-3333-444455556666",
    "name": "王小明",
    "email": null,
    "phone_number": null
  },
  "sender": {
    "id": "5c1d3e2a-0000-4b1e-9d0e-7b2f4a5c6d7e",
    "name": "AI 智能客服",
    "type": "chatbot"
  },
  "attachments": [
    {
      "id": "a1b2c3d4-0000-4b1e-9d0e-7b2f4a5c6d7e",
      "type": "image",
      "filename": "photo.jpg",
      "source_url": "https://example.com/photo.jpg"
    }
  ],
  "metadata": {}
}
```

### 欄位說明 <a href="#standard-fields" id="standard-fields"></a>

| 欄位                                | 類型             | 說明                                                                                                                  |
| --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `id`                              | string         | 訊息 ID（UUID）                                                                                                         |
| `type`                            | string         | `incoming`（訪客訊息）／`outgoing`（AI 或真人回覆）                                                                               |
| `content`                         | string         | 訊息文字                                                                                                                |
| `created_at`                      | string         | ISO 8601 時間                                                                                                         |
| `conversation.id`                 | string         | 對話 ID                                                                                                               |
| `conversation.title`              | string         | 對話標題                                                                                                                |
| `conversation.type`               | string         | `individual`／`group`                                                                                                |
| `conversation.status`             | string         | `open`／`queued`（排隊等候真人）／`resolved`                                                                                  |
| `conversation.progress_status`    | string         | 目前由誰處理：`ai_processing`／`waiting_for_human`／`human_serving`／`resolved`／`open`                                        |
| `conversation.auto_reply_enabled` | boolean        | `false` 表示 AI 已停止自動回覆（轉真人中）                                                                                         |
| `conversation.assignee`           | object \| null | 接手的客服成員 `{ id, name }`；未指派為 `null`                                                                                  |
| `inbox.id` / `inbox.name`         | string         | 對話平台 ID 與名稱                                                                                                         |
| `inbox.channel_type`              | string         | `web`／`line`／`messenger`／`instagram`／`telegram`／`teams`／`email`／`whatsapp`／`slack`／`team_plus`／`line_works`／`viber` |
| `contact`                         | object \| null | 訪客（聯絡人）`{ id, name, email, phone_number }`                                                                          |
| `sender.type`                     | string         | `chatbot`（Agent）／`user`（真人客服）／`contact`（訪客）                                                                         |
| `attachments[]`                   | array          | 附件 `{ id, type, filename, source_url }`；`type` 為 `image`／`video`／`audio`／`sticker`／`other`                          |
| `metadata`                        | object         | 訊息附加資料                                                                                                              |

{% hint style="warning" %}
`attachments[].source_url` 是附件的**來源網址**（外部平台帶入的原始連結）。由 MaiAgent 儲存的檔案（例如 API 上傳、AI 產生的檔案）此欄可能為空；若您需要可下載的網址，請改用自訂範本並取 `attachment.file.url`（見下方範例），或以訊息 ID 呼叫訊息 API 取得附件。
{% endhint %}

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

當您的系統要求特定 JSON 結構時，選擇 <mark style="color:blue;">自訂範本</mark>，以 Jinja2 語法撰寫 payload。範本渲染結果必須是合法 JSON；儲存時會以測試資料驗證一次。

### 可用變數 <a href="#template-variables" id="template-variables"></a>

| 變數             | 內容                                                                                                                               |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `message`      | `id`、`type`、`content`、`created_at`（datetime）、`metadata`                                                                          |
| `conversation` | `id`、`title`、`type`、`status`、`progress_status`、`auto_reply_enabled`、`assignee`（可為 `None`；有值時 `assignee.id`、`assignee.user.name`） |
| `inbox`        | `id`、`name`、`channel_type`                                                                                                       |
| `contact`      | `id`、`name`、`email`、`phone_number`、`source_id`（您以身分同步 API 帶入的 `sourceId`）；可為 `None`                                              |
| `sender`       | `id`、`name`；`sender_type` 為 `chatbot`／`user`／`contact`                                                                           |
| `attachments`  | 附件列表，每筆有 `id`、`type`、`filename`、`source_url`、`file`（`file.url` 為可下載網址）                                                           |
| `now`          | 渲染當下時間（datetime）                                                                                                                 |

字串請一律經過 `| tojson` 過濾器輸出，才會正確跳脫引號與換行。

### 範例：回傳您系統的使用者 ID 與可下載附件網址 <a href="#template-example" id="template-example"></a>

```jinja
{
  "external_user_id": {{ (contact.source_id if contact else "") | tojson }},
  "conversation_id": "{{ conversation.id }}",
  "direction": "{{ message.type }}",
  "from_human_agent": {{ (sender_type == "user") | tojson }},
  "text": {{ message.content | tojson }},
  "attachments": [{% for a in attachments %}{
    "type": "{{ a.type }}",
    "url": {{ (a.file.url if a.file else a.source_url) | tojson }}
  }{% if not loop.last %},{% endif %}{% endfor %}],
  "sent_at": "{{ message.created_at.isoformat() }}"
}
```

## 驗證簽章與時間戳 <a href="#webhook-signature" id="webhook-signature"></a>

Webhook 設定了 Secret Token 時，每次推送都帶下列標頭，接收端可據此確認請求來自 MaiAgent、內容未被竄改，並拒絕過期的重放請求：

| 標頭                            | 內容                                                                                       |
| ----------------------------- | ---------------------------------------------------------------------------------------- |
| `X-MaiAgent-Timestamp`        | 這次送出當下的 Unix 時間（秒）                                                                       |
| `X-MaiAgent-Signature-256`    | `sha256=<hex>`：以 Secret Token 為金鑰，對「`時間戳` + `.` + 原始 request body」計算的 HMAC-SHA256，十六進位小寫 |
| `X-MaiAgent-Delivery-Attempt` | 第幾次嘗試，第一次為 `1`（不論有無設定 Secret Token 都會帶）                                                  |
| `X-Webhook-Secret`            | Secret Token 原文。為了相容只比對這個值的既有接收端暫時照送，已不建議使用，請改驗簽章                                        |

### 驗證步驟 <a href="#verify-steps" id="verify-steps"></a>

1. 讀取**原始 request body 的位元組**，不要先解析 JSON 再序列化。MaiAgent 送出的是緊湊（無空白）、UTF-8 編碼、中文不轉義的 JSON，重新序列化後的位元組通常不同，簽章會對不上。
2. 取 `X-MaiAgent-Timestamp`，與接收端目前時間相比，超出您設定的容許範圍（例如 5 分鐘）就拒絕。每次嘗試（含重送）都以送出當下的時間重新計算時間戳與簽章，重送不會因時間戳過舊而被拒。
3. 以 Secret Token 計算 `HMAC-SHA256(secret, f"{timestamp}.".encode() + raw_body)`，轉成十六進位後加上 `sha256=` 前綴。
4. 以固定時間比較（例如 Python 的 `hmac.compare_digest`）比對 `X-MaiAgent-Signature-256`，不相符就回 `401`。

```python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300  # 自訂的時間戳容許範圍


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

{% hint style="warning" %}
轉真人 Webhook 的簽章標頭也叫 `X-MaiAgent-Signature-256`，但只對 request body 簽章、不含時間戳。同一支接收端同時處理兩者時，請依路徑分開驗證，見[第三方客服系統串接（含轉真人）](/tech/serve/customer-service-system-integration.md)。
{% endhint %}

## 傳送行為 <a href="#delivery" id="delivery"></a>

* 每則訊息、每個啟用中的 Webhook 各推送一次；請求逾時 **30 秒**。
* 回應 **2xx** 視為成功。
* 端點 URL 會經過 SSRF 防護：私有網段、內網位址的目的地會被拒絕。
* 每次嘗試（含重送）的請求內容、回應與狀態碼各留一筆紀錄，可在 Agent 頁面的 <mark style="color:blue;">Webhook</mark> 分頁檢視，用於排查。

### 自動重送 <a href="#retry" id="retry"></a>

| 每次嘗試的結果                                           | 處理方式                                                    |
| ------------------------------------------------- | ------------------------------------------------------- |
| 逾時、連線失敗、`5xx`、`429`                               | 視為暫時性失敗，約 **1、5、15 分鐘**後各重送一次，**最多重送 3 次**（含第一次共 4 次嘗試） |
| 其他 `4xx`（例如 `400`、`401`、`404`）                    | 記錄為失敗，不重送                                               |
| 目的地被 SSRF 防護封鎖（網域名稱解析失敗也算，包含暫時性的 DNS 失敗）、自訂範本渲染失敗 | 記錄為失敗，不重送                                               |

* 重送的 request body 與第一次**位元組完全相同**；`X-MaiAgent-Delivery-Attempt` 依序為 `2`、`3`、`4`，時間戳與簽章依送出當下重新計算。
* URL、自訂標頭與 Secret Token 取重送當下的 Webhook 設定；重送前 Webhook 已停用或刪除、或訊息已刪除，就不再重送。
* 3 次重送都失敗後即放棄，不會再補送。需要補資料時，可呼叫 `GET /api/v1/messages/?conversation={conversationId}` 查詢該對話的訊息（`conversation` 參數必填，見[對話與訊息回覆（串流/同步）](/tech/api-integration/dui-hua-yu-xun-xi-hui-fu-chuan-liu-tong-bu.md)）。
* 重送間隔固定，不讀取回應的 `Retry-After` 標頭。
* Payload 格式為 Genesys Cloud 時，收到 `401` 會先更新 access token 並立即重送一次，不占上述 3 次重送額度；這次重送若遇到暫時性失敗，照上表排程重送，仍是 `401` 則停止。

### 冪等處理建議 <a href="#idempotency" id="idempotency"></a>

有了重送，推送語意是「至少送達一次」：您的端點其實已處理、但回應逾時或回了 `5xx`，同一則訊息會再收到一次。建議：

* 以 payload 的訊息 `id` 去重，已處理過的 `id` 直接回 `2xx`。使用自訂範本時，請在範本裡保留 `message.id`，否則無法去重。
* 去重請用訊息 `id`，不要用 `X-MaiAgent-Timestamp` 或簽章：兩者每次嘗試都會變。
* 先把收到的內容落地（寫入資料庫或佇列）再回 `2xx`，耗時的處理改為非同步，避免超過 30 秒逾時而觸發不必要的重送。
* 只有確定這則請求永遠無法處理時才回 `4xx`（不會重送）；暫時無法處理（例如下游服務故障）請回 `5xx` 或 `429`，讓 MaiAgent 稍後重送。

## 以 API 管理 Webhook <a href="#manage-by-api" id="manage-by-api"></a>

除了後台介面，也可以用 API 建立與維護 Webhook（欄位對應後台設定）。認證方式見 [快速上手](/tech/api-integration/quickstart.md)。

```bash
# 列出 / 新增
GET  https://api.maiagent.ai/api/v1/inboxes/{inboxId}/webhooks/
POST https://api.maiagent.ai/api/v1/inboxes/{inboxId}/webhooks/

# 修改 / 刪除
PATCH  https://api.maiagent.ai/api/v1/inboxes/{inboxId}/webhooks/{webhookId}/
DELETE https://api.maiagent.ai/api/v1/inboxes/{inboxId}/webhooks/{webhookId}/
```

```json
{
  "name": "回覆同步到客服系統",
  "direction": "outgoing",
  "isEnabled": true,
  "url": "https://your-system.example.com/maiagent/reply",
  "method": "POST",
  "headers": { "X-Api-Key": "your-key" },
  "templatePreset": "maiagent_standard",
  "customTemplate": null,
  "secretToken": "shared-secret"
}
```

`inboxId` 可從 [建立對話](/tech/api-integration/conversations-and-messages.md) 回應的 `inbox.id` 取得，或於後台對話平台設定頁查看。

## 與「轉真人 Webhook」的差別 <a href="#vs-handoff-webhook" id="vs-handoff-webhook"></a>

本頁的 Webhook 以**訊息**為單位推送。若您的系統需要知道對話**何時轉給真人、何時回到 AI**（例如由您的客服系統接手服務），請使用另一組以對話生命週期為單位的轉真人 Webhook，見 [第三方客服系統串接（含轉真人）](/tech/serve/customer-service-system-integration.md)。


---

# 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/tech/serve/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.
