# API gửi tin nhắn Zalo cá nhân

> Cho hệ thống khác (CRM, website, phần mềm bán hàng…) ra lệnh cho kênh Zalo cá nhân của PenAI gửi tin nhắn: theo số điện thoại hoặc theo UID, có thể kèm ảnh.

Áp dụng cho PenAI từ bản 1.3.0 trở lên · Cập nhật 06/10/2026 · Nguồn: https://help.penai.vn/zalo-api/

## Tổng quan

PenAI có hai đường để gửi tin Zalo từ bên ngoài:

| Đường | Dùng khi | Xác thực |
|---|---|---|
| **REST API** (gọi HTTP thông thường) | Phần mềm của bạn tự gọi: CRM, website, script, n8n, Make… | Khóa API `psk_…` |
| **MCP** (Model Context Protocol — chuẩn để ứng dụng AI gọi công cụ bên ngoài) | Muốn Claude, ChatGPT… gửi tin thay bạn bằng câu lệnh tự nhiên | OAuth (đăng nhập và cấp quyền trên trình duyệt) |

Trong bài, `https://TEN-MIEN-PENAI` là địa chỉ bản PenAI của bạn (ví dụ `https://penai.congty.vn`).

## Chuẩn bị

> Chưa có khóa API hoặc chưa thử kết nối? Làm theo bài **[Lấy khóa API, kết nối và kiểm tra kết nối](https://help.penai.vn/khoa-api/)** trước (bản Markdown cho AI: https://help.penai.vn/khoa-api.md), rồi quay lại đây.

1. **Kênh Zalo cá nhân đã kết nối.** Dashboard → **Channels** → thêm kênh loại `zalo_personal` → **Kết nối QR** → quét bằng Zalo trên điện thoại.
2. **Khóa API.** Dashboard → **🔑 Khóa API (tích hợp)** → tạo khóa với vai trò **Vận hành** (`operator`) trở lên. Khóa có dạng `psk_…` và **chỉ hiện một lần** — lưu lại ngay. Khóa vai trò "Chỉ xem" không gửi được tin. Hướng dẫn từng bước có hình: [Lấy khóa API](https://help.penai.vn/khoa-api/).

Mọi lệnh gọi đều kèm header (phần đầu của yêu cầu HTTP):

```
Authorization: Bearer psk_xxxxxxxxxxxxxxxx
Content-Type: application/json
```

> Khóa API có toàn quyền gửi tin bằng tài khoản Zalo của bạn. Chỉ đặt khóa ở phía máy chủ — không đưa vào mã chạy trên trình duyệt hay ứng dụng di động.

## 1. Lấy mã kênh Zalo

**GET** `/v1/zalo-inbox/channels`

Trả về các kênh Zalo mà khóa được dùng, kèm trạng thái kết nối. Lấy trường `id` làm `CHANNEL_ID` cho các lệnh bên dưới.

```bash
curl https://TEN-MIEN-PENAI/v1/zalo-inbox/channels \
  -H "Authorization: Bearer psk_xxx"
```

```json
{
  "channels": [
    {
      "id": "3f0c1a2b-....-....-....-............",
      "name": "Zalo CSKH",
      "enabled": true,
      "connected": true,
      "account": { "id": "1234567890123456789", "name": "Nguyễn Văn A" }
    }
  ]
}
```

`connected: false` nghĩa là kênh đã mất đăng nhập — cần vào Dashboard quét QR lại trước khi gửi.

## 2. Gửi tin theo số điện thoại hoặc UID

**POST** `/v1/zalo-inbox/CHANNEL_ID/new`

Đây là lệnh chính. Truyền **một trong hai**: `phone` hoặc `to`.

| Trường | Kiểu | Ý nghĩa |
|---|---|---|
| `phone` | chuỗi | Số điện thoại người nhận, 8–15 chữ số, giữ số 0 đầu hoặc dạng `+84…`. PenAI tự tra UID trên Zalo rồi gửi luôn. |
| `to` | chuỗi số | UID (mã định danh Zalo của người nhận) hoặc ID nhóm. Gửi thẳng, không tra cứu. |
| `peerKind` | `"direct"` hoặc `"group"` | Chỉ cần khi `to` là ID nhóm chưa từng xuất hiện trong Inbox: đặt `"group"`. Mặc định `"direct"` (cá nhân). |
| `text` | chuỗi | Nội dung tin, tối đa 10.000 ký tự. |
| `image` | chuỗi | Ảnh kèm theo: URL `http(s)` công khai, hoặc `data:image/png;base64,…`. Nhận PNG, JPEG, GIF, WEBP, tối đa 10 MB. |

Phải có ít nhất `text` hoặc `image`; có thể gửi cả hai.

### Ví dụ: gửi theo số điện thoại, kèm ảnh

```bash
curl -X POST https://TEN-MIEN-PENAI/v1/zalo-inbox/CHANNEL_ID/new \
  -H "Authorization: Bearer psk_xxx" \
  -H "Content-Type: application/json" \
  --data-binary @body.json
```

Nội dung file `body.json` (lưu dạng UTF-8):

```json
{
  "phone": "0912345678",
  "text": "Chào anh, đơn hàng #1024 đã được giao cho đơn vị vận chuyển.",
  "image": "https://example.com/hoa-don-1024.jpg"
}
```

### Ví dụ: gửi theo UID, chỉ có chữ

```json
{ "to": "1234567890123456789", "text": "Lịch hẹn của anh là 9h sáng mai." }
```

### Ví dụ: gửi vào nhóm

```json
{ "to": "9876543210987654321", "peerKind": "group", "text": "Báo cáo cuối ngày đã sẵn sàng." }
```

### Kết quả thành công

```json
{ "sent": true, "threadId": "1234567890123456789", "peerKind": "direct" }
```

`threadId` chính là UID của người nhận (hoặc ID nhóm). Khi gửi theo số điện thoại, hãy **lưu lại `threadId`** để lần sau gửi thẳng bằng `to` — nhanh hơn và không phải tra Zalo lần nữa.

## 3. Tra UID từ số điện thoại (không gửi tin)

**GET** `/v1/zalo-inbox/CHANNEL_ID/find-phone?phone=0912345678`

```bash
curl "https://TEN-MIEN-PENAI/v1/zalo-inbox/CHANNEL_ID/find-phone?phone=0912345678" \
  -H "Authorization: Bearer psk_xxx"
```

```json
{ "found": true, "user": { "uid": "1234567890123456789", "name": "Trần Thị B", "avatar": "https://..." } }
```

Không tìm thấy: `{ "found": false, "user": null }`. Trường hợp này có thể do số chưa dùng Zalo **hoặc** người đó tắt "cho phép tìm qua số điện thoại" — API không phân biệt được hai trường hợp.

## 4. Gửi vào hội thoại đã có

**POST** `/v1/zalo-inbox/CHANNEL_ID/threads/THREAD_ID/send`

Dùng khi đã biết `THREAD_ID` (UID người nhận hoặc ID nhóm). Body giống lệnh ở mục 2 nhưng không có `to`/`phone`:

```json
{ "text": "Cảm ơn anh đã phản hồi.", "image": "https://example.com/anh.png" }
```

Kết quả:

```json
{ "sent": true, "msgIds": ["6871234567890"] }
```

## 5. Các API Inbox khác

| Lệnh | Việc |
|---|---|
| `GET /v1/zalo-inbox/CHANNEL_ID/threads?q=&kind=&unread=1` | Danh sách hội thoại. `q` tìm theo tên / UID / số điện thoại (tìm cả danh bạ chưa từng chat); `kind` = `direct` hoặc `group`. |
| `GET /v1/zalo-inbox/CHANNEL_ID/threads/THREAD_ID/messages?before=&limit=` | Đọc tin nhắn của một hội thoại (mặc định 50 tin mới nhất). |
| `POST /v1/zalo-inbox/CHANNEL_ID/threads/THREAD_ID/read` | Đánh dấu đã đọc. |
| `PUT /v1/zalo-inbox/CHANNEL_ID/threads/THREAD_ID/ai` | Bật/tắt AI tự trả lời cho riêng hội thoại: `{ "mode": "auto" }` hoặc `{ "mode": "off" }`; `{ "resume": true }` để AI trả lời lại ngay. |
| `POST /v1/zalo-inbox/CHANNEL_ID/threads/THREAD_ID/messages/MESSAGE_ID/react` | Thả cảm xúc: `{ "reaction": "heart" }` — nhận `heart`, `like`, `haha`, `wow`, `cry`, `angry`, `none`. |
| `POST /v1/zalo-inbox/CHANNEL_ID/sync-contacts` | Kéo lại danh bạ (bạn bè + nhóm) từ Zalo. |
| `GET /v1/zalo-inbox/events` | Luồng sự kiện thời gian thực (SSE — máy chủ đẩy tin mới về ngay khi có). |
| `GET /v1/contacts/export.xlsx?channelId=CHANNEL_ID` | Xuất danh sách người/nhóm đã nhắn tới kèm UID ra Excel. |

## Mã lỗi

Khi lỗi, API trả JSON dạng `{ "error": "mô tả", "code": "MÃ" }`.

| HTTP | Ý nghĩa | Tin đã gửi chưa? | Nên làm gì |
|---|---|---|---|
| `400` | Dữ liệu sai: thiếu `to`/`phone`, thiếu nội dung, số điện thoại sai định dạng, ảnh hỏng / quá lớn / URL trỏ vào mạng nội bộ | Chưa | Sửa dữ liệu rồi gọi lại |
| `401` | Thiếu hoặc sai khóa API | Chưa | Kiểm tra header `Authorization` |
| `403` | Khóa không được dùng kênh này (sai `CHANNEL_ID` hoặc khóa vai trò Chỉ xem) | Chưa | Dùng khóa Vận hành trở lên |
| `404` | Không tìm thấy tài khoản Zalo với số điện thoại này | Chưa | Số chưa dùng Zalo hoặc chặn tìm kiếm — không thử lại |
| `409` `NOT_CONNECTED` | Kênh Zalo mất kết nối | Chưa | Vào Dashboard quét QR lại |
| `422` `ZALO_REJECTED` | Zalo từ chối: UID không tồn tại, sai loại cá nhân/nhóm, gửi cho chính tài khoản đang kết nối, bị người nhận chặn… | Chưa | Kiểm tra UID và `peerKind` |
| `502` | Lỗi khi gửi (mạng, Zalo không phản hồi) | **Không chắc** | Kiểm tra Inbox trước khi gửi lại để tránh trùng |

## Kết nối ứng dụng AI (MCP)

Địa chỉ MCP của bạn: `https://TEN-MIEN-PENAI/mcp` — xem và sao chép ở Dashboard → **🔗 Kết nối AI bên ngoài**.

- **Claude**: Settings → Connectors → Add custom connector → dán địa chỉ → Connect.
- **ChatGPT**: Settings → Apps & Connectors → Advanced → bật Developer mode → Create → dán địa chỉ, chọn OAuth.

Trình duyệt mở trang cấp quyền của PenAI: đăng nhập bằng tài khoản Dashboard, tick quyền rồi bấm **Cấp quyền**. Mật khẩu không chuyển cho ứng dụng AI.

| Công cụ | Quyền | Tham số |
|---|---|---|
| `zalo_list_channels` | read | — → kênh, trạng thái kết nối |
| `zalo_list_contacts` | read | `query?`, `limit?`, `cursor?` → `uid`, tên, số điện thoại |
| `zalo_list_groups` | read | như trên → `group_id`, tên, số thành viên |
| `zalo_find_user_by_phone` | read | `phone` → `uid`, tên |
| `zalo_send_message_by_phone` | send | `phone`, `message?`, `image_url?`, `request_id?` |
| `zalo_send_message` | send | `to` (uid / group_id), `thread_type?`, `message?`, `image_url?`, `request_id?` |
| `zalo_react_latest` | send | `thread_id`, `reaction?` → thả cảm xúc vào tin mới nhất của khách |
| `zalo_list_conversations`, `zalo_get_messages`, `zalo_search_messages` | messages | Đọc hội thoại — chỉ có khi quản trị bật ở Inbox → ⚙️ Cài đặt (mặc định tắt) |

Khác với REST API, đường MCP có sẵn hai lớp bảo vệ:

- **Chống gửi trùng**: truyền `request_id` (một mã UUID) cho mỗi tin; thử lại cùng tin thì giữ nguyên mã — PenAI sẽ không gửi lần hai.
- **Giãn nhịp**: mỗi kênh 1 tin / 2 giây; gửi dồn nhận `RATE_LIMITED` kèm `retry_after_seconds` (số giây cần chờ).

## Lưu ý quan trọng

- **Zalo cá nhân không phải kênh chính thức cho gửi hàng loạt.** Kết nối dùng cơ chế của Zalo Web; gửi nhiều tin cho người lạ trong thời gian ngắn có thể làm tài khoản bị Zalo giới hạn hoặc khóa. Nên gửi cách nhau vài giây và ưu tiên người đã từng nhắn với tài khoản.
- **REST API không tự chống gửi trùng.** Gọi lệnh gửi hai lần là gửi hai tin. Hệ thống của bạn cần tự ghi nhận tin nào đã gửi; khi gặp lỗi `502` thì kiểm tra trước, đừng tự động gửi lại.
- **Gửi theo UID không kiểm tra trước.** UID sai sẽ được Zalo từ chối ở bước gửi (lỗi `422`).
- **Không gửi được cho chính UID** của tài khoản đang kết nối.
- **Một tài khoản — một nơi đăng nhập Zalo Web.** Mở Zalo Web trên trình duyệt bằng cùng tài khoản có thể làm PenAI mất kết nối.
- Tin gửi qua REST API hiện trong Inbox Zalo như tin do người trực gửi; AI sẽ tạm im trong hội thoại đó theo số phút ở Inbox → ⚙️ Cài đặt (mặc định 30 phút).
