Bắt đầu với Care X API
Một profile API key dùng cho các account mà profile có quyền. Mỗi account ứng với một Zalo OA. Có hai môi trường:
environment=development— development: gửi qua chế độ development của Zalo, chỉ tới quản trị viên của Zalo App hoặc OA (người khác bị Zalo từ chối mã-127), trừ vào ví development của Zalo. Chỉ gửi qua số điện thoại.environment=live— gửi thật: mở sau khi tài khoản kết nối OA và gửi thử thành công một tin để xác minh ZBS trả phí.
Xác thực: Authorization: Bearer $PROFILE_API_KEY. Giới hạn 60 yêu cầu/phút cho mỗi API key (vượt quá trả 429 RATE_LIMITED, thử lại sau; một đợt gửi hàng loạt tính là một yêu cầu).
Tài liệu OpenAPI đầy đủ (mọi endpoint /v1 có schema yêu cầu, phản hồi và lỗi): https://api.carex.io.vn/v1/openapi.json. Dành cho AI agent (Markdown, đầy đủ endpoint, lỗi, webhook): /developers/agent-guide.md · /llms.txt.
Thiết lập qua API với profile key
Quản trị Care X tạo profile cho công ty. Công ty tạo profile key ở cổng 3000, rồi giữ key trên máy chủ của mình. App ID, App Secret, access token và refresh token được nhập tại ứng dụng công ty; máy chủ công ty chuyển cấu hình đó tới Care X qua API.
Trên trang danh sách account, mục API access của profile cho phép tạo key sau khi xác nhận mật khẩu. Key không hết hạn và không cần chọn quyền; quyền thực hiện luôn theo membership hiện tại của profile trên từng account. Chọn “Tất cả account của profile” để dùng cả account hiện tại và account tham gia/tạo sau này; hoặc “Account được chọn” để giới hạn một/nhiều account. Key giới hạn không được tạo account mới. Đổi phạm vi trong Dashboard cần xác nhận mật khẩu; mất membership hoặc hạ vai trò sẽ giảm quyền của key ở các lần gọi tiếp theo. Không cần tạo key bên trong account.
- Owner/Admin: cấu hình Zalo, mẫu, webhook và account; đồng thời đọc/gửi tin.
- Operator: quản lý người nhận, đọc/gửi/hủy/thay thế tin; không đổi cấu hình Zalo.
- Viewer: đọc trạng thái account, kết nối, mẫu và báo cáo.
Phạm vi key luôn giao với quyền hiện tại: từ Admin xuống Viewer thì mất quyền ghi; bị gỡ khỏi account thì mất truy cập, dù account vẫn nằm trong danh sách đã chọn. Đổi phạm vi tại nút Đổi phạm vi trong Dashboard; API key không được tự mở rộng phạm vi của chính nó.
curl https://api.carex.io.vn/v1/accounts -H "Authorization: Bearer $PROFILE_API_KEY"
Danh sách account được lọc theo phạm vi key và membership hiện tại. Ví dụ tạo account bên dưới cần key chọn Tất cả account của profile. Lưu account.id trả về vào ACCOUNT_ID; nếu dùng key giới hạn, chọn account từ danh sách thay vì tạo mới.
curl -X POST https://api.carex.io.vn/v1/accounts \
-H "Authorization: Bearer $PROFILE_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Chi nhánh A"}'
curl -X PUT https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/zalo/app \
-H "Authorization: Bearer $PROFILE_API_KEY" -H "Content-Type: application/json" \
-d "@$APP_CONFIG_FILE"
curl -X POST https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/zalo/tokens \
-H "Authorization: Bearer $PROFILE_API_KEY" -H "Content-Type: application/json" \
-d "@$TOKEN_CONFIG_FILE"
curl -X POST https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/templates/sync \
-H "Authorization: Bearer $PROFILE_API_KEY"Ứng dụng công ty thu thập cấu hình và gửi qua máy chủ: App config có provider_app_id, app_secret, tùy chọn oa_secret_key, appsecret_proof; token config có refresh_token và tùy chọn access_token. Secret/token được lưu mã hóa và không trả lại qua API đọc.
Tạo mẫu qua POST /v1/accounts/:accountId/template-drafts, tải logo/ảnh qua /template-media và gửi duyệt qua /template-drafts/:draftId/submit. Mẫu cần được Zalo duyệt trước khi gửi. Account mới vẫn cần kết nối OA và xác minh ZBS trước khi gửi thật. Nếu mất phản hồi khi tạo account/mẫu, đối soát danh sách trước khi tạo lại; không tự retry nhập refresh token vì Zalo có thể đã sử dụng token.
Domain được phép gọi API (CORS)
Cấu hình trong Cài đặt từng account hoặc PATCH /v1/accounts/:accountId với {"cors_origins":["https://app.cong-ty.vn"]}. Mỗi origin phải cụ thể, có giao thức và port nếu cần, không wildcard hoặc đường dẫn. Danh sách trống chặn mọi origin website trên API nghiệp vụ của account. Lời gọi máy chủ không gửi Origin vẫn hoạt động. CORS không giới hạn IP và không thay thế xác thực; website nên gọi máy chủ công ty, không đưa API key vào trình duyệt.
Mọi API nghiệp vụ bên dưới cần query environment=development hoặc environment=live. API cấu hình account/Zalo/mẫu không cần môi trường, trừ đọc thư viện mẫu trong nhóm nghiệp vụ.
1. Tạo khách hàng (tùy chọn)
Có thể bỏ qua: gửi thẳng bằng "recipient": { "phone": "0901234567" }. Tạo khách hàng khi cần gắn mã khách của hệ thống bạn và quản lý đồng ý nhận tin.
curl -X PUT "https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/contacts/external/dentalx/PATIENT-456?environment=development" \
-H "Authorization: Bearer $PROFILE_API_KEY" -H "Content-Type: application/json" \
-d '{"display_name":"Nguyễn Văn A","identities":[{"channel":"zalo_phone","value":"0901234567"}]}'Lưu id trả về làm contact_id của khách trong account. Khi gửi, có thể dùng {"contact_id":"cnt_…","phone":"0901234567"} hoặc {"contact_id":"cnt_…","phone_hash":"…"} ở recipient để chọn đúng địa chỉ. Giữ cùng mã khi khách đổi SĐT; lọc báo cáo bằng contact_id sẽ lấy đủ lịch sử qua các địa chỉ. Care X không tự gộp khách dùng chung SĐT hoặc suy ra liên hệ giữa SĐT và hash.
2. Lấy template_id và tham số
Mẫu tin thuộc OA của bạn, đồng bộ về Care X. Xem template_id và tên tham số ngay trên trang Mẫu tin của dashboard, hoặc qua API:
curl "https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/templates?environment=development&status=ENABLE" -H "Authorization: Bearer $PROFILE_API_KEY"
3. Gửi thông báo
Gửi bằng template_id với đúng tên tham số của mẫu; Care X kiểm tra đủ tham số bắt buộc, kiểu và độ dài trước khi gửi. Idempotency-Key là bắt buộc và được giữ 30 ngày: gửi lại cùng key và cùng nội dung trả về đúng phản hồi của lần tạo đầu tiên (ảnh chụp lúc tạo, không phải trạng thái hiện tại — xem trạng thái bằng GET), không tạo tin mới; cùng key khác nội dung trả 409 IDEMPOTENCY_CONFLICT. API trả 202 khi yêu cầu đã được lưu bền vững. purpose là tùy chọn, mặc định theo loại mẫu: transaction (giao dịch), customer_care, promotion, otp; loại khác là general.
curl -X POST "https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/notifications?environment=development" \
-H "Authorization: Bearer $PROFILE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: dentalx-apt-123-r4-reminder" \
-d '{
"template_id": "639714",
"recipient": { "contact_id": "cnt_…" },
"parameters": { "ten_kh": "Nguyễn Văn A", "gio_hen": "09:00", "ngay_hen": "05/10/2026", "phong_kham": "Quận 1" },
"schedule_at": "2026-10-04T09:00:00+07:00",
"event_at": "2026-10-05T09:00:00+07:00",
"external_reference": { "namespace": "dentalx", "type": "appointment", "id": "APT-123", "revision": 4 }
}'Hẹn giờ: thêm schedule_at vào body gửi tin, ví dụ 2026-10-05T09:00:00+07:00 là 09:00 giờ Việt Nam. API trả 202 với trạng thái scheduled nếu còn hơn một giây mới tới lịch. Thời điểm này là lúc bắt đầu được phép gửi; thời gian tới máy còn phụ thuộc hàng đợi và Zalo. Có thể đặt expires_at làm hạn chót, ví dụ 2026-10-05T09:30:00+07:00 để bỏ tin nếu quá muộn. Mặc định tin hết hạn 24 giờ sau lịch gửi đã chuẩn hóa. Mỗi item trong batch cũng nhận các trường này; thiếu hoặc đặt giờ đã qua dùng giờ hợp lệ sớm nhất từ hiện tại.
Phần mềm công ty tự quét khách rồi gửi lịch: sinh nhật hẹn 07:00; nhắc hẹn lúc 08:30 cho cuộc hẹn 09:00 nên đặt expires_at là 09:00 (kèm múi giờ). Care X dành riêng năng lực cho tin gửi ngay và tin có hạn gửi trong vòng một giờ sau lịch, đồng thời tiếp tục xử lý đợt hàng loạt. Mục tiêu nội bộ cho sinh nhật là gửi sang Zalo trong 30 phút; tin không tự hết hạn lúc 07:30.
Chính sách Zalo dùng chung toàn hệ thống: dự kiến từ 15/10/2026, Tag 1 gửi 24/24; Tag 2/3 chỉ gửi 07:00 đến trước 22:00, giờ Việt Nam, áp dụng mọi tuyến gửi. Lịch ban đêm của Tag 2/3 chuyển về 21:00 trước đó nếu chưa qua; nếu đã qua, chuyển sang 07:00 sau lịch yêu cầu. Xem scheduled_at trả về để biết lịch thực tế. Tag lấy từ mẫu Zalo đã đồng bộ; account không có cấu hình giờ gửi riêng.
Worker kiểm tra lại chính sách, giờ hiện tại, hạn gửi và quyền profile/key trên account khi gửi. Tin xử lý trễ ban đêm chờ giờ mở; quá hạn thì bỏ. Gửi thử ngoài giờ áp dụng sẽ báo lỗi để thử lại sau. Tham khảo thông báo Zalo ngày 01/10/2026.
Hết hạn mức Zalo: tin cùng phạm vi OA, mẫu hoặc người nhận được giữ và thử vào kỳ ngày/tháng tiếp theo, trong giờ cho phép nếu còn hạn. Sự kiện tạm giữ có retry_at. Thiếu số dư hoặc thanh toán ZBS thất bại sẽ tạm ngưng gửi thật; doanh nghiệp khắc phục rồi chọn tiếp tục gửi. Không tự gửi lại tin chưa rõ kết quả.
Đổi lịch: gửi lại với revision lớn hơn (thông báo cũ chưa gửi sẽ bị thay thế) hoặc dùng POST /v1/accounts/:accountId/notifications/:id/replace?environment=live. Revision cũ hơn trả 409 STALE_REVISION; cùng revision khi tin trước còn đang xử lý trả 409 REVISION_CONFLICT (tăng revision để thay thế). Hủy trước khi gửi: POST /v1/accounts/:accountId/notifications/:id/cancel?environment=live — tin đã bắt đầu gửi trả 409 DISPATCH_ALREADY_STARTED, tin đã kết thúc (đã tới máy, thất bại, hết hạn; với replace cả tin đã hủy) trả 409 NOTIFICATION_FINAL.
Lỗi luôn có dạng { "error": { "code", "message", "retryable", "details"? }, "request_id" }. Xử lý theo code, không theo message. Trường details[].path chỉ ra trường sai, ví dụ số điện thoại không hợp lệ: 422 VALIDATION_FAILED với path /recipient/phone; thời điểm gửi ngoài ±7 ngày quanh sự kiện: 422 OUTSIDE_EVENT_WINDOW với path /event_at.
4. Nhận webhook
Owner/Admin dùng cùng profile key để tạo endpoint bằng POST /v1/accounts/:accountId/webhook-endpoints với {url, environment, subscriptions?, make_default?}, rồi xác minh qua /webhook-endpoints/:endpointId/verify. Secret ký webhook chỉ hiển thị khi tạo hoặc xoay; đây là secret riêng, không dùng profile API key để kiểm tra chữ ký.
Mỗi sự kiện được ký: X-CareX-Signature: t=…,v1=… với v1 = HMAC-SHA256(secret, t + "." + event_id + "." + raw_body), dùng nguyên chuỗi secret whsec_… làm khóa. Kiểm tra chữ ký trên body thô và khử trùng theo id sự kiện. Giới hạn lệch thời gian 300 giây do hệ thống nhận tự kiểm tra (Care X không chặn). Mỗi lần gửi lại (retry hoặc gửi lại thủ công) đều được ký lại với t mới và body giữ nguyên, nên tin gửi lại không bị loại vì quá hạn.
import crypto from 'node:crypto';
export function verify(rawBody, eventId, header, secrets) {
const parts = Object.groupBy(header.split(','), (p) => p.split('=')[0]);
const t = parts.t?.[0]?.split('=')[1];
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const signed = `${t}.${eventId}.${rawBody}`;
return secrets.some((s) => {
const expected = crypto.createHmac('sha256', s).update(signed).digest('hex');
return (parts.v1 ?? []).some((p) => {
const got = p.split('=')[1];
return got.length === 64 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
});
});
}5. Gửi hàng loạt
Tối đa 1.000 tin mỗi đợt, một Idempotency-Key cho cả đợt. Người nhận có thể là contact_id, phone (Care X tự tìm hoặc tạo khách hàng) hoặc phone_hash. Dòng sai — kể cả sai định dạng (thiếu trường, sai kiểu, contact_id hỏng) — bị loại riêng và trả lỗi theo index (kèm details[].path tính trong dòng đó); các dòng khác vẫn được nhận. Chỉ khi cả yêu cầu sai (không có items, quá 1.000 dòng) hoặc tài khoản đang tạm ngưng gửi thật thì cả đợt bị từ chối.
curl -X POST "https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/notifications/batch?environment=live" \
-H "Authorization: Bearer $PROFILE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: dentalx-reminders-2026-10-01" \
-d '{
"name": "Nhắc lịch ngày 01/10",
"items": [
{ "template_id": "639714", "recipient": { "phone": "0901234567", "name": "Lan" },
"parameters": { "ten_kh": "Lan", "gio_hen": "09:00", "ngay_hen": "01/10/2026", "phong_kham": "Quận 1" } }
]
}'
# → { "batch_id": "bat_…", "accepted": 1, "rejected": 0, "results": [{ "index": 0, "status": "accepted", "notification_id": "ntf_…" }] }
curl "https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/batches/bat_…?environment=live" -H "Authorization: Bearer $PROFILE_API_KEY" # tiến độ theo trạng thái, chi phí6. Không muốn chia sẻ số điện thoại: phone_hash
Gửi "recipient": { "phone_hash": "<sha256 hex>" } — Care X gửi qua API hash phone của Zalo và không bao giờ nhận số thật. Băm số ở dạng chuẩn hóa 84xxxxxxxxx (Zalo chưa ghi rõ dạng chuẩn hóa — đang kiểm chứng). Chỉ dùng ở môi trường gửi thật.
7. Khi Zalo tạm ngưng gửi (hết số dư ZBS, mất token…)
Khi Zalo báo lỗi, Care X tạm ngưng gửi thật: tin đang chờ được giữ lại (chỉ tin chưa gửi mới được gửi lại, không bao giờ gửi lại tin không rõ kết quả), tin mới bị từ chối với 409 ACCOUNT_NOT_READY và hướng dẫn khắc phục, webhook nhận account.sending_suspended. Trạng thái hiện tại: GET /v1/accounts/:accountId/account?environment=live → zalo_connection.sending_suspended và suspension.cause:
provider_route— lỗi của tuyến gửi (-115hết số dư ZBS,-137thanh toán lỗi,-136App chưa liên kết ZBS…): sau khi xử lý, người quản trị bấm “Tiếp tục gửi” trên dashboard hoặc ứng dụng công ty gọiPOST /v1/accounts/:accountId/zalo/resume. Kết nối lại OA không gỡ loại tạm ngưng này.repeated_provider_failures— mặc định 5 tin khác nhau lỗi bất thường liên tiếp trong 10 phút (lỗi chưa phân loại, timeout/phản hồi bất thường hoặc lỗi tạm thời trước gửi). Xemsuspension.failuresđể biết số lỗi, thời điểm và mã lỗi gần nhất. Doanh nghiệp kiểm tra nguyên nhân rồi dùng cùng luồng resume có tin thử tính phí. Tin đã failed/unknown không tự gửi lại. Lỗi người nhận/mẫu/quota, điều kiện nội bộ và development không cộng vào ngưỡng.reauth_required— token OA không còn dùng được: kết nối lại OA (dán refresh token mới). Kết nối lại xong, gửi thật vẫn tạm ngưng. Chủ động bấm “Tiếp tục gửi” hoặc gọi API resume; Care X kiểm tra App/Secret, token/OA, mẫu và gửi một tin thử tính phí trước khi kích hoạt.
Khi hết tạm ngưng, Care X phát account.sending_resumed một lần và gửi lại các tin đang giữ; tin giữ quá expires_at chuyển sang expired.
Resume cần số nhận, mẫu và tham số do doanh nghiệp chỉ định, cùng Idempotency-Key. Chỉ khi Zalo nhận tin thử tính phí và quyền/cấu hình còn đúng, account mới mở gửi. Cùng key và body trả kết quả đã lưu; 202 là đang kiểm tra. Kết quả unknown cần đối soát, không tự gửi lại với key mới. Một tin thử thành công không bảo đảm số dư/quota cho toàn bộ hàng đợi. Tin thử ghi ở audit, không thuộc báo cáo tin nghiệp vụ.
curl -X POST "https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/zalo/resume" \
-H "Authorization: Bearer $PROFILE_API_KEY" \
-H "Idempotency-Key: $ACTIVATION_REQUEST_ID" \
-H 'Content-Type: application/json' \
-d '{"phone":"0901234567","template_id":"YOUR_TEMPLATE_ID","template_data":{"customer_name":"Nguyen Van A"}}'8. Đối soát và báo cáo
Nếu bỏ lỡ webhook, đọc GET /v1/accounts/:accountId/events?environment=live&cursor=… — cùng định dạng sự kiện, cursor gắn với account và môi trường được chọn; sự kiện lưu 90 ngày, cursor quá hạn trả 410 CURSOR_EXPIRED (đọc lại từ đầu, không truyền cursor). Báo cáo: GET /v1/accounts/:accountId/reports/summary?environment=live&from=…&to=… (số tin theo trạng thái, tỉ lệ tới máy, lý do lỗi hàng đầu, chi phí, chia theo template_id, số công việc chưa xử lý phát sinh trong kỳ).
Dùng GET /v1/accounts/:accountId/reports/daily cho chart theo ngày và /reports/details cho danh sách đối soát từng tin. Cả ba báo cáo cần environment, hỗ trợ from/to, phone, contact_id, template_id và status. Khoảng thời gian tối đa 90 ngày; mặc định 30 ngày. Chọn date_field=accepted_at (nhận yêu cầu), scheduled_at (lịch hiệu lực), dispatched_at (gửi sang Zalo) hoặc delivered_at (giao tin). Chart mặc định giờ Việt Nam, có thể chọn timezone=UTC; ngày không có dữ liệu vẫn trả số 0.
curl -G https://api.carex.io.vn/v1/accounts/$ACCOUNT_ID/reports/daily \ -H "Authorization: Bearer $PROFILE_API_KEY" \ --data-urlencode "environment=live" \ --data-urlencode "from=2026-09-01T00:00:00+07:00" \ --data-urlencode "to=2026-10-01T00:00:00+07:00" \ --data-urlencode "date_field=delivered_at"
Chi tiết đối soát gồm mã tin Zalo, các mốc nhận/gửi/giao, trạng thái và chi phí ước tính, đã ghi nhận, giải phóng và điều chỉnh. SĐT trả về được che; chi phí chưa có giá là null. Đây là số liệu Care X ghi nhận; đối chiếu báo cáo chi tiêu ZBS khi chốt chi phí chính thức.
Phân trang
Danh sách account, mẫu, bản nháp, webhook, lịch sử webhook, tin, đợt gửi và chi tiết đối soát dùng limit và cursor. Mặc định 50, tối đa 100 mục/trang; lấy next_cursor cho lần gọi tiếp theo và giữ nguyên bộ lọc. has_more=false và next_cursor=null là hết danh sách. Cursor được ký, gắn với tài nguyên/account/môi trường/bộ lọc tương ứng; thay bộ lọc thì đọc lại từ đầu. Events là luồng riêng: mặc định 100, tối đa 200 mục, giữ cursor cuối để tiếp tục nhận sự kiện mới. API giới hạn 60 request/phút/profile key; vượt giới hạn trả 429.
API account key cũ được giữ để tương thích. Tích hợp mới dùng profile key và đường dẫn có account như hướng dẫn này; không cần tạo key riêng cho từng account.