Hướng dẫn › Tích hợp & API
.md

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

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

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.

curl https://TEN-MIEN-PENAI/v1/zalo-inbox/channels \
  -H "Authorization: Bearer psk_xxx"
{
  "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

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):

{
  "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ữ

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

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

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

Kết quả thành công

{ "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
curl "https://TEN-MIEN-PENAI/v1/zalo-inbox/CHANNEL_ID/find-phone?phone=0912345678" \
  -H "Authorization: Bearer psk_xxx"
{ "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:

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

Kết quả:

{ "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).