Kết nối tài khoản của bạn.
Tích hợp vào hệ thống của bạn.
Từ đăng nhập QR đến gửi tin nhắn, nhận sự kiện và tích hợp cuộc gọi. Hướng dẫn dành cho đội kỹ thuật của đối tác.
Bắt đầu tích hợp
ZeWork Connect cung cấp API để hệ thống của bạn kết nối tài khoản Zalo riêng, gửi tin nhắn, quản lý liên hệ và nhận sự kiện. Người dùng quét QR và xác nhận trên ứng dụng Zalo; hệ thống trả về accountId để bạn thao tác. Không cần đăng nhập ứng dụng ZeWork.
HTTPS, xác thực API và kết nối WebSocket đã được kiểm tra. Đăng nhập QR đã tạo được mã thật. Gửi nhận với tài khoản thực và âm thanh gọi hai chiều cần được nghiệm thu trước khi sử dụng chính thức. mode: live nghĩa là adapter dùng giao thức thật, không phải cam kết mọi chức năng đã nghiệm thu.
Chuẩn bị API key do ZeWork cấp riêng, một tài khoản được phép đăng nhập và người nhận được phép thử gửi/gọi. Tất cả ID và dữ liệu trong ví dụ là minh họa; thay bằng giá trị trả về từ API.
| Kết nối | Địa chỉ |
|---|---|
| REST API | https://dev-connect.zework.com/v1 |
| Sự kiện | wss://dev-connect.zework.com/v1/ws |
| Signaling call | wss://dev-connect.zework.com/v1/call-ws |
| Health không cần key | GET /health |
export BASE='https://dev-connect.zework.com'
# Nạp PARTNER_API_KEY từ secret manager hoặc biến môi trường của backend.
# Không đưa key thật vào mã nguồn, frontend hay URL.
export ACCOUNT_ID='acc_REPLACE_WITH_RETURNED_ID'curl --fail-with-body "$BASE/v1/capabilities" \
-H "Authorization: Bearer $PARTNER_API_KEY"GET /v1/capabilities cho biết các adapter được cấu hình và giới hạn upload. Domain production sẽ được cấp riêng khi hoàn tất nghiệm thu.
Xác thực và định danh
Mọi API /v1 sử dụng Authorization: Bearer <API_KEY>. Key xác định đối tác; mỗi đối tác chỉ truy cập được tài khoản, media, operation và sự kiện thuộc mình. Không gửi partnerId để chọn đối tác trong request.
| Trường | Ý nghĩa |
|---|---|
accountId | ID tài khoản trong Connect, nhận sau đăng nhập. Dùng trong đường dẫn API. |
externalAccountId | Định danh do đối tác đặt, tối đa 200 ký tự, ví dụ mã cửa hàng. Không phải Zalo UID và không chứng minh quyền sở hữu tài khoản. |
recipientId | Zalo UID người nhận hoặc ID nhóm khi isGroup: true. Luôn truyền dạng chuỗi, không dùng số điện thoại trực tiếp. |
operationId | ID tác vụ gửi/đổi dữ liệu bất đồng bộ; dùng truy vấn kết quả. |
eventId | ID sự kiện để chống xử lý trùng giữa webhook, socket và polling. |
callId | ID cuộc gọi do Connect trả về; dùng cho API điều khiển cuộc gọi. |
Giữ API key ở backend. Frontend gọi backend của bạn để lấy QR hoặc token socket ngắn hạn; không gọi REST trực tiếp bằng API key từ trình duyệt. Không cần đưa cookie hay khóa phiên Zalo cho ứng dụng đối tác. Cấp/thu hồi/đổi key hiện thực hiện qua đội triển khai ZeWork; chưa có API tự quản lý key hoặc scopes chi tiết.
Đăng nhập QR và tài khoản
1. Tạo phiên đăng nhập
curl --fail-with-body "$BASE/v1/login-sessions" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Content-Type: application/json" \
--data '{"method":"pc","externalAccountId":"store-001","metadata":{"label":"Cửa hàng 01"}}'{
"loginSessionId": "login_example",
"status": "waiting_scan",
"accountId": null,
"expiresAt": "2026-10-31T10:05:00.000Z",
"qrImage": "data:image/png;base64,...",
"mode": "live"
}Phản hồi 201. method hiện chỉ hỗ trợ pc. Hai trường externalAccountId và metadata không bắt buộc; metadata là object JSON tối đa 4 KiB. Hiển thị qrImage trực tiếp bằng ảnh, không ghi QR vào log công khai. Phiên có thời hạn khoảng 5 phút, tối đa 3 phiên đang chờ mỗi đối tác.
Proxy đăng nhập (tùy chọn)
Thêm "proxy":{"type":"socks5","host":"IP_PUBLIC","port":1080,"username":"user","password":"pass"} vào body tạo QR. Type hỗ trợ socks5 hoặc http; host chỉ nhận IPv4 public; port 1–65535. Bỏ cả username/password nếu không xác thực. Thông tin đăng nhập không chứa dấu hai chấm. Không dùng proxy: bỏ trường hoặc truyền null. Cấu hình được mã hóa trong DB và giữ khi reconnect; đổi proxy bằng đăng nhập QR lại cùng externalAccountId. Không trả password qua API tài khoản.
Proxy hiện áp dụng cho QR, xác nhận đăng nhập, socket tài khoản và upload chunk. Chưa bao phủ mọi RPC nhắn tin/danh bạ hoặc media call; không phải chế độ ép toàn bộ lưu lượng qua proxy.
2. Theo dõi quét và xác nhận
curl --fail-with-body "$BASE/v1/login-sessions/login_example" \
-H "Authorization: Bearer $PARTNER_API_KEY"Poll khoảng 2–3 giây/lần và dừng khi hoàn tất hoặc hết hạn. Cập nhật lại ảnh nếu qrImage thay đổi. Luồng trạng thái: waiting_scan → waiting_confirmation → authenticated → connected; nhánh kết thúc có expired hoặc cancelled. authenticated đã xác thực nhưng chưa bảo đảm kết nối gửi/nhận sẵn sàng.
3. Lưu accountId và chờ tài khoản connected
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID" \
-H "Authorization: Bearer $PARTNER_API_KEY"{
"accountId": "acc_example",
"externalAccountId": "store-001",
"metadata": {"label":"Cửa hàng 01"},
"status": "connected",
"mode": "live"
}| API | Cách dùng |
|---|---|
GET /v1/accounts | Danh sách {items:[...]}, tối đa 1.000 tài khoản. |
GET /v1/accounts/{accountId} | Kiểm tra connecting / connected / disconnected / reauth_required / logged_out. |
POST /v1/accounts/{accountId}/reconnect | Body {}. Kết nối lại bằng phiên còn hợp lệ; nếu cần xác thực lại, tạo QR mới. |
POST /v1/accounts/{accountId}/sync | Body {}. Yêu cầu đồng bộ từ upstream khi tài khoản online; không phải API tải toàn bộ lịch sử có bảo đảm. |
POST /v1/accounts/{accountId}/logout | Body {}. Xóa phiên trong Connect và đóng socket/call liên quan; không phải đăng xuất mọi thiết bị Zalo. |
QR hết hạn: tạo phiên mới. Đăng nhập lại nên dùng cùng externalAccountId và cùng tài khoản Zalo. Một danh tính Zalo đã ràng buộc không tự chuyển sang đối tác khác khi logout; cần liên hệ ZeWork nếu muốn chuyển quyền.
Gửi tin nhắn ảnh và file
Tin nhắn văn bản
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: crm-msg-001" \
-H "Content-Type: application/json" \
--data '{"recipientId":"123456789","type":"text","text":"Xin chào từ hệ thống của bạn","externalRequestId":"crm-msg-001"}'{
"operationId": "op_example",
"accountId": "acc_example",
"kind": "messages",
"status": "queued",
"result": null,
"mode": "live"
}202 chỉ xác nhận đã nhận tác vụ, chưa phải người nhận đã nhận/đọc. text từ 1 đến 10.000 ký tự; externalRequestId tùy chọn, tối đa 200 ký tự, xuất hiện trong sự kiện kết quả. Gửi vào nhóm thêm "isGroup":true và truyền ID nhóm trong recipientId.
Upload media trước khi gửi
curl --fail-with-body "$BASE/v1/media" -H "Authorization: Bearer $PARTNER_API_KEY" -H "Content-Type: image/jpeg" -H "X-File-Name: photo.jpg" --data-binary @./photo.jpg{"mediaId":"media_example","mime":"image/jpeg","name":"photo.jpg","size":12345,"mode":"live"}Upload raw binary, không phải multipart/form-data hay JSON base64. Tối đa 5 MiB mỗi file. Với PDF dùng Content-Type: application/pdf; file khác dùng MIME phù hợp hoặc application/octet-stream. Tên file tối đa 200 ký tự; nên dùng tên ASCII trong header. Media thuộc đối tác đã upload, không dùng chéo đối tác.
Gửi ảnh
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: crm-image-001" \
-H "Content-Type: application/json" \
--data '{"recipientId":"123456789","type":"image","mediaId":"media_example","caption":"Ảnh sản phẩm"}'Gửi file
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: crm-file-001" \
-H "Content-Type: application/json" \
--data '{"recipientId":"123456789","type":"file","mediaId":"media_pdf_example"}'Ảnh cần media có MIME bắt đầu bằng image/. Caption áp dụng cho ảnh. Media upload thành công chưa đồng nghĩa đã gửi lên Zalo; tác vụ file còn chờ xác nhận upload từ upstream. Video, voice và các thao tác nâng cao được hướng dẫn ở các mục tiếp theo.
Upload stream V2 và voice
Luồng dành cho file lớn hơn 5 MiB, voice và video: khởi tạo upload → PUT stream file → nhận mediaId → tạo message → theo dõi operation. Không dùng S3, không yêu cầu đối tác cung cấp URL tải file. Service lưu file trên PVC riêng, worker đọc từng chunk 512 KiB để upload sang Zalo.
Dev giới hạn 30 MiB/file, 2 upload đồng thời và khoảng 1 GiB tổng dung lượng dành cho stream. Phiên upload có hạn 15 phút; file hoàn tất được dọn sau khoảng 24 giờ kể từ hết hạn phiên nếu không còn operation queued/running dùng file. Đây không phải kho lưu trữ lâu dài. PUT lỗi/đứt kết nối: kiểm tra GET session; nếu complete lấy lại mediaId, nếu failed/interrupted tạo phiên mới. Chưa resume từ byte giữa file.
curl --fail-with-body "$BASE/v1/media/upload-sessions" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Content-Type: application/json" \
--data '{"fileName":"voice.webm","mime":"audio/webm","size":12345}'{
"uploadSessionId":"upload_example", "method":"PUT",
"uploadPath":"/v1/media/upload-sessions/upload_example/content",
"expiresAt":"2026-10-31T10:15:00.000Z", "maxBytes":31457280, "mode":"live"
}curl --fail-with-body -X PUT "$BASE/v1/media/upload-sessions/upload_example/content" -H "Authorization: Bearer $PARTNER_API_KEY" -H "Content-Type: application/octet-stream" --data-binary @./voice.webmsize phải bằng đúng số byte file. PUT thành công trả mediaId, size, mime, name và sha256. Đọc lại trạng thái bằng GET /v1/media/upload-sessions/{uploadSessionId}. Trong Explorer có thể chọn file ngay ở bước khởi tạo để tự điền tên/MIME/size.
Gửi voice
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: voice-001" \
-H "Content-Type: application/json" \
--data '{"recipientId":"123456789","type":"voice","mediaId":"media_example"}'Voice nhận audio MIME hoặc video/webm, kiểm tra nội dung thực bằng ffprobe. Worker chuyển WebM/Opus và audio hợp lệ sang AAC-LC mono 44100Hz, kiểm tra decode, tối đa 30 phút, rồi upload endpoint voice và gửi bản tin voice. Không chỉ đổi phần mở rộng, không gửi audio dưới dạng file thông thường. Lỗi codec/nội dung không hợp lệ được phản ánh trong operation. File ghi âm do trình duyệt tạo cần được người dùng cho phép microphone ở ứng dụng của bạn.
Gửi video
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: video-001" \
-H "Content-Type: application/json" \
--data '{"recipientId":"123456789","type":"video","mediaId":"media_video","thumbnailMediaId":"media_thumb","duration":5,"width":1280,"height":720}'Video MP4 tối đa 30 MiB qua stream; thumbnail ảnh tối đa 5 MiB upload riêng. duration dùng giây, width/height dùng pixel; client phải cung cấp metadata đúng. Tiếp tục dùng type image/file cho ảnh và tài liệu. Media stream lớn hơn 5 MiB không dùng được cho type image hoặc thumbnail trong bản này.
Forward reply và thao tác tin nhắn
Đọc GET /v1/events hoặc nhận webhook/socket, chọn event loại message.received hoặc message.echo. Dùng eventId của event làm sourceEventId; không dùng msgId của Zalo hay operationId thay thế. Tin nguồn phải thuộc đúng tài khoản và đối tác. Explorer có ô chọn tin nguồn khi xem danh sách sự kiện.
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages/forward" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: forward-001" \
-H "Content-Type: application/json" \
--data '{"sourceEventId":"evt_example","targets":[{"recipientId":"123456789","isGroup":false},{"recipientId":"987654321","isGroup":true}]}'Forward text dùng mforward; ảnh/video dùng native forward và renew link. Mỗi đích trả operation riêng trong items, không tự retry toàn bộ batch khi chỉ một đích lỗi. Tối đa 20 đích, không trùng. Forward file cần upload bản gốc vào service rồi thêm mediaId vào request để re-upload và giữ descriptor/tên file; không âm thầm chuyển thành file.bin. Forward voice dùng URL voice từ event nguồn. Media nguồn hết hạn hoặc loại chưa hỗ trợ sẽ báo lỗi rõ ràng.
| API sau /v1/accounts/{accountId} | Body |
|---|---|
| POST /messages/reply | {sourceEventId, text}. Quote text, ảnh hoặc file. Đích hội thoại suy ra từ tin nguồn. |
| POST /messages/reaction | {sourceEventId, reactionType, icon}. Dùng mã cảm xúc/icon của client Zalo. |
| POST /messages/recall | {sourceEventId}. Chỉ tin do tài khoản hiện tại gửi, thường lấy từ message.echo. |
| POST /messages/pin | {sourceEventId}. Ghim tin text. |
| GET /pins?recipientId=...&isGroup=false | Danh sách tin ghim; lấy topicId để bỏ ghim. |
| POST /messages/unpin | {recipientId, isGroup, topicId}. |
| POST /messages/typing | {recipientId, isGroup:false}. Chỉ cá nhân. |
| POST /messages/batch | {messages:[...]}: 1–20 nội dung theo schema messages. Tin độc lập, chưa phải album grid. |
| GET /stickers?q=... | Danh mục hoặc tìm sticker. GET /stickers/{stickerId} lấy chi tiết. |
Mọi POST trong bảng cần Idempotency-Key. Với /messages còn có các type sau, dùng cùng recipientId và isGroup:
{"recipientId":"123","type":"sticker","stickerId":1234,"categoryId":12}
{"recipientId":"123","type":"location","latitude":10.7769,"longitude":106.7009}
{"recipientId":"123","type":"contact","contactId":"456"}
{"recipientId":"987","isGroup":true,"type":"mention","text":"@Ten Xin chào","mentions":[{"userId":"123","offset":0,"length":4}]}Mention dùng vị trí/độ dài theo UTF-16 như JavaScript string; kiểm tra vị trí theo đúng text cuối. Text có thể thêm textProperties object để truyền định dạng mà app của bạn đã dùng. Vị trí dùng latitude −90…90, longitude −180…180. Danh thiếp dùng contactId là Zalo UID. Các adapter mới cần nghiệm thu thực tế với tài khoản thử trước vận hành.
Nhãn quản lý riêng của đối tác
Nhãn dưới đây tương đương lớp quản lý nhãn hội thoại của ứng dụng, lưu độc lập trong Connect. Không tự ghi nhãn sang Zalo và không dùng nhãn staff/branch ZeWork. Sticker gửi trong chat là type sticker, khác nhãn quản lý.
| API sau /v1/accounts/{accountId} | Cách dùng |
|---|---|
| GET /labels | Danh sách labelId, name, color. |
| POST /labels | {name, color}, màu dạng #2563eb. Tối đa 100 nhãn/tài khoản. POST không có idempotency; đọc danh sách trước khi retry. |
| PATCH /labels/{labelId} | Sửa name hoặc color. |
| DELETE /labels/{labelId} | Xóa nhãn và liên kết nhãn trong tài khoản. |
| GET /conversations/{recipientId}/labels?isGroup=false | Danh sách nhãn của người/nhóm. |
| PUT /conversations/{recipientId}/labels?isGroup=false | {labelIds:[...]}. Thay toàn bộ, tối đa 30 nhãn; [] gỡ hết. Có thể lặp lại cùng payload. |
Sự kiện: label.created, label.updated, label.deleted và conversation.labels_updated. Label ID của tài khoản khác bị từ chối.
Kết quả tác vụ và chống gửi trùng
curl --fail-with-body "$BASE/v1/operations/op_example" \
-H "Authorization: Bearer $PARTNER_API_KEY"{
"operationId":"op_example", "accountId":"acc_example",
"kind":"messages", "status":"succeeded",
"result":{"provider":{"example":"provider-dependent response"},"simulated":false},
"mode":"live"
}| Trạng thái | Hành động |
|---|---|
queued | Đang chờ xử lý. Theo dõi bằng GET operation hoặc sự kiện. |
running | Đang gọi upstream. Không tạo lại yêu cầu với key mới. |
succeeded | Adapter nhận kết quả thành công từ upstream; không đồng nghĩa đã đọc. result.provider tùy loại thao tác. |
failed | Đọc result.error và providerCode. Khắc phục nguyên nhân trước khi tạo thao tác mới. |
unknown_outcome | Không xác định chắc kết quả sau gián đoạn/timeout. Đối soát tin nhắn hoặc liên hệ hỗ trợ; không tự gửi lại bằng key mới. |
Mỗi thao tác mới cần một Idempotency-Key riêng, tối đa 200 ký tự. Khi mất phản hồi HTTP, retry cùng key, cùng endpoint, cùng tài khoản và cùng payload để nhận lại tác vụ cũ. Đổi payload nhưng giữ key trả 409 IDEMPOTENCY_PAYLOAD_MISMATCH. Với thao tác nhóm/liên hệ, giữ nguyên cả cách dựng payload khi retry.
Key gửi tin được ràng buộc theo đối tác, tài khoản và loại thao tác; key tạo call theo đối tác. Dùng UUID hoặc mã nghiệp vụ duy nhất cho mỗi hành động. Không dùng một key cho nhiều người nhận. Có thể poll 2–3 giây/lần và tăng khoảng chờ nếu tác vụ kéo dài.
Danh bạ kết bạn và nhóm
Các đường dẫn dưới đây bắt đầu bằng /v1/accounts/{accountId}. GET trả {"data":...,"mode":"live"}; cấu trúc bên trong data còn theo upstream. POST thay đổi dữ liệu cần Idempotency-Key và trả operation để theo dõi.
| Method và phần đường dẫn | Payload hoặc tham số |
|---|---|
GET /contacts | Danh bạ. |
GET /contacts/resolve?phone=... | Tra số điện thoại: tự đổi 0 đầu thành 84 và bỏ dấu +; ví dụ 0912345678 → 84912345678. Encode + thành %2B. Kết quả phụ thuộc quyền riêng tư upstream. |
GET /profile/{zaloUid} | Thông tin người dùng; GET /profile cho tài khoản hiện tại. |
GET /friend-requests | Danh sách lời mời. |
POST /friend-requests | {"recipientId":"123456789","message":"Xin chào","externalRequestId":"friend-001"} |
POST /friend-requests/{uid}/accept | {} |
POST /friend-requests/{uid}/reject | {} |
POST /friend-requests/{uid}/cancel | {} — hủy lời mời đã gửi. |
GET /groups | Danh sách nhóm. |
GET /groups/{groupId} | Thông tin nhóm. |
POST /groups | {"name":"Nhóm hỗ trợ","memberIds":["123","456"]} |
POST /groups/{groupId}/rename | {"name":"Tên mới"} |
POST /groups/{groupId}/add-members | {"memberIds":["123"]} |
POST /groups/{groupId}/remove-members | {"memberIds":["123"]} |
POST /groups/{groupId}/leave | {} |
Tên nhóm tối đa 100 ký tự; danh sách thành viên tối đa 100 ID/request. Truyền ID dạng chuỗi. Thành công còn phụ thuộc quyền của tài khoản trong nhóm và chính sách của upstream; không suy ra thành công từ việc API đã trả 202.
Nhận sự kiện và polling
| Cơ chế | Khi sử dụng |
|---|---|
| Webhook | Backend có HTTPS endpoint nhận sự kiện. Cần ZeWork duyệt origin trước khi đăng ký. |
| WebSocket có ACK | Backend hoặc ứng dụng cần sự kiện thời gian thực, không cần public endpoint nhận webhook. |
| Polling | Đơn giản để bắt đầu hoặc đối soát khi mất kết nối. Tự lưu cursor sau xử lý. |
Webhook và WebSocket có thể giao lại cùng sự kiện. Lưu eventId để chống trùng; nếu dùng nhiều kênh, dùng chung cơ chế dedupe. Không có bảo đảm exactly-once. Thời điểm nhận sự kiện không phải bằng chứng người nhận đã đọc tin.
{
"eventId":"evt_example", "cursor":42, "version":1,
"type":"message.received", "occurredAt":"2026-10-31T10:00:00.000Z",
"accountId":"acc_example", "externalAccountId":"store-001",
"mode":"live", "simulated":false,
"data":{"providerMessageId":"123456789","isGroup":false,
"message":{"msgId":"123456789","uidFrom":"987654321","content":"Xin chào"}}
}Ví dụ minh họa; data.message là dữ liệu upstream, cần xử lý trường thiếu và loại nội dung khác văn bản. accountId có thể null với sự kiện ở giai đoạn login.
| Nhóm sự kiện | Tên |
|---|---|
| Đăng nhập | login.qr_updated, login.expired; đọc lại login session để lấy QR mới. |
| Tài khoản | account.authenticated, account.connected, account.disconnected, account.reauth_required, account.logged_out |
| Tin nhắn và tác vụ | message.received, message.echo, message.sent, friend_request.sent, operation.succeeded, operation.failed, operation.unknown_outcome |
| Call | call.ringing, call.answered, call.ended, call.failed; cho phép bỏ qua loại chưa biết. |
| Upstream chưa chuẩn hóa | zalo.control, zalo.reacts, zalo.reactGroups, zalo.delivereds, zalo.seens, zalo.actions, zalo.friends |
Polling
curl --fail-with-body "$BASE/v1/events?after=0&limit=100" \
-H "Authorization: Bearer $PARTNER_API_KEY"{"items":[],"nextCursor":0}Giới hạn 1–100 sự kiện/lần, theo cursor tăng dần. Xử lý và lưu dữ liệu thành công rồi lưu nextCursor; lần sau truyền after=nextCursor. Cursor có thể nhảy số, không cộng 1 hoặc suy ra số sự kiện từ chênh lệch. Polling không cập nhật ACK của WebSocket. Lịch sử event chỉ gồm những gì service đã lưu, không phải toàn bộ lịch sử tài khoản Zalo.
Webhook và xác minh chữ ký
Gửi origin HTTPS nhận webhook của bạn cho đội ZeWork để được cho phép. Bản dev chưa mở origin khách hàng mặc định; đăng ký trước khi allowlist sẽ trả WEBHOOK_ORIGIN_NOT_ALLOWLISTED. Không gửi secret/API key trong URL.
curl --fail-with-body "$BASE/v1/webhooks" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Content-Type: application/json" \
--data '{"url":"https://your-domain.example/webhooks/zework","types":["message.received","message.sent","operation.failed","call.ringing","call.ended"]}'{
"webhookId":"hook_example", "signingSecret":"RETURNED_ONCE_STORE_SECURELY",
"url":"https://your-domain.example/webhooks/zework",
"types":["message.received","message.sent","operation.failed","call.ringing","call.ended"]
}signingSecret chỉ trả khi tạo, khác API key. types:["*"] đăng ký tất cả loại sự kiện. Tối đa 10 webhook đang hoạt động/đối tác và 30 loại/event filter. Chỉ các sự kiện phát sinh sau đăng ký mới tự được đưa vào hàng giao webhook; sự kiện cũ đọc qua polling.
Xác minh trước khi xử lý
Headers: x-ze-event-id, x-ze-timestamp (Unix giây), x-ze-signature. Chữ ký là v1=HEX(HMAC_SHA256(secret, timestamp + "." + rawBody)). Phải dùng đúng raw bytes, không JSON.stringify lại object đã parse.
// Node.js — rawBody là Buffer nhận trước middleware JSON parser.
const {createHmac, timingSafeEqual} = require('node:crypto');
function verifyWebhook(rawBody, headers, secret) {
const ts = headers['x-ze-timestamp'];
const sig = headers['x-ze-signature'];
if (typeof ts !== 'string' || !/^\d+$/.test(ts)) return false;
if (Math.abs(Date.now()/1000 - Number(ts)) > 300) return false;
if (typeof sig !== 'string' || !/^v1=[a-f0-9]{64}$/.test(sig)) return false;
const expected = createHmac('sha256', secret)
.update(ts + '.').update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(sig.slice(3), 'hex'));
}
// Sau verify: parse JSON, kiểm tra header event ID trùng body.eventId,
// lưu eventId duy nhất + payload vào DB/queue bền vững rồi trả HTTP 2xx.
// Event trùng đã lưu: trả 2xx, không chạy nghiệp vụ lần nữa.Đồng bộ đồng hồ máy nhận. Trả 2xx nhanh sau khi lưu bền vững; xử lý nghiệp vụ dài ở worker. Thời gian chờ mỗi lần gửi khoảng 5 giây; timeout/non-2xx sẽ retry tăng dần, tối đa 8 lần thử rồi chuyển dead. Không theo HTTP redirect và không bảo đảm thứ tự webhook.
| API | Kết quả |
|---|---|
GET /v1/webhooks | Danh sách cấu hình; không trả lại signingSecret. |
DELETE /v1/webhooks/{webhookId} | Tắt webhook, hủy delivery đang chờ. Request đã gửi ra ngoài không thể thu hồi. |
GET /v1/deliveries | 100 bản ghi gần nhất: deliveryId, webhookId, eventId, status, attempts, lastHttpStatus. |
POST /v1/deliveries/{deliveryId}/replay | Body {}, trả 202, đưa lại vào hàng chờ. Webhook phải còn hoạt động; khách nhận vẫn cần dedupe. |
WebSocket: nhận sự kiện trong 5 phút
Bạn chỉ cần API key ở backend và một consumerId cố định. WebSocket nhận sự kiện; gửi tin vẫn dùng REST API. Không cần public URL như webhook, không cần tự nối socket Zalo hoặc tự gửi lệnh lấy bù của Zalo.
1. Thử ngay trên API Explorer
Mở API Explorer, nhập API key, tìm “Thử nhận sự kiện WebSocket · có ACK” và kết nối. Xác nhận đã nhận ready/event. Mở listener không tự gửi tin hoặc gọi. Nếu account chưa có hoạt động mới, socket có thể chỉ nhận ready hoặc phát lại lịch sử đã lưu.
2. Hiểu bốn bước
- Backend gọi
POST /v1/socket-tokensbằng API key. - Mở
wss://dev-connect.zework.com/v1/ws, gửi auth với token vừa nhận. - Nhận event, ghi bền vững vào DB/queue; chống trùng bằng eventId.
- Gửi ACK đúng cursor. Mất kết nối: xin token mới, dùng lại consumerId.
curl --fail-with-body "$BASE/v1/socket-tokens" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Content-Type: application/json" \
--data '{}'→ {"type":"auth","token":"TOKEN_VUA_NHAN","consumerId":"crm-demo-01"}
← {"type":"ready","cursor":0,"mode":"live"}
← {"type":"event","event":{"eventId":"evt_demo","cursor":42,"accountId":"acc_demo","type":"message.received","data":{"providerMessageId":"123","isGroup":false,"message":{"content":"Xin chào"}}}}
→ {"type":"ack","cursor":42}ACK nghĩa là “hệ thống tôi đã lưu sự kiện”, không phải người dùng đã đọc tin Zalo. Một event chờ ACK mỗi kết nối; không ACK sẽ không nhận event tiếp theo.
| Trường | Dùng thế nào |
|---|---|
| consumerId | Tên luồng nhận của bạn, ví dụ crm-demo-01. Giữ nguyên khi restart; không tạo UUID mới mỗi lần nối. Chỉ chạy một kết nối cho tên này. |
| eventId | Khóa chống trùng. Webhook và socket có thể giao cùng event; dùng UNIQUE(eventId) trong inbox. |
| cursor | Số do Connect cấp. ACK nguyên giá trị vừa nhận; không cộng 1, không dùng msgId thay thế. Server lưu ACK giúp bạn. |
| accountId | Tài khoản phát sinh event. Socket nhận sự kiện toàn đối tác; lọc account tại backend. |
| data.providerMessageId / data.message.msgId | ID tin Zalo, giữ dạng chuỗi. Không phải cursor ACK. |
3. Copy client backend chạy được
Cần Node.js ≥24.2. Tạo thư mục riêng, cài ws rồi lưu đoạn mã dưới thành websocket-client.cjs. Mẫu tự xin token, reconnect có backoff, lưu inbox SQLite trước ACK và chống trùng qua restart. Đây là demo một process; production thay inbox bằng DB/queue bền vững của bạn và có worker xử lý nghiệp vụ riêng.
npm init -y
npm install ws
# Nạp PARTNER_API_KEY từ môi trường/secret manager; không ghi key vào source.
export CONSUMER_ID='crm-demo-01'
node websocket-client.cjs// Node.js >=24.2. Backend only. npm install ws
const WebSocket = require('ws');
const { DatabaseSync } = require('node:sqlite');
const base = 'https://dev-connect.zework.com';
const apiKey = process.env.PARTNER_API_KEY;
const consumerId = process.env.CONSUMER_ID || 'crm-demo-01';
if (!apiKey) throw Error('Thiếu biến môi trường PARTNER_API_KEY');
// Durable inbox demo. Chạy một process trên file này, giữ file khi restart.
const db = new DatabaseSync('connect-inbox.sqlite');
db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
CREATE TABLE IF NOT EXISTS inbox (
event_id TEXT PRIMARY KEY, cursor INTEGER, type TEXT, payload TEXT
)`);
const insert = db.prepare('INSERT OR IGNORE INTO inbox VALUES(?,?,?,?)');
let stopped = false, socket, attempt = 0;
process.once('SIGINT', () => { stopped = true; socket?.close(); });
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function session(token) {
return new Promise(resolve => {
const ws = socket = new WebSocket(base.replace('https:', 'wss:') + '/v1/ws', {
handshakeTimeout: 10000
});
const authTimer = setTimeout(() => ws.terminate(), 15000);
ws.on('open', () => ws.send(JSON.stringify({type:'auth', token, consumerId})));
ws.on('message', raw => {
try {
const frame = JSON.parse(String(raw));
if (frame.type === 'ready') {
clearTimeout(authTimer);
console.log('Đã kết nối. Cursor ACK:', frame.cursor);
return;
}
if (frame.type !== 'event') return;
const e = frame.event;
// Commit vào inbox trước ACK. Trùng eventId không ghi lại.
const result = insert.run(e.eventId, e.cursor, e.type, JSON.stringify(e));
console.log(result.changes ? 'Đã lưu' : 'Đã có', e.type, e.eventId);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({type:'ack', cursor:e.cursor}));
attempt = 0;
}
// Worker nghiệp vụ đọc inbox riêng; không xử lý việc dài trước ACK.
} catch {
console.error('Không lưu được event: chưa ACK, sẽ nhận lại.');
ws.close();
}
});
ws.on('error', () => ws.terminate());
ws.on('close', (code, reason) => {
clearTimeout(authTimer);
console.log('Socket đóng:', code, String(reason));
if (code === 4409) {
console.error('consumerId đang được dùng. Dừng process trùng trước.');
stopped = true;
}
resolve();
});
});
}
async function main() {
while (!stopped) {
try {
// Luôn xin token mới cho mỗi lần nối. Không log key/token.
const res = await fetch(base + '/v1/socket-tokens', {
method:'POST', headers:{Authorization:'Bearer ' + apiKey,
'Content-Type':'application/json'}, body:'{}',
signal:AbortSignal.timeout(10000)
});
if ([401,403].includes(res.status)) {
console.error('API key không hợp lệ hoặc không có quyền.'); break;
}
if (!res.ok) throw Error('TOKEN_HTTP_' + res.status);
const {token} = await res.json();
if (!stopped) await session(token);
} catch {
console.error('Chưa kết nối được; sẽ thử lại.');
}
if (!stopped) await sleep(Math.min(30000, 1000 * 2 ** Math.min(attempt++,5))
+ Math.floor(Math.random()*1000));
}
db.close();
}
main().catch(() => { console.error('Client dừng do lỗi nội bộ.'); process.exitCode=1; });Kết quả mong đợi: “Đã kết nối. Cursor ACK: …”, sau đó “Đã lưu message.received …” khi có tin đến. File connect-inbox.sqlite chứa payload; không xóa file để thử reconnect. Mẫu không gửi tin và không thực thi nghiệp vụ tự động.
4. Tự kiểm tra reconnect
- Chạy client, xác nhận ready và event được lưu.
- Nhấn Ctrl+C rồi chạy lại cùng consumerId: tiếp tục từ ACK cuối.
- Để thử replay, dùng consumer thử riêng, tạm bỏ dòng gửi ACK: sau khi nối lại phải nhận đúng event chưa ACK. Khôi phục ACK sau thử.
- Không đổi consumerId để chữa mất kết nối: tên mới là luồng độc lập và có thể phát lại lịch sử từ đầu.
5. Lỗi thường gặp và thời hạn
| Hiện tượng | Cách xử lý |
|---|---|
| 401/403 khi xin token | Kiểm tra API key và quyền với ZeWork; không retry vô hạn bằng key sai. |
| INVALID_TOKEN / AUTH_TIMEOUT | Token dùng một lần, TTL 60 giây; gửi auth trong 5 giây. Xin token mới mỗi lần nối, không đặt token trong URL. |
| CONSUMER_ALREADY_CONNECTED / 4409 | Dừng process đang dùng cùng consumerId. Không chạy hai socket trùng tên. |
| ACK_MUST_MATCH_DELIVERED_EVENT | Gửi đúng frame.event.cursor hiện tại, không dùng eventId/msgId hoặc cursor đã tự tăng. |
| Socket đóng sau khi nhận event | Kiểm tra đã ACK chưa. Hạn ACK khoảng 30 giây; lưu inbox nhanh, xử lý việc dài ở worker. |
| Chỉ ready, chưa có tin mới | Kiểm tra account connected và phát sinh hoạt động thật; ready chỉ xác nhận kết nối Connect. |
| Nhận event cũ/trùng | Có thể đã lưu nhưng mất mạng trước ACK. Dedupe eventId rồi ACK; đây là hành vi phục hồi bình thường. |
| Proxy Zalo bị mất kết nối | Theo dõi account.disconnected/account.connected. Connect tự reconnect/lấy bù upstream; socket đối tác có thể vẫn mở. |
Phân biệt hai kết nối: backend của bạn ↔ Connect dùng consumerId/cursor/ACK; Connect ↔ Zalo dùng proxy, msgId và sync/verify. Bạn không cần quản lý cursor Zalo. Khả năng lấy bù vẫn phụ thuộc dữ liệu upstream còn trả về.
message.sent là gửi thành công ở adapter; message.echo là phản chiếu tin tự gửi; message.received là tin đến. zalo.delivereds báo đã giao, zalo.seens báo đã xem theo upstream. Không dùng ACK của bạn làm trạng thái đã xem.
Token socket có quyền nhận sự kiện toàn đối tác. Không cấp trực tiếp cho frontend chỉ được xem một account: backend phải lọc rồi phát lại. ws tự trả pong. Event socket không truyền âm thanh call. Bỏ qua loại event chưa biết sau khi lưu; không làm ngừng cả luồng.
Đã kiểm tra trên dev ngày 31/10/2026: auth, nhận event, replay chưa ACK, ACK rồi reconnect giữ cursor. Chưa nghiệm thu mất proxy thật với backlog lớn.
Nhóm, thành viên và reply có nhắc tên
Chọn account, lấy groupId từ danh sách nhóm. Các thao tác thay đổi cần Idempotency-Key, trả operationId; đọc kết quả operation hoặc nhận operation.succeeded/operation.failed. Chỉ dùng nhóm bạn được phép quản trị; Zalo kiểm tra quyền của tài khoản thực hiện.
| API sau /v1/accounts/{accountId} | Cách dùng |
|---|---|
| GET /groups/{groupId}/members?offset=0&limit=50 | Trả memberIds, profiles, nextOffset, availableMembers/totalMembers và complete. nextOffset=null chỉ hết danh sách đã nhận, không bảo đảm upstream đã trả đủ toàn nhóm. |
| GET /groups/{groupId}/settings | Đọc cài đặt, creatorId/adminIds. |
| POST /groups/{groupId}/settings | Body {settings:{joinAppr:1}}; merge cài đặt hiện tại khi chạy, không xóa field bạn không gửi. |
| GET /groups/{groupId}/pending-members | Xem thành viên chờ duyệt. |
| POST /groups/{groupId}/review-members | Body {memberIds:["UID"],approve:true}; false để từ chối. |
| GET /groups/{groupId}/invite-link | Xem link mời. POST thêm /renew hoặc /disable để tạo lại/tắt link. |
| GET /groups/{groupId}/blocked-members?page=1 | Danh sách bị chặn, 50 bản ghi/trang theo upstream. |
| POST /groups/{groupId}/block-members hoặc /unblock-members | Body {memberIds:["UID"]}. |
| POST /groups/{groupId}/admins/add hoặc /admins/remove | Thêm/gỡ phó nhóm, body {memberIds:["UID"]}. |
| POST /groups/{groupId}/transfer-owner | Body {newOwnerId:"UID"}; chuyển trưởng nhóm thật. |
| POST /groups/{groupId}/avatar | Body {mediaId:"media_..."}; upload ảnh trước. |
| GET /group-invitations | Danh sách lời mời vào nhóm; hiện helper chỉ trả trang đầu. |
| POST /groups/{groupId}/accept-invitation | Chấp nhận lời mời; body {}. |
Cài đặt cho phép giá trị 0/1: blockName, signAdminMsg, addMemberOnly, setTopicOnly, enableMsgHistory, lockCreatePost, lockCreatePoll, joinAppr, lockSendMsg, lockViewMember. Với lock/block, 1 là khóa. joinAppr=1 bật duyệt, enableMsgHistory=1 bật xem lịch sử. Không gửi cả object lấy từ upstream; chỉ gửi field cần đổi.
Giới hạn rõ ràng: chưa cung cấp giải tán nhóm: helper tên ApiDeleteGroup thực tế là rời nhóm. /dissolve trả GROUP_DISSOLVE_NOT_SUPPORTED (501), không âm thầm rời nhóm. Không cam kết có toàn bộ lịch sử chat, bình chọn/ghi chú hay mọi chức năng của ứng dụng Zalo.
Gửi tin nhắc tên trong nhóm
Nhập đúng chuỗi hiển thị trong text; userId lấy từ thành viên nhóm. Chỉ thêm @Tên vào text sẽ chưa tạo tag, phải có mentions. offset bắt đầu từ 0, offset/length tính bằng UTF-16 như JavaScript .length; không dùng số byte UTF-8. Không cho phép các đoạn tag chồng nhau hoặc cắt đôi emoji. Ví dụ @An dài 3 ký tự:
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: mention-001" \
-H "Content-Type: application/json" \
--data '{"recipientId":"GROUP_ID","isGroup":true,"type":"mention","text":"@An xem giúp mình nhé","mentions":[{"userId":"USER_ID","offset":0,"length":3}]}'Cũng có thể dùng type:text với cùng trường mentions. Tag nhiều người: thêm nhiều phần tử với vị trí đúng theo text; tối đa 50. Chưa có contract riêng cho @all; không giả lập bằng UID bất kỳ.
Reply trích dẫn tin nguồn và tag người
Lấy eventId từ message.received hoặc message.echo của đúng tài khoản. Service tự xác định nhóm/hội thoại và tin trích dẫn, không cần tự ghép msgId. Reply cá nhân dùng cùng API nhưng bỏ mentions. Đang hỗ trợ quote text/ảnh/file; đây là trả lời tin trong hội thoại, không phải tạo thread độc lập.
curl --fail-with-body "$BASE/v1/accounts/$ACCOUNT_ID/messages/reply" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: reply-001" \
-H "Content-Type: application/json" \
--data '{"sourceEventId":"evt_SOURCE","text":"@An mình trả lời ở đây","mentions":[{"userId":"USER_ID","offset":0,"length":3}]}'Trong các ví dụ thay GROUP_ID/USER_ID bằng ID dạng chuỗi số thực tế, evt_SOURCE bằng eventId đã nhận. API Explorer có mục Quản lý nhóm để thử từng thao tác. Nhận operation thành công không thay thế việc kiểm tra hiệu ứng/tag hiển thị trên Zalo.
Tích hợp cuộc gọi WebRTC
→ Mở trang thử call: chọn tài khoản, gọi và bắt máy bằng nút bấm
Đường WSS đã kiểm tra kết nối/xác thực; chưa nghiệm thu cuộc gọi thực và âm thanh hai chiều. Chưa hỗ trợ SIP, API tải ghi âm hoặc SDK call hoàn chỉnh. REST và WebSocket điều khiển cuộc gọi; âm thanh truyền bằng WebRTC, không qua webhook.
1. Chuẩn bị media và token
Tài khoản phải connected. Trình duyệt chạy HTTPS, người dùng chủ động bật microphone. Backend tạo token gắn với tài khoản:
curl --fail-with-body "$BASE/v1/call-tokens" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Content-Type: application/json" \
--data '{"accountId":"acc_example"}'{
"token":"ONE_USE_CALL_TOKEN", "connectionId":"agent_example",
"path":"/v1/call-ws", "tokenExpiresAt":"2026-10-31T10:01:00.000Z",
"connectionExpiresAt":"2026-10-31T11:00:00.000Z", "mode":"live"
}Browser kết nối /v1/call-ws, gửi {"type":"auth","token":"..."} trong 5 giây, chờ {"type":"ready","connectionId":"...","accountId":"..."}. Token dùng một lần trong 60 giây; phiên media có thời hạn khoảng một giờ. Token call khác token sự kiện.
2. Tạo cuộc gọi từ backend
curl --fail-with-body "$BASE/v1/calls" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: crm-call-001" \
-H "Content-Type: application/json" \
--data '{"accountId":"acc_example","recipientId":"123456789","connectionId":"agent_example"}'{
"callId":"call_example", "accountId":"acc_example", "recipientId":"123456789",
"status":"dialing", "direction":"outgoing", "result":null, "mode":"live"
}recipientId là Zalo UID dạng chuỗi 1–30 chữ số. Mỗi tài khoản chỉ một cuộc gọi đang hoạt động. 202 là chấp nhận gọi, chưa phải bên kia đã nhấc máy. Browser phải giữ socket đang ready; backend dùng đúng connectionId đó.
3. Trao đổi SDP ICE và phát âm thanh
Gọi đến: sau khi API answer thành công, browser tạo RTCPeerConnection có microphone, createOffer/setLocalDescription rồi gửi {type: "offer", sdp: pc.localDescription.sdp} qua call WebSocket. Nhận {type: "answer", sdp: "..."} từ PBX thì setRemoteDescription. Trao đổi candidate và đợi WebRTC connected. Chỉ gọi API answer mà không thương lượng media sẽ chưa có âm thanh microphone.
Gọi đi: PBX gửi webrtc_offer chứa data.sdp và data.type. Browser áp dụng remote description, thêm microphone, tạo answer rồi gửi webrtc_answer. Candidate dùng trường candidate. Một số frame data có thể là JSON string; parse trước khi dùng. Đây là khung xử lý media, không phải SDK có quản lý lifecycle đầy đủ:
// Chạy trong browser sau thao tác cho phép microphone của người dùng.
// ws là call WebSocket đã auth và ready. remoteAudio là thẻ <audio>.
const mic = await navigator.mediaDevices.getUserMedia({audio:true});
const pc = new RTCPeerConnection();
mic.getTracks().forEach(track => pc.addTrack(track, mic));
const pending = [];
pc.ontrack = ({track, streams}) => {
remoteAudio.srcObject = streams[0] || new MediaStream([track]);
remoteAudio.play().catch(() => { /* Hiển thị nút người dùng bấm để nghe */ });
};
pc.onicecandidate = ({candidate}) => {
if (candidate && ws.readyState === WebSocket.OPEN)
ws.send(JSON.stringify({type:'candidate',candidate:candidate.toJSON()}));
};
// Gọi tuần tự từ hàng đợi xử lý message của call WebSocket.
async function handleSignal(frame) {
const data = typeof frame.data === 'string' ? JSON.parse(frame.data) : frame.data;
if (frame.type === 'webrtc_offer') {
await pc.setRemoteDescription({type:'offer',sdp:data.sdp});
while (pending.length) await pc.addIceCandidate(pending.shift());
await pc.setLocalDescription(await pc.createAnswer());
ws.send(JSON.stringify({type:'webrtc_answer',data:{
type:'answer',sdp:pc.localDescription.sdp
}}));
} else if (frame.type === 'candidate') {
const c = typeof frame.candidate === 'string'
? JSON.parse(frame.candidate) : frame.candidate;
if (!c) return;
if (pc.remoteDescription) await pc.addIceCandidate(c); else pending.push(c);
}
}
// Khi kết thúc hoặc lỗi: gọi API hangup nếu cần, pc.close(),
// mic.getTracks().forEach(t => t.stop()), đóng socket và cập nhật giao diện.Đừng gửi lệnh call qua socket; tạo và điều khiển call bằng REST. Các frame như callRinging, callAnswered, call_ended, error dùng cập nhật UI. Dùng callId cấp trên cùng của frame hoặc REST; không dùng ID upstream trong data. Theo dõi iceConnectionState, xử lý từ chối mic, tự động phát bị chặn và mạng chặn UDP. Cấu hình TURN chưa được cung cấp cho khách hàng ở bản dev.
4. Nhận gọi đến và điều khiển
Giữ kênh sự kiện để nhận call.ringing, lấy data.callId, chuẩn bị call socket ready cho đúng account rồi gọi /answer. Sau đó browser chủ động gửi offer và xử lý answer theo luồng gọi đến ở trên.
| API | Payload hoặc kết quả |
|---|---|
GET /v1/calls | {items:[...]}, tối đa 100 cuộc gần nhất. |
GET /v1/calls/{callId} | Trạng thái và kết quả. Các trạng thái gồm dialing, ringing, answered, ended, failed, interrupted. |
POST /v1/calls/{callId}/answer | {"connectionId":"agent_example"} |
POST /v1/calls/{callId}/hangup | Kết thúc, cùng payload connectionId. |
POST /v1/calls/{callId}/decline | Từ chối cuộc gọi, cùng payload. |
POST /v1/calls/{callId}/hold | Giữ cuộc gọi, cùng payload. |
POST /v1/calls/{callId}/unhold | Tiếp tục cuộc gọi, cùng payload. |
Điều khiển thành công trả {"callId":"...","action":"hangup","accepted":true,"mode":"live"}; theo dõi sự kiện/GET để biết trạng thái cuối. Không dùng token của tài khoản hoặc connection khác để điều khiển cuộc gọi.
Giới hạn lỗi và xử lý sự cố
| Giới hạn dev hiện tại | Giá trị |
|---|---|
| HTTP mỗi đối tác | 600 request/phút; bộ đếm theo process, không phải quota thương mại. |
| Upload / JSON request | 5 MiB mỗi media / khoảng 1 MiB cho body JSON. |
| Hàng chờ gửi tin và kết bạn | Tối đa 1.000 queued operations mỗi đối tác. |
| Token event / call | Một lần, 60 giây để dùng; auth trong 5 giây sau mở socket. |
| Sự kiện / danh sách | 100 event/lần; 100 delivery và 100 call gần nhất. |
| WebSocket event | Tối đa 100 socket toàn instance dev; 1 event đang chờ ACK/consumer. |
{
"error":{"code":"ACCOUNT_OFFLINE","requestId":"req_example"}
}HTTP response có x-request-id. Khi nhờ hỗ trợ, gửi thời gian, requestId, operationId/callId và mã lỗi; không gửi API key, token, signing secret hoặc QR còn hạn.
| HTTP và code thường gặp | Cách xử lý |
|---|---|
| 400 · INVALID_JSON, RECIPIENT_REQUIRED, UNSUPPORTED_MESSAGE_TYPE | Kiểm tra body, MIME và trường bắt buộc. |
| 400 · IDEMPOTENCY_KEY_REQUIRED | Thêm key duy nhất cho thao tác gửi/đổi dữ liệu/tạo call. |
| 400 · WEBHOOK_ORIGIN_NOT_ALLOWLISTED | Đề nghị ZeWork cấu hình origin webhook trước. |
| 401 · UNAUTHORIZED | Kiểm tra Bearer key trên backend; liên hệ cấp lại nếu cần. |
| 403 · CALL_AGENT_MISMATCH | Điều khiển bằng đúng connection được gắn với call. |
| 404 · ACCOUNT_NOT_FOUND, MEDIA_NOT_FOUND, OPERATION_NOT_FOUND, CALL_NOT_FOUND | Kiểm tra ID và đúng API key của đối tác sở hữu. |
| 409 · ACCOUNT_OFFLINE, REAUTH_REQUIRED | Đọc trạng thái account, reconnect hoặc quét QR mới. |
| 409 · IDEMPOTENCY_PAYLOAD_MISMATCH | Giữ nguyên request khi retry, key mới chỉ cho thao tác mới. |
| 409 · MEDIA_CONNECTION_REQUIRED, PROVIDER_CALL_PENDING, ACCOUNT_BUSY | Chờ socket ready / mã call upstream; một call mỗi account. Không gọi liên tục để vượt trạng thái. |
| 413 · BODY_TOO_LARGE | Giảm kích thước file/body. |
| 429 · RATE_LIMITED, QUEUE_FULL, TOO_MANY_LOGIN_SESSIONS | Giảm tốc; backoff có jitter. Với quota phút, chờ sang phút sau. |
| 502 hoặc 503 · PBX_ACTION_FAILED, PBX_NOT_CONFIGURED, QR_UNAVAILABLE | Chờ và kiểm tra lại; kèm requestId khi báo hỗ trợ. |
| 500 · INTERNAL_ERROR | Giữ requestId; với mutation bị timeout/mất phản hồi, retry cùng idempotency key hoặc đối soát trước. |
Lỗi khi xử lý nền nằm trong GET /operations/{id}, ví dụ UPLOAD_COMPLETION_NOT_RECEIVED, UPLOAD_NOT_ASSEMBLED, ZALO_REJECTED, PROVIDER_ERROR. Đây không nhất thiết là status HTTP của request tạo tác vụ.
| Socket close code | Hướng xử lý |
|---|---|
| 4400 | Auth/frame không hợp lệ hoặc ACK sai; đọc close reason. Lấy token mới khi cần. |
| 4401 | Không auth đúng hạn hoặc phiên call hết hạn. |
| 4408 | ACK_TIMEOUT: xử lý/lưu event nhanh hơn rồi nối lại cùng consumerId. |
| 4409 | CONSUMER_ALREADY_CONNECTED: đóng kết nối cũ hoặc chọn consumer độc lập. |
| 1011 | PBX_UNAVAILABLE: backoff, kiểm tra sức khỏe và kết nối lại. |
Kiểm tra trước khi vận hành
- Kiểm tra health và capabilities; request thiếu key phải bị từ chối.
- Quét tài khoản riêng, lưu accountId, chờ connected; thử mất kết nối, reconnect và xác thực lại.
- Gửi text, ảnh, file tới người nhận thử được phép; kiểm tra operation và nội dung thực nhận.
- Retry cùng idempotency key không tạo tác vụ thứ hai; xử lý unknown_outcome bằng đối soát.
- Nhận tin đến, chống trùng eventId; thử socket mất kết nối trước/sau ACK và polling tiếp cursor.
- Nếu dùng webhook: duyệt origin, xác minh chữ ký, thử nhận trùng và lỗi tạm thời rồi replay.
- Nếu dùng call: thử gọi đi/đến, nghe và nói hai chiều, hangup, decline, mic bị từ chối và mạng thực tế.
- Chốt môi trường production, hạn mức, hỗ trợ vận hành và lưu trữ dữ liệu với ZeWork trước khi đưa khách hàng cuối vào.
Ngày cập nhật: 31/10/2026 · Tài liệu cho API dev 0.2.0. Các endpoint trong trang phản ánh bản tích hợp hiện tại; trạng thái adapter không thay thế nghiệm thu thực tế. Liên hệ đầu mối ZeWork đã cấp API key để cấu hình webhook, cấp/đổi key hoặc xử lý sự cố.