API Hội thoại & Tin nhắn
Đọc danh sách hội thoại, đọc tin nhắn và gửi tin chữ / ảnh / video vào một hội thoại theo conversation_id. Tin gửi đi ra đúng kênh của khách (Zalo, Facebook, Telegram, Viber, WhatsApp, Email, Web Chat…) như khi nhân viên trả lời trong app. Base URL, cách lấy API key, xác thực, mã lỗi và giới hạn tần suất: xem Tổng quan.
- Quyền (scope) của API key:
conversations:read(xem) ·conversations:write(gửi tin + tải tệp) ·customers:read(tìm khách theo số điện thoại) ·zalo:use(tìm người dùng Zalo theo SĐT, kết bạn, nhắn theo uid). Chọn khi tạo key ở web → API Key.
Luồng điển hình
Khách nhắn ──► Siêu trợ lý ──webhook message.incoming──► Hệ thống của bạn
│ (có conversation_id, message_id, content, media_url)
Hệ thống của bạn ──POST /conversations/{id}/messages──► Siêu trợ lý ──► kênh của khách- Đăng ký webhook ở web → Sự Kiện & Webhook với sự kiện
message.incoming(khách nhắn đến) và/hoặcmessage.sent(tin đã gửi đi). - Khi nhận sự kiện, lấy
conversation_idrồi gọiPOST /conversations/{id}/messagesđể trả lời. - Hoặc chủ động duyệt:
GET /conversations→ chọn hội thoại →GET /conversations/{id}/messages.
Nội dung webhook message.incoming
{ "event_type": "message.incoming", "workspace_id": "…", "data": {
"message_id": "…", "conversation_id": "…", "channel_id": "…", "contact_id": "…", "contact_name": "Nguyễn Văn An",
"content": "Cho mình hỏi giá", "message_type": "text", "media_url": null, "created_at": "2026-10-11T10:00:00.000Z" } }Tin ảnh/video/tệp của khách có message_type = image | video | file và media_url là liên kết tải. message.sent có thêm source: agent (nhân viên) hoặc api (đối tác) — dùng để bỏ qua tin do chính bạn gửi.
Đối tượng
Hội thoại (conversation)
{ "id": "…", "status": "open", "channel": { "id": "…", "type": "zalo_personal", "name": "Zalo shop" },
"contact": { "id": "…", "name": "Nguyễn Văn An", "phone": "0901234567", "avatar": null, "is_group": false, "customer_id": "…" },
"last_message": "Cho mình hỏi giá", "last_message_type": "text", "last_message_at": "…", "unread_count": 2,
"assigned_to": null, "created_at": "…" }status: open · pending · resolved.
Tin nhắn (message)
{ "id": "…", "conversation_id": "…", "direction": "in", "sender_type": "contact", "content": "Xin chào",
"message_type": "text", "media_url": null, "status": "sent", "reply_to_message_id": null, "created_at": "…" }direction: in = khách gửi · out = nhân viên / bot / API gửi.
Endpoints
GET /customers?phone=<số điện thoại> — customers:read
Tìm khách hàng theo số điện thoại và trả kèm các hội thoại của khách — chỉ thông tin hội thoại, không có tin nhắn/lịch sử (muốn xem tin nhắn gọi GET /conversations/{id}/messages với quyền conversations:read). Nhận mọi dạng số Việt Nam: 0901234567, +84901234567, 84901234567, 090 123 4567. Khớp cả số lưu ở hồ sơ khách lẫn số của kênh chat (Zalo, WhatsApp…) gắn với khách. Chỉ tìm trong workspace của API key. Trả { "data": [customer…] } — thường 1 khách; có thể nhiều nếu workspace có hồ sơ trùng số; không thấy → []. Thiếu hoặc sai phone → 400 invalid_request; key thiếu quyền → 403 insufficient_scope.
{ "data": [ {
"id": "c1…", "name": "Nguyễn An", "phone": "0901234567", "email": "[email protected]", "address": "12 Lê Lợi, Q1", "avatar": null,
"notes": null, "gender": null, "has_purchased": true, "label_ids": [], "custom_fields": {}, "created_at": "2026-10-01T03:00:00Z", "updated_at": "2026-10-10T08:00:00Z",
"conversations": [ // mới nhất trước (tối đa 500); cùng cấu trúc `conversation` ở mục Đối tượng
{ "id": "v1…", "status": "open", "channel": { "id": "…", "type": "zalo_personal", "name": "Zalo A" }, "contact": { "id": "…", "name": "An Zalo", "phone": null, "customer_id": "c1…" },
"last_message": "xin chào", "last_message_at": "2026-10-16T01:00:00Z", "unread_count": 1, "assigned_to": null }
] } ] }last_message chỉ là đoạn xem trước tin cuối để chọn hội thoại. Cần đối chiếu đơn hàng của khách thì dùng GET /orders?customer_phone=… (quyền orders:read).
Zalo cá nhân: tìm theo SĐT, kết bạn, nhắn theo uid — zalo:use
Dùng kênh Zalo cá nhân (kết nối bằng QR) của workspace. Nếu workspace có nhiều kênh Zalo cá nhân thì gửi thêm channel_id (lấy ở web → Tích hợp); chỉ có một kênh thì có thể bỏ qua. Để tránh Zalo khóa tài khoản vì thao tác ồ ạt, mỗi kênh có hạn mức: 40 lượt tìm/giờ, 30 lời mời kết bạn/ngày, 1.000 tin nhắn/ngày tới người chưa từng có hội thoại (nhắn người đã có hội thoại không bị tính). Vượt hạn mức → 429 zalo_rate_limited. Lỗi từ Zalo → 502 zalo_error; tài khoản Zalo đang ngắt kết nối → 409 channel_disconnected.
GET /zalo/users?phone=0901234567[&channel_id=…] — tìm tài khoản Zalo theo số điện thoại (nhận 0901…, +84901…, 84901…). Trả:
{ "found": true, "channel_id": "…", "phone": "0901234567",
"user": { "uid": "1234567890123", "name": "Trần Bình", "display_name": "Bình", "avatar": "https://…", "gender": 0 },
"friend_status": { "is_friend": false, "is_requested": false, "is_requesting": false },
"contact": { "id": "…", "name": "Trần Bình", "customer_id": "…", "conversation_id": "…" } }friend_status: is_requested = mình đã gửi lời mời (đang chờ), is_requesting = họ đã gửi lời mời cho mình. contact chỉ có khi người này đã có trong hệ thống. Không thấy (hoặc họ tắt "tìm theo SĐT" trong Zalo) → { "found": false, … }.
POST /zalo/friend-requests — gửi lời mời kết bạn: { "uid": "1234567890123", "message": "Xin chào…", "channel_id": "…" } (message ≤ 150 ký tự, bỏ trống = lời nhắn mặc định). 201 { "ok": true, "uid", "channel_id", "message" }.
POST /zalo/messages — nhắn tin theo uid: { "uid": "1234567890123", "content": "Chào bạn…", "name": "Trần Bình", "channel_id": "…" } (content ≤ 2000 ký tự; name tuỳ chọn, bỏ trống = lấy tên từ Zalo). Hệ thống tự tạo khách + hội thoại nếu chưa có, tin hiện trong Inbox như nhân viên gửi (nguồn api). 201 { "ok": true, "message_id", "conversation_id", "contact_id", "customer_id", "channel_id", "uid", "created_conversation": true }. Sau đó dùng conversation_id với các API hội thoại ở trên.
GET /conversations — conversations:read
Danh sách hội thoại, mới nhất trước. Tham số (đều tùy chọn): status, channel_id, contact_id, unread=true, q (tên hoặc số điện thoại khách, hoặc nội dung tin cuối), updated_since (ISO 8601 — trả hội thoại có tin từ mốc đó, cũ → mới, dùng để đồng bộ tăng dần), page, limit (≤ 100, mặc định 30). Trả { "data": [conversation…], "pagination": { page, limit, total, total_pages } }.
GET /conversations/{id} — conversations:read
Một hội thoại.
GET /conversations/{id}/messages — conversations:read
Tin nhắn, mới nhất trước. Tham số: limit (≤ 100, mặc định 30), before (ISO 8601 — lấy tin cũ hơn mốc này). Trả { "data": [message…], "has_more": true, "next_before": "…" }; muốn tải tiếp tin cũ hơn thì gọi lại với before=next_before.
POST /conversations/{id}/messages — conversations:write
Gửi một tin vào hội thoại. Trả 201 với message vừa tạo.
| Trường | Bắt buộc | Mô tả |
|---|---|---|
type | không | text (mặc định) · image · video · file |
content | với text | Nội dung chữ (≤ 4000 ký tự). Với ảnh/video/tệp là chú thích tùy chọn (≤ 1000) |
media_url | với image/video/file | Chỉ nhận url do POST /media/presign trả về (xem dưới) — URL ngoài bị từ chối |
reply_to_message_id | không | Trả lời một tin cụ thể trong cùng hội thoại |
client_message_id | khuyến nghị | Mã do bạn đặt (≤ 80 ký tự). Gửi lại cùng mã (do timeout/mạng) → trả 200 tin đã gửi với "duplicate": true, không gửi trùng |
curl -X POST "$BASE/conversations/$CID/messages" -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"type":"text","content":"Chào anh An, giá 50.000đ ạ","client_message_id":"order-123-reply-1"}'POST /media/presign — conversations:write
Xin URL tải ảnh/video/tệp lên kho lưu trữ (không đi qua máy chủ Siêu trợ lý). Body: { "filename": "sp.jpg", "content_type": "image/jpeg", "size": 20480 }. Ảnh ≤ 20 MB; video/tệp ≤ 100 MB; không nhận HTML/SVG/script/tệp thực thi. Phản hồi:
{ "method": "PUT", "upload_url": "https://…", "headers": { "Content-Type": "image/jpeg", "Cache-Control": "…" }, "url": "https://…/sp.jpg", "expires_in": 600, "expires_at": "…" }PUT upload_urlvới đúng các header trongheadersvà body là tệp (đúngsizeđã khai; không gửiAuthorization) →200.- Gửi tin với
"type":"image"và"media_url": <url>.
# Gửi ảnh
SIZE=$(wc -c < sp.jpg)
P=$(curl -s -X POST "$BASE/media/presign" -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d "{\"filename\":\"sp.jpg\",\"content_type\":\"image/jpeg\",\"size\":$SIZE}")
URL=$(echo "$P" | jq -r .upload_url); MEDIA=$(echo "$P" | jq -r .url)
curl -X PUT "$URL" -H "Content-Type: image/jpeg" -H "Cache-Control: public, max-age=31536000, immutable" --data-binary @sp.jpg
curl -X POST "$BASE/conversations/$CID/messages" -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d "{\"type\":\"image\",\"media_url\":\"$MEDIA\",\"content\":\"Ảnh sản phẩm\"}"Lưu ý: tệp tải lên cho hội thoại tự bị xóa sau khoảng 7 ngày (khách đã nhận trên kênh của họ thì vẫn xem được). Tải lại nếu cần dùng lại.
Lỗi
Cùng định dạng { "error": { "code", "message" } }: 401 unauthorized · 403 insufficient_scope · 404 not_found (hội thoại không có hoặc thuộc workspace khác) · 400 invalid_request · 413 payload_too_large · 429 rate_limited.
Lưu ý
- Tin gửi qua API được ghi nguồn "API" (tên key) và hiện trong app như tin của nhân viên; hội thoại được đánh dấu đã đọc.
- Tin được lưu và đẩy đến app ngay; việc chuyển sang kênh của khách (Zalo, Facebook…) chạy nền, giống app. Kết quả giao tin phụ thuộc kênh (ví dụ một số kênh nhận video dưới dạng liên kết).
- Quyền theo workspace của API key: key chỉ thấy và gửi được trong workspace của nó.
