> ## Documentation Index
> Fetch the complete documentation index at: https://docs-agents.fpt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Nhận câu trả lời (callback)

> Kiểm chứng webhook, cấu trúc sự kiện callback, các khối nội dung và cơ chế gửi lại

Câu trả lời của Agent được gửi tới webhook của bạn bằng một lệnh `POST` đã ký, theo đúng [cách ký](/tich-hop/live-chat-api/kenh-api/xac-thuc-chu-ky) của chiều gửi lên.

```mermaid theme={null}
sequenceDiagram
  participant D as Message Dispatching
  participant Y as API Webhook của bạn
  D->>Y: POST callback_url (nội dung sự kiện + X-Hub-Signature-256)
  Note over Y: Kiểm tra chữ ký
  Y->>Y: Xác thực HMAC-SHA256
  Y->>Y: Chống trùng theo eventId
  Y->>Y: Gửi trả lời tới người dùng
  Y-->>D: 200 OK
  Note over D,Y: Không phải 2xx thì nền tảng thử lại tối đa 3 lần (1s, 3s, 9s)
```

## Kiểm chứng điểm cuối webhook

Trước khi tin nhắn thật đầu tiên tới, nút **Kiểm tra callback** trên Console gửi một yêu cầu đã ký để xác nhận điểm cuối của bạn hoạt động đúng.

```mermaid theme={null}
sequenceDiagram
  participant P as FPT AI Agents Platform
  participant Y as Webhook Server của bạn
  P->>Y: POST callback_url  type = verification, challenge
  Y->>Y: Xác thực chữ ký X-Hub-Signature-256
  Y-->>P: 200 OK + challenge
  P->>P: Đối chiếu challenge
  Note over P: Khớp: ok. Không khớp: challenge_mismatch
```

```json Yêu cầu kiểm chứng theme={null}
{
  "type": "verification",
  "challenge": "6326e43c9f0b4a1d8e77c0b511a2d3f4",
  "occurredAt": "2026-09-07T11:08:40Z"
}
```

Để vượt qua, hãy trả về mã 2xx kèm phần thân JSON lặp lại đúng giá trị `challenge`:

```javascript theme={null}
app.post('/agent-replies', (req, res) => {
  // ... xác thực chữ ký ...
  const event = JSON.parse(raw.toString('utf8'));

  if (event.type === 'verification') {
    return res.json({ challenge: event.challenge });
  }

  res.sendStatus(200);
  handleAsync(event);
});
```

* Yêu cầu kiểm chứng được ký bằng **đúng cách ký** như mọi gói tin khác.
* Trường `type` **chỉ xuất hiện trên yêu cầu kiểm chứng**, không bao giờ có trên tin nhắn thật.
* **Bắt buộc phải lặp lại `challenge`.** Trả 200 suông là chưa đủ, vì một tên miền bỏ trống hay trang báo lỗi của CDN cũng trả về 200.
* Mỗi lần kiểm chứng dùng một `challenge` khác nhau.
* Chúng tôi **không đi theo chuyển hướng** 301/302/303; hãy trỏ thẳng tới địa chỉ cuối cùng.
* Kết quả đạt chỉ có nghĩa điểm cuối trả lời đúng **tại thời điểm đó**, không phải một chứng nhận được lưu lại.

| Lý do thất bại | Ý nghĩa và cách xử lý |
| - | - |
| `no_secret` | Chưa có khóa ký nên chúng tôi không ký được, **không có yêu cầu nào được gửi đi**. Hãy tạo khóa trước. |
| `url_forbidden` | Không phải URL https tuyệt đối, hoặc phân giải ra địa chỉ không công khai. |
| `unreachable` | Không kết nối được. Dịch vụ đã chạy và mở ra Internet chưa? |
| `http_status` | Điểm cuối trả về mã ngoài 2xx; mã cụ thể nằm ở trường `httpStatus`. |
| `challenge_mismatch` | Trả về 2xx nhưng không lặp lại đúng `challenge`. |

## Cấu trúc sự kiện callback

```http theme={null}
POST <callback_url>
Content-Type: application/json
User-Agent: fpt-agent-platform-channel-gateway/1
X-Hub-Timestamp: 1755763260
X-Hub-Signature-256: sha256=<chữ ký>
```

| Trường | Kiểu | Mô tả |
| - | - | - |
| `eventId` | string | ID sự kiện, **ổn định qua các lần gửi lại**. Dùng làm khóa chống trùng. |
| `integrationKey` | string | Khóa định tuyến của kênh. |
| `conversationId` | string | ID cuộc hội thoại, dùng cho [API đọc lịch sử](/tich-hop/live-chat-api/kenh-api/lich-su-hoi-thoai). **Hãy lưu lại giá trị này.** |
| `runId` | string | ID của một lượt trả lời. Mỗi lượt một giá trị. |
| `from` | `bot` hoặc `operator` | Lượt nói do Agent trả lời hay do nhân viên của bạn tiếp quản. |
| `user.id` | string | ID khách hàng, đúng giá trị bạn đã gửi lên. |
| `text` | string | Nội dung văn bản. **Luôn có mặt**, kể cả khi có nút bấm hay carousel. |
| `buttons` | `Array<Button>` | Tùy chọn: danh sách nút bấm. |
| `quick_replies` | `Array<QuickReply>` | Tùy chọn: danh sách quick reply. |
| `carousels` | `Array<Carousel>` | Tùy chọn: danh sách thẻ carousel. |
| `references` | `Array<Reference>` | Tùy chọn: các nguồn mà câu trả lời đã trích dẫn. |
| `attachments` | `Array<Attachment>` | Tùy chọn, giữ chỗ cho tương lai. Xem lưu ý bên dưới. |
| `occurredAt` | string | Thời điểm phát sinh, định dạng RFC 3339 UTC. |

<Note>
  Các trường tầng ngoài dùng kiểu lowerCamelCase (`eventId`, `conversationId`), còn các khối nội dung bên trong dùng snake\_case (`quick_replies`, `sub_title`, `image_url`, `file_name`). Đây là chủ ý: các khối nội dung dùng chung bộ từ vựng với widget chat và lịch sử hội thoại.
</Note>

### Các khối nội dung

<AccordionGroup>
  <Accordion title="Button" icon="hand-pointer">
    | Trường | Kiểu | Mô tả |
    | - | - | - |
    | `type` | `postback`, `url`, `phone_call`, `webview` | Loại nút |
    | `title` | string | Tiêu đề nút |
    | `data` | string | Tùy chọn: dữ liệu ẩn khi `type` là `postback`. Gửi lại giá trị này trong trường `postback` khi khách bấm nút. |
    | `url` | string | Tùy chọn: đường dẫn khi `type` là `url` hoặc `webview` |
    | `phone` | string | Tùy chọn: số điện thoại khi `type` là `phone_call` |
  </Accordion>

  <Accordion title="QuickReply" icon="reply">
    | Trường | Kiểu | Mô tả |
    | - | - | - |
    | `type` | `text`, `phone_call` | Loại quick reply |
    | `title` | string | Tiêu đề quick reply |
    | `data` | string | Tùy chọn: dữ liệu ẩn khi `type` là `text` |
    | `phone` | string | Tùy chọn: số điện thoại khi `type` là `phone_call` |
  </Accordion>

  <Accordion title="Carousel" icon="images">
    | Trường | Kiểu | Mô tả |
    | - | - | - |
    | `title` | string | Tiêu đề carousel |
    | `sub_title` | string | Tùy chọn: tiêu đề con |
    | `image_url` | string | Tùy chọn: đường dẫn ảnh |
    | `buttons` | `Array<Button>` | Tùy chọn: danh sách nút |

    Ảnh kèm nút bấm được gửi dưới dạng carousel có 1 phần tử.
  </Accordion>

  <Accordion title="Reference" icon="quote-left">
    | Trường | Kiểu | Mô tả |
    | - | - | - |
    | `id` | string | Định danh của nguồn trong lượt trả lời này |
    | `type` | `knowledge`, `web_search` | Nguồn từ kho tri thức hay từ tìm kiếm web. Giá trị lạ vẫn hiển thị được như một thẻ thường. |
    | `title` | string | Tùy chọn: tiêu đề nguồn |
    | `url` | string | Tùy chọn: đường dẫn, với nguồn từ web |
    | `file_name` | string | Tùy chọn: tên tệp, với nguồn từ kho tri thức |
    | `uri` | string | Tùy chọn: định danh tài liệu trong kho tri thức |
    | `page` | number | Tùy chọn: số trang trong tài liệu |
    | `metadata` | object | Tùy chọn: dữ liệu riêng của từng loại nguồn (tên miền và favicon với nguồn web; vị trí trong trang với nguồn tri thức) |
  </Accordion>

  <Accordion title="Attachment" icon="paperclip">
    | Trường | Kiểu | Mô tả |
    | - | - | - |
    | `name` | string | Tên tệp |
    | `mime` | string | Kiểu nội dung |
    | `size` | number | Kích thước, tính bằng byte |
    | `objectKey` | string | Khóa đối tượng, **không phải đường dẫn tải**. Đổi lấy đường dẫn ngắn hạn qua [API tải tệp do Agent tạo ra](/tich-hop/live-chat-api/live-chat/visitor-api). |

    Hiện chưa có tính năng nào đặt tệp lên lượt trả lời của kênh API, nên trường `attachments` **chưa xuất hiện** trên callback. Bạn chưa cần viết nhánh xử lý cho nó.
  </Accordion>
</AccordionGroup>

### Ví dụ nội dung sự kiện

<CodeGroup>
  ```json Text theme={null}
  {
    "eventId": "out:run-8891:0",
    "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
    "conversationId": "7412",
    "runId": "run-8891",
    "from": "bot",
    "user": { "id": "cust-42" },
    "text": "Đơn hàng của bạn sẽ được giao vào ngày mai.",
    "occurredAt": "2026-09-14T09:14:20Z"
  }
  ```

  ```json Text có nút theme={null}
  {
    "eventId": "out:run-8892:0",
    "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
    "conversationId": "7412",
    "runId": "run-8892",
    "from": "bot",
    "user": { "id": "cust-42" },
    "text": "Bạn muốn làm gì tiếp theo?",
    "buttons": [
      { "type": "postback", "title": "Theo dõi đơn", "data": "track:SO-7781" },
      { "type": "phone_call", "title": "Gọi điện", "phone": "+84982123456" },
      { "type": "url", "title": "Website", "url": "https://example.com" },
      { "type": "webview", "title": "Chi tiết", "url": "https://example.com/d" }
    ],
    "occurredAt": "2026-09-14T09:15:02Z"
  }
  ```

  ```json Quick reply theme={null}
  {
    "eventId": "out:run-8893:0",
    "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
    "conversationId": "7412",
    "runId": "run-8893",
    "from": "bot",
    "user": { "id": "cust-42" },
    "text": "Bạn cần hỗ trợ thêm gì không?",
    "quick_replies": [
      { "type": "text", "title": "Đổi trả", "data": "intent:return" },
      { "type": "phone_call", "title": "Gọi điện", "phone": "+84982123456" }
    ],
    "occurredAt": "2026-09-14T09:16:30Z"
  }
  ```

  ```json Carousel theme={null}
  {
    "eventId": "out:run-8894:0",
    "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
    "conversationId": "7412",
    "runId": "run-8894",
    "from": "bot",
    "user": { "id": "cust-42" },
    "text": "Đây là các sản phẩm phù hợp với bạn.",
    "carousels": [
      {
        "title": "Nón bảo hiểm A",
        "sub_title": "Đạt chuẩn QCVN 2:2008",
        "image_url": "https://example.com/img/non-a.jpg",
        "buttons": [
          { "type": "postback", "title": "Đặt mua", "data": "buy:NON-A" },
          { "type": "url", "title": "Chi tiết", "url": "https://example.com/non-a" }
        ]
      },
      {
        "title": "Nón bảo hiểm B",
        "sub_title": "Có kính chắn gió",
        "image_url": "https://example.com/img/non-b.jpg",
        "buttons": [
          { "type": "postback", "title": "Đặt mua", "data": "buy:NON-B" }
        ]
      }
    ],
    "occurredAt": "2026-09-14T09:18:11Z"
  }
  ```

  ```json Trích dẫn nguồn theme={null}
  {
    "eventId": "out:run-8895:0",
    "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
    "conversationId": "7412",
    "runId": "run-8895",
    "from": "bot",
    "user": { "id": "cust-42" },
    "text": "Chính sách đổi trả áp dụng trong 30 ngày kể từ ngày nhận hàng.",
    "references": [
      { "id": "r1", "type": "knowledge", "title": "Chính sách đổi trả", "file_name": "chinh-sach-2026.pdf", "page": 4 },
      { "id": "r2", "type": "web_search", "title": "Quy định bảo hành", "url": "https://example.com/bao-hanh" }
    ],
    "occurredAt": "2026-09-14T09:20:45Z"
  }
  ```

  ```json Nhân viên tiếp quản theme={null}
  {
    "eventId": "out:run-8896:0",
    "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
    "conversationId": "7412",
    "runId": "run-8896",
    "from": "operator",
    "user": { "id": "cust-42" },
    "text": "Chào anh, em là Lan từ bộ phận CSKH, em sẽ hỗ trợ anh ngay ạ.",
    "occurredAt": "2026-09-14T09:41:00Z"
  }
  ```
</CodeGroup>

### Lưu ý khi xử lý callback

* **`text` luôn có mặt**, kể cả khi có `buttons` hay `carousels`. Đó là phương án hiển thị dự phòng cho hệ thống chỉ hiển thị văn bản.
* **Danh sách rỗng thì vắng mặt, không phải `[]`.** `buttons`, `quick_replies`, `carousels` và `references` bị lược bỏ khi lượt trả lời không có.
* **`conversationId` và `from` có thể vắng mặt** trên callback từ phiên bản cũ của nền tảng; hãy hiểu sự vắng mặt là chưa xác định.
* **Chống trùng theo `eventId`**, không phải `runId`.
* **Nối dữ liệu theo `conversationId`**, đừng nối theo `runId`.
* **Trả `200` ngay rồi xử lý bất đồng bộ.** Mỗi lần gọi chỉ có 10 giây; xử lý chậm sẽ bị tính là thất bại và kích hoạt gửi lại.

## Khi điểm cuối của bạn lỗi

| Phản hồi của bạn | Hành vi của nền tảng |
| - | - |
| 2xx | Coi như đã gửi thành công. |
| 5xx, 429, 408, hết thời gian chờ, lỗi kết nối | **Gửi lại tối đa 3 lần** với thời gian chờ tăng dần (khoảng 1s, 3s, 9s). Header `Retry-After` được tôn trọng, tối đa 30 giây. |
| Các mã 4xx khác, hoặc chuyển hướng | **Không gửi lại.** Coi như bị từ chối vĩnh viễn. |
| Vẫn lỗi sau 3 lần | Chuyển vào hàng đợi lỗi ở phía chúng tôi. |

<Warning>
  Khi điểm cuối của bạn gián đoạn kéo dài, cơ chế giao nhận là **nhiều nhất một lần**, và hàng đợi lỗi **không tự động phát lại** cho bạn. Đường phục hồi là [API đọc lại lịch sử hội thoại](/tich-hop/live-chat-api/kenh-api/lich-su-hoi-thoai).
</Warning>

## Yêu cầu với Callback URL

* Giao thức `https` (`http` chỉ dùng được trong môi trường phát triển nội bộ).
* **Không nhúng thông tin đăng nhập trong URL**; bạn xác thực chúng tôi bằng chữ ký.
* Phải phân giải ra **địa chỉ công khai trên Internet**. Địa chỉ loopback, mạng nội bộ, link-local, unique-local, multicast và dải NAT của nhà mạng đều bị từ chối. Phép kiểm tra chạy ngay lúc kết nối.
* Tối đa 2048 ký tự.
* Trỏ thẳng tới địa chỉ cuối cùng, vì chúng tôi **không đi theo chuyển hướng**.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.