> ## 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.

# Visitor API

> Tự xây giao diện chat riêng trên nền Live Chat: đọc cấu hình, tạo phiên, nhận luồng trả lời, gửi tin và tệp

Phần này dành cho bên muốn dựng giao diện chat riêng thay vì dùng widget. Mọi đường dẫn dưới đây là tương đối so với `https://agents.fpt.ai/direct-bff`.

Toàn bộ bề mặt này **không dùng phiên đăng nhập của nền tảng**. Khách hàng ẩn danh được cấp một `visitorToken` ngắn hạn, và chính token đó quyết định họ thuộc tổ chức nào, phiên nào, Agent nào.

<Info>
  Trình tự bắt buộc: **đọc cấu hình → tạo phiên → mở luồng nhận trả lời → gửi tin nhắn**.
</Info>

| Method | Đường dẫn | Xác thực | Mục đích |
| - | - | - | - |
| `GET` | `/v1/visitor/config` | không cần | Đọc cấu hình hiển thị của kênh |
| `POST` | `/v1/visitor/session` | không cần | Tạo phiên, lấy `visitorToken` |
| `GET` | `/v1/sessions/{sessionId}/stream` | `visitorToken` | Nhận luồng trả lời thời gian thực (SSE) |
| `POST` | `/v1/runs` | `visitorToken` | Gửi một tin nhắn của khách hàng |
| `POST` | `/v1/visitor/uploads` | `visitorToken` | Tải tệp đính kèm lên |
| `POST` | `/v1/visitor/files/presign` | `visitorToken` hoặc khóa API | Lấy đường dẫn tải tệp do Agent tạo ra |
| `POST` | `/v1/visitor/feedback` | `visitorToken` | Gửi đánh giá cho một câu trả lời |

## 1. Đọc cấu hình hiển thị

```http theme={null}
GET https://agents.fpt.ai/direct-bff/v1/visitor/config?connectionKey=wgt_abc123
```

Không cần xác thực, vì giao diện phải vẽ được trước khi có phiên.

```json Phản hồi theme={null}
{
  "name": "Acme Support",
  "avatarUrl": "https://…/logo.png",
  "theme": { "primary": "#203BDC", "position": "right" },
  "welcome": { "title": "Xin chào!", "subtitle": "Chúng tôi có thể giúp gì?" },
  "starters": ["Theo dõi đơn hàng", "Đổi trả", "Liên hệ nhân viên"],
  "turnstileSitekey": "0x4AAA…",
  "locales": ["vi", "en", "ja", "id", "zh"],
  "localeDefault": "vi",
  "chat": {
    "placeholder": "",
    "disclaimer": "Nội dung do AI tạo, vui lòng kiểm chứng.",
    "poweredBy": "Powered by FPT.AI",
    "showLogoHeader": true,
    "showCitations": true,
    "allowAttachments": true
  }
}
```

* `turnstileSitekey`, `avatarUrl` và `localeDefault` trả về `null` khi chưa đặt, không bao giờ là chuỗi rỗng. Một `turnstileSitekey` rỗng sẽ bật cổng chống bot mà không khách hàng nào qua được.
* `chat` luôn là một đối tượng, không bao giờ `null`. Ba giá trị đúng/sai trong đó luôn có mặt và mặc định là bật.
* `chat.placeholder` rỗng nghĩa là tổ chức không tự soạn; hãy dùng chuỗi đã dịch sẵn của bạn.
* `theme` và `welcome` được trả về **nguyên trạng**, không kiểm tra định dạng. Hãy tự kiểm tra trước khi đưa vào CSS.
* Danh sách tên miền cho phép không nằm trong kết quả và không chi phối lệnh này; nó được áp dụng ở lệnh tạo phiên.

<Note>
  Lệnh này trả về `404 CONNECTION_NOT_FOUND` khi `connectionKey` không tồn tại, kênh chưa kết nối, chưa gắn Agent, hoặc kênh đã bị xóa. Lệnh không trả về 403 và không kiểm tra Origin, vì nội dung trả về là thông tin thương hiệu công khai. Cửa kiểm soát nằm ở lệnh tạo phiên.
</Note>

## 2. Tạo phiên

```http theme={null}
POST https://agents.fpt.ai/direct-bff/v1/visitor/session
Content-Type: application/json
Origin: https://shop.example.com

{
  "connectionKey": "wgt_abc123",
  "locale": "vi",
  "visitorToken": "eyJ…",
  "visitorName": "Nguyễn Văn An",
  "turnstileToken": "0.abc…"
}
```

| Tham số | Kiểu | Mô tả | Bắt buộc |
| - | - | - | - |
| `connectionKey` | chuỗi | Khóa kết nối của kênh Website. Tối đa 256 byte. | Bắt buộc |
| `locale` | chuỗi | Ngôn ngữ hội thoại. | Tùy chọn |
| `visitorToken` | chuỗi | Token của phiên trước, lấy từ localStorage. Gửi kèm để **khách hàng cũ quay lại đúng hội thoại cũ**. Tối đa 512 byte. | Tùy chọn |
| `visitorName` | chuỗi | Tên khách hàng tự nhập ở màn hình chào. Tối đa 256 byte. | Tùy chọn |
| `turnstileToken` | chuỗi | Bắt buộc khi kênh có `turnstileSitekey`. | Có điều kiện |

```json Phản hồi 200 theme={null}
{
  "visitorToken": "eyJ…",
  "expiresAt": "2026-09-14T10:00:00Z",
  "sessionId": "7412",
  "endUserId": "9876"
}
```

* `visitorToken` sống **1 giờ**. Gửi nó ở header `Authorization: Bearer` cho mọi lệnh gọi sau đó.
* `sessionId` là định danh hội thoại, giữ nguyên qua các lần kết nối lại.
* Tất cả định danh số nguyên đều là **chuỗi** trên đường truyền, vì chúng vượt ngưỡng số nguyên an toàn của JavaScript.

### Khách hàng quay lại

Gửi kèm `visitorToken` cũ thì hệ thống nối lại **đúng khách hàng và đúng hội thoại**, kèm hạn dùng mới. Token vẫn được chấp nhận **kể cả khi đã hết hạn**: hạn dùng chỉ chặn việc sử dụng, còn chữ ký mới là căn cứ xác định danh tính. Token thiếu, hỏng, sai chữ ký hoặc thuộc tổ chức khác không gây lỗi: hệ thống lặng lẽ cấp một danh tính mới.

Về `visitorName`: gửi `visitorToken` mà **không** kèm tên thì tên đã lưu được giữ nguyên; gửi một tên **khác** thì tên mới ghi đè. Phản hồi 200 không trả tên về, để token không trở thành công cụ tra cứu danh tính.

<Note>
  Tên được cắt còn 64 ký tự, và **bị bỏ hoàn toàn** nếu chứa ký tự điều khiển hay ký tự xuống dòng Unicode. Không trường hợp nào báo lỗi: một cái tên không dùng được chỉ làm mất lời chào, không làm hỏng cuộc trò chuyện.
</Note>

| Mã | Ý nghĩa |
| - | - |
| `400 INVALID_ARGUMENT` | Thiếu `connectionKey`, hoặc một trường vượt giới hạn độ dài. |
| `403 ORIGIN_NOT_ALLOWED` | Origin không nằm trong danh sách cho phép, hoặc không có header `Origin`. |
| `403 CONVERSATION_UNAVAILABLE` | Khách hàng này đã bị chặn. |
| `404` | `connectionKey` không tồn tại hoặc kênh đã ngắt kết nối. |
| `429 RATE_LIMITED` | Vượt hạn mức tạo phiên; đọc header `Retry-After`. |

## 3. Mở luồng nhận trả lời

```http theme={null}
GET https://agents.fpt.ai/direct-bff/v1/sessions/7412/stream
Authorization: Bearer <visitorToken>
Accept: text/event-stream
```

Luồng SSE phát các sự kiện theo chuẩn AG-UI: `RunStarted`, `TextMessageStart` / `TextMessageContent` / `TextMessageEnd`, `ToolCall*`, `StateSnapshot`, `RunFinished`, `RunError`. Tin nhắn nhiều định dạng (nút bấm, quick reply, carousel) đến trong sự kiện `CUSTOM` mang tên `ui_blocks`; thẻ nguồn trích dẫn đến trong `CUSTOM` mang tên `references`.

<Warning>
  **Mở luồng trước khi gửi tin nhắn đầu tiên.** Token đã phát đi không bao giờ được phát lại: nếu mất kết nối giữa chừng, hãy tải lại tin nhắn đã hoàn tất từ [lịch sử hội thoại](/tich-hop/live-chat-api/kenh-api/lich-su-hoi-thoai) thay vì chờ phát lại.
</Warning>

<Note>
  Thẻ trích dẫn trên luồng SSE mang thêm trường `index` (số thứ tự `[n]` gắn với câu trả lời) và ghi các trường vắng thành `null`. Thẻ trong callback kênh API và trong API đọc lịch sử không có `index`, và trường vắng bị lược bỏ hẳn. Nếu dùng chung một hàm phân tích, hãy coi `index` là tùy chọn và coi khóa vắng mặt giống khóa mang giá trị `null`.
</Note>

## 4. Gửi một tin nhắn

```http theme={null}
POST https://agents.fpt.ai/direct-bff/v1/runs
Authorization: Bearer <visitorToken>
Content-Type: application/json

{
  "sessionId": "7412",
  "agentId": "5",
  "input": "Đơn hàng của tôi đang ở đâu?",
  "metadata": { "postback": "order:list" },
  "attachments": [
    {
      "objectKey": "ws/1/channels/42/7412/9f3c….pdf",
      "name": "bao-gia.pdf",
      "mimeType": "application/pdf",
      "sizeBytes": 184320
    }
  ]
}
```

| Tham số | Mô tả | Bắt buộc |
| - | - | - |
| `sessionId` | Lấy từ bước tạo phiên. | Bắt buộc |
| `agentId` | Agent gắn với kênh. | Bắt buộc |
| `input` | Nội dung khách hàng gõ. Khi khách bấm nút, đây là **nhãn của nút**. | Bắt buộc |
| `metadata.postback` | Dữ liệu ẩn của nút khách vừa bấm. Tối đa 1024 byte. | Tùy chọn |
| `attachments` | Danh sách tệp đã tải lên ở mục 5 bên dưới. Mỗi mục gồm `objectKey`, `name`, `mimeType`, `sizeBytes`, đúng như phản hồi của lệnh tải lên. Tối đa 5 tệp. | Tùy chọn |

* `metadata` là một đối tượng **đóng**: chỉ `postback` được đọc, mọi khóa khác bị bỏ qua.
* `workspaceId`, `connectionId`, `endUserId` và `userId` không được nhận trong phần thân vì đều lấy từ token. Gửi kèm bất kỳ trường nào sẽ nhận `400 WORKSPACE_ID_NOT_ACCEPTED`; giá trị `null` tường minh được coi như không gửi. Riêng `sessionId` vẫn hợp lệ và được đối chiếu với token.
* Phần thân tối đa **256 KiB**.
* Tối đa 5 tệp đính kèm trên một lượt gửi; tệp thứ sáu bị từ chối bằng `400 INVALID_ARGUMENT`. Hãy kiểm tra số lượng ở phía giao diện trước khi tải lên.

Câu trả lời **không** nằm trong phản hồi của lệnh này; nó chảy về qua luồng SSE đã mở ở bước trước.

<Note>
  Nếu tổ chức đã gỡ kênh trong lúc hội thoại đang mở, lệnh vẫn trả về 200 nhưng `status` có giá trị `channel_unavailable`, `output` rỗng, và không có câu trả lời nào về qua SSE nữa. Hãy dừng chờ và báo cho khách rằng kênh không còn khả dụng; tin nhắn họ vừa gửi vẫn được lưu trong lịch sử.
</Note>

## 5. Tải tệp đính kèm lên

```http theme={null}
POST https://agents.fpt.ai/direct-bff/v1/visitor/uploads
Authorization: Bearer <visitorToken>
Content-Type: multipart/form-data

file=<nội dung tệp>
```

```json Phản hồi theme={null}
{
  "objectKey": "ws/1/channels/42/7412/9f3c….pdf",
  "name": "bao-gia.pdf",
  "mimeType": "application/pdf",
  "sizeBytes": 184320
}
```

* Một tệp cho mỗi lần gọi, tối đa **30 MiB**.
* Phần mở rộng được chấp nhận: pdf, doc, docx, ppt, pptx, jpg, jpeg, png, gif, svg, webp, heic, jfif, xlsx, xls, csv.
* Phần mở rộng **và** các byte đầu tệp đều được kiểm tra. Tệp thực thi đổi tên thành `.png` sẽ bị từ chối bằng **415**.
* Không nhận `workspaceId`, `sessionId` hay `connectionId` trong form; tất cả lấy từ token.

| Mã | Ý nghĩa |
| - | - |
| `413 FILE_TOO_LARGE` | Quá 30 MiB. |
| `415 UNSUPPORTED_TYPE` | Phần mở rộng ngoài danh sách, hoặc nội dung tệp không khớp phần mở rộng. |
| `400 INVALID_REQUEST_BODY` | Phần thân không phải multipart hợp lệ. |
| `503 STORAGE_DISABLED` | Hệ thống chưa bật lưu trữ tệp. Chat vẫn hoạt động bình thường, chỉ là không gửi được đính kèm. |

## 6. Tải tệp do Agent tạo ra

Khi Agent tạo ra một tệp, thứ đi kèm câu trả lời là **khóa đối tượng**, không phải đường dẫn tải. Đổi khóa đó lấy một đường dẫn ngắn hạn:

```http theme={null}
POST https://agents.fpt.ai/direct-bff/v1/visitor/files/presign
Authorization: Bearer <visitorToken hoặc khóa API>
Content-Type: application/json

{ "objectKey": "ws/1/artifacts/7311064883817021440/bao-cao.pdf", "disposition": "attachment" }
```

```json Phản hồi theme={null}
{ "url": "https://…", "expiresInSeconds": 300 }
```

* **`disposition` quyết định tệp được tải về hay mở ra.** Dùng `attachment` cho mọi đường dẫn bạn định điều hướng tới; dùng `inline` (mặc định) cho đường dẫn đặt vào thẻ `img` hoặc tải ngầm. Tệp lưu dưới dạng `text/html` sẽ hiển thị như một trang web khi trình duyệt điều hướng tới, thay thế luôn khung chat của khách hàng.
* Đường dẫn sống 300 giây và được cấp mới mỗi lần gọi. **Đừng lưu lại, hãy xin lại khi cần.**
* `visitorToken` chỉ đổi được tệp do **chính hội thoại của họ** tạo ra. Khóa API đổi được mọi tệp trong tổ chức của mình.
* **404** cho cả tệp không tồn tại lẫn tệp không thuộc quyền, cố ý dùng chung một mã để không ai dò được tệp nào đang tồn tại.

## 7. Đánh giá một câu trả lời

```http theme={null}
POST https://agents.fpt.ai/direct-bff/v1/visitor/feedback
Authorization: Bearer <visitorToken>

{ "messageId": "01J…", "rating": "up", "comment": "" }
```

Trả về **204** không kèm nội dung. `rating` nhận `up`, `down`, hoặc `none` để thu hồi đánh giá. Thu hồi một đánh giá vốn không tồn tại cũng trả về 204.

* `messageId` là mã tin nhắn AG-UI mà giao diện của bạn đã hiển thị, **không phải** mã số nội bộ.
* `comment` là tùy chọn và bị **cắt** ở 2000 ký tự chứ không bị từ chối.
* Không có đường đọc ngược: nếu muốn giữ trạng thái nút thích sau khi tải lại trang, hãy tự nhớ ở phía giao diện.

<Card title="CORS và tên miền cho phép" icon="shield-halved" href="/tich-hop/live-chat-api/live-chat/cors-va-ten-mien">
  Bắt buộc đọc trước khi gọi Visitor API từ giao diện của chính bạn.
</Card>


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