> For the complete documentation index, see [llms.txt](https://help.firstline.cc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.firstline.cc/developer/api/jin-jie/headless-conversation-api.md).

# Headless Conversation API

使用 Headless Conversation API，從企業自有系統傳送客戶訊息、接收回覆及對話事件。

{% hint style="info" %}
本 API 用於整合「自訂管道」，目前為 **Beta** 功能。正式使用前，請先於 FIRST LINE 完成管道建立、接待者設定及啟用。功能與介面可能依版本調整。
{% endhint %}

**Headless Conversation API** 讓企業以自己的網站、App 或客戶服務介面與 FIRST LINE 串接。企業負責前端介面與使用者身分驗證，FIRST LINE 依管道設定處理訊息，並透過事件回傳 AI 代理、交談腳本或真人客服的回覆。

若尚未建立管道，請先閱讀 [自訂管道設定教學](/feature/channels/headless-api.md)。

## 快速開始

1. 在 FIRST LINE 建立自訂管道，妥善保存 **Client ID**、**Client Secret** 及 **Webhook Secret**。
2. 選擇接待者（Fancy AI 代理或交談腳本），完成必要設定並啟用管道。
3. 由企業後端透過 OAuth 2.0 **Client Credentials** 取得 Access Token。
4. 呼叫傳送訊息 API。
5. 以 **Webhook** 或 **事件查詢（Polling）** 取得回覆，再顯示於自有介面。

{% hint style="warning" %}
Client Secret 和 Webhook Secret 僅於建立或重新產生時顯示，請保存在可信任的後端環境，不要寫入網站前端、App 原始碼或公開儲存庫。
{% endhint %}

## 1. 身分驗證

Headless Conversation API 使用 OAuth 2.0 Client Credentials 授權，所需 Scope 為 `headless:use`。請使用 FIRST LINE 所屬租戶提供的 API 網域，並以管道建立時取得的 Client ID 與 Client Secret 申請 Access Token。

請以 **POST JSON Body** 傳送 Client Credentials。不要將 `client_id`、`client_secret`、`scope` 或其他識別／憑證資料組合在網址或 Query String 中；部分環境在處理 POST 請求時可能無法正確讀取網址上的參數，且敏感資料也可能被保留在伺服器、代理或瀏覽器的網址紀錄中。

請求範例：

```http
POST https://YOUR_TENANT_HOST/oauth/token
Accept: application/json
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "scope": "headless:use"
}
```

cURL 範例：

```bash
curl -X POST "https://YOUR_TENANT_HOST/oauth/token" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "scope": "headless:use"
  }'
```

{% hint style="warning" %}
請勿使用下列方式傳送憑證：

```
POST /oauth/token?client_id=...&client_secret=...&scope=...
```

Client ID、Client Secret 與 Scope 應放在 **JSON Request Body** 中傳送，不要附加在網址後方。
{% endhint %}

{% hint style="info" %}
正式串接時，請確認所使用租戶的 Token Endpoint 與憑證發放方式；本頁的 `YOUR_TENANT_HOST` 均為示意網域。
{% endhint %}

完成授權後，於 API 請求中帶入：

```http
Authorization: Bearer YOUR_ACCESS_TOKEN
Accept: application/json
Content-Type: application/json
```

**注意：** Headless Conversation API 的 Client Credentials 與一般 FIRST LINE 員工 API Token 不同，請勿混用。

## 2. 傳送客戶訊息

<mark style="color:green;">POST</mark> `/api/headless/v1/messages`

### 傳送文字

```bash
curl -X POST "https://YOUR_TENANT_HOST/api/headless/v1/messages" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "external_user_id": "customer_123",
    "client_message_id": "msg_0001",
    "message": {
      "type": "text",
      "text": "請問退貨流程如何辦理？"
    },
    "profile": {
      "name": "測試客戶"
    }
  }'
```

### Request Body

| 欄位                  | 必填         | 說明                                        |
| ------------------- | ---------- | ----------------------------------------- |
| `external_user_id`  | 是          | 企業系統的客戶識別值，最多 191 字元。同一管道內同一客戶請持續使用相同識別值。 |
| `client_message_id` | 否（建議提供）    | 此次訊息的唯一識別值，最多 191 字元，用於安全重送。              |
| `message.type`      | 是          | `text` 或 `postback`。                      |
| `message.text`      | 文字訊息時      | 訊息內容，最多 1,000 字元。                         |
| `message.value`     | Postback 時 | 客戶點選選項後回傳的值，最多 1,000 字元。                  |
| `profile.name`      | 否          | 客戶顯示名稱，最多 50 字元。                          |
| `context`           | 否          | 附加情境資料，詳見下方說明。                            |

Postback 範例：

```json
{
  "external_user_id": "customer_123",
  "client_message_id": "msg_0002",
  "message": {
    "type": "postback",
    "value": "check_order"
  }
}
```

### 成功回應

HTTP `202 Accepted`：

```json
{
  "request_id": "9bfb0e4f-8b67-4f6d-9d34-44e8b3ac6ae2",
  "conversation_id": "CONVERSATION_ID",
  "sequence": 1
}
```

| 欄位                | 說明                    |
| ----------------- | --------------------- |
| `request_id`      | 本次訊息的請求識別值，可用來對應後續事件。 |
| `conversation_id` | 對話識別值；查詢事件時使用。        |
| `sequence`        | 事件序號，可協助處理事件順序。       |

{% hint style="info" %}
**HTTP 202 不表示 AI 已完成回答。** FIRST LINE 接受訊息後，請透過 Webhook 或事件查詢取得後續的訊息及處理結果。
{% endhint %}

### 避免重複送出

建議每筆新訊息都提供唯一的 `client_message_id`。當網路逾時而不確定訊息是否已被接收時，請以**相同識別值及相同內容**重試。

若原訊息仍在處理中，可能收到 `409 Conflict`；若同一識別值被用於不同訊息內容，則會收到 `422 Unprocessable Entity`。不要為了重試而產生新的訊息 ID，否則可能造成重複發送。

## 3. 查詢對話事件

<mark style="color:blue;">GET</mark> `/api/headless/v1/conversations/{conversation_id}/events`

可利用事件查詢 API 取得指定對話的新回覆與狀態通知：

```bash
curl "https://YOUR_TENANT_HOST/api/headless/v1/conversations/CONVERSATION_ID/events?after_sequence=0" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Accept: application/json"
```

| 參數                | 說明                      |
| ----------------- | ----------------------- |
| `conversation_id` | 傳送訊息 API 回傳的對話識別值。      |
| `after_sequence`  | 選用；只取得序號大於指定值的事件，預設為 0。 |

回應中的 `data` 為事件清單，可能為空陣列：

```json
{
  "data": [
    {
      "event_id": "evt_1201",
      "type": "message.created",
      "conversation_id": "CONVERSATION_ID",
      "external_user_id": "customer_123",
      "sequence": 2,
      "request_id": "9bfb0e4f-8b67-4f6d-9d34-44e8b3ac6ae2",
      "occurred_at": "2026-10-14T13:30:00+08:00",
      "data": {
        "message_id": 8001,
        "sender": "bot",
        "message": {
          "type": "text",
          "content": "您好，請問需要什麼協助？"
        }
      }
    }
  ]
}
```

*以上為示意資料，非實際客戶或對話。*

請依 `sequence` 順序處理事件，並儲存最後成功處理的序號，供下一次查詢使用。事件可能因訂閱設定而略過某些序號，**不需要等待連續數字**。Polling 會立即回傳目前可取得的事件，不會持續等待新事件。

## 4. 事件類型

Webhook 與事件查詢使用相同的單筆事件資料格式。

| 事件                                  | 說明            |
| ----------------------------------- | ------------- |
| `message.received`                  | 收到客戶訊息。       |
| `message.created`                   | 產生要傳送給客戶的訊息。  |
| `turn.completed`                    | 本輪客戶訊息的處理已完成。 |
| `conversation.transferred_to_human` | 對話轉交真人客服流程。   |
| `conversation.ended`                | 對話已結束。        |
| `tool.invoking`                     | AI 工具開始執行。    |
| `tool.completed`                    | AI 工具完成執行。    |

可於管道的 Webhook 設定選擇要接收的事件。預設包含 `message.created`、`turn.completed`、`conversation.transferred_to_human` 與 `conversation.ended`；**事件查詢也依相同的訂閱清單過濾**。

### 訊息事件

`message.created` 的 `data` 範例：

```json
{
  "message_id": 8001,
  "sender": "bot",
  "message": {
    "type": "text",
    "content": "您好，請問有什麼可以協助您？"
  }
}
```

`sender` 可能為：

* `bot`：自動回覆，包括 AI 代理、交談腳本或系統訊息。
* `human`：真人客服發出的訊息。

訊息類型可包含 `text`、`image`、`quickreply`、`button`、`carousel`；`content` 的內容會因類型而異。請依 `message.type` 呈現，並為無法辨識的類型提供適當降級顯示。

### 回合完成事件

`turn.completed` 的 `data` 可能包含：

```json
{
  "in_reply_to": [
    "9bfb0e4f-8b67-4f6d-9d34-44e8b3ac6ae2"
  ],
  "unfinished_tool_call_ids": []
}
```

`in_reply_to` 是本輪已處理的請求 ID 清單。請注意：**回合完成不等於整段對話已結束**。如果要判斷對話已正式結束，應參考 `conversation.ended` 等事件。

## 5. 接收 Webhook

在 FIRST LINE 的自訂管道設定中，填寫可公開存取的 **HTTPS** Webhook URL，並選擇需要訂閱的事件。FIRST LINE 會以 HTTP POST 傳送事件 JSON。

Webhook 會帶入下列 Headers：

| Header                  | 說明                       |
| ----------------------- | ------------------------ |
| `X-FirstLine-Event`     | 事件類型。                    |
| `X-FirstLine-Event-Id`  | 事件識別值。                   |
| `X-FirstLine-Timestamp` | Unix Timestamp（秒）。       |
| `X-FirstLine-Signature` | Webhook HMAC SHA-256 簽章。 |

### 驗證 Webhook 簽章

為確認請求來自預期來源，請使用管道的 **Webhook Secret** 驗證簽章：

```
signed_content = timestamp + "." + raw_request_body
expected_signature = "sha256=" + HMAC_SHA256(webhook_secret, signed_content).hex()
```

必須使用收到的**原始 HTTP Request Body** 計算，不可先將 JSON 解碼後重新編碼。

Node.js 驗證計算範例：

```javascript
import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyWebhook(rawBody, timestamp, signature, secret) {
  if (!/^\d+$/.test(timestamp) || !/^sha256=[0-9a-f]{64}$/.test(signature)) {
    return false
  }

  // 建議檢查時間差，降低重播請求風險
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false
  }

  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest('hex')

  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}
```

驗證完成後，請以 `event_id` 進行去重，再由企業系統處理事件。接收端應盡快回傳 2xx，避免執行耗時工作造成逾時。Webhook 可能重送，**請勿依賴通知僅送達一次或一定按序到達**。

### 推送失敗時

Webhook 若發生暫時性連線問題，或接收端回覆 `408`、`429`、`5xx`，系統可能自動重試。其他 `4xx` 通常不會重試。建議整合端保留事件處理紀錄，並在必要時使用事件查詢 API 核對。

{% hint style="warning" %}
重新產生 Webhook Secret 後，舊密鑰將失效。請同步更新企業端的驗簽設定，以免無法接收後續通知。
{% endhint %}

## 6. 情境資料 Context（選用）

傳送訊息時，可提供與本次服務相關的輔助資料，例如：

```json
{
  "context": {
    "membership_level": "gold",
    "preferred_language": "zh_tw"
  }
}
```

限制如下：

* 最多 20 組鍵值。
* Key 限小寫英文字母、數字與底線，長度 1～40 字元。
* Value 必須是字串，最多 500 字元。
* 超過限制將回傳 `422`，而不是截斷內容。

Context 可供服務流程及 AI 代理參考，但不應用來傳送密碼、存取權杖或其他不必要的敏感資訊。企業應先驗證資料來源，避免客戶自行偽造會員身分或服務權限。

## 7. 常見錯誤與處理方式

| 狀態碼   | 情況           | 建議處理                              |
| ----- | ------------ | --------------------------------- |
| `200` | 事件查詢成功。      | 依序處理取得的事件。                        |
| `202` | 客戶訊息已接收。     | 等待 Webhook 或查詢後續事件。               |
| `401` | 存取權杖或管道狀態無效。 | 檢查 Token、Client 憑證及管道啟用狀態。        |
| `403` | 存取權限不足。      | 確認 Token 具備 `headless:use` Scope。 |
| `404` | 找不到可存取的對話。   | 確認 `conversation_id` 及管道。         |
| `409` | 相同訊息仍在處理。    | 稍後以相同 ID 與內容重試。                   |
| `422` | 欄位格式不符。      | 修正請求資料。                           |
| `429` | 請求過於頻繁。      | 降低頻率並採用退避重試。                      |
| `5xx` | 服務暫時異常。      | 記錄錯誤，以相同訊息 ID 有限次重試。              |

{% hint style="info" %}
事件資料有保存期限，長期留存需求請由企業依自身政策規劃。需要長時間追蹤對話時，建議妥善保存已收到的事件及處理紀錄。
{% endhint %}

## 上線前檢查

* 已建立並啟用自訂管道，且接待者設定正確。
* Access Token 可成功授權，憑證只儲存於後端。
* 可傳送文字訊息並收到 HTTP 202。
* 可透過 Webhook 或事件查詢收到回覆。
* 已處理事件去重、訊息重試與非同步回覆。
* 已測試真人轉交、對話結束及常見錯誤情況。

**完成上述步驟後，即可將 FIRST LINE 的對話能力整合至企業自己的服務介面。**
