- Tích hợp Zalo ZNS API yêu cầu Zalo OA đã xác thực, App ID + Secret, và template được duyệt trước khi gửi tin.
- Quy trình 5 bước: Tạo Zalo App → Lấy access token (OAuth 2.0) → Tạo và duyệt template → Gửi ZNS qua API → Xử lý webhook callback.
- Rate limit từ 10 đến 50 request/giây tùy gói, timeout mỗi request 30 giây, template phê duyệt trong 1-3 ngày làm việc.
- Non-developer có thể dùng công cụ no-code (Zalo Broadcast, tích hợp qua nền tảng trung gian) để gửi ZNS mà không cần viết code.
Tích Hợp Zalo ZNS API: Hướng Dẫn Kỹ Thuật Từng Bước Cho Developer và Non-Dev
Tích hợp Zalo ZNS API cho phép hệ thống của bạn tự động gửi thông báo giao dịch - xác nhận đơn hàng, OTP, nhắc lịch hẹn - trực tiếp vào ứng dụng Zalo của khách hàng với tỷ lệ mở trên 80%. Bài hướng dẫn này đi qua toàn bộ quy trình kỹ thuật: từ cấp phép OAuth 2.0, code mẫu PHP và Python, xử lý webhook, đến checklist go-live - kèm phần dành riêng cho non-developer muốn dùng ZNS mà không cần lập trình.
ZNS (Zalo Notification Service) là kênh thông báo chính thức của Zalo dành cho doanh nghiệp, hoạt động qua Zalo Official Account (OA) đã xác thực. Khác với tin nhắn OA thông thường bị giới hạn trong 48 giờ sau tương tác, ZNS cho phép gửi thông báo ngoài khung giờ đó - miễn là nội dung mang tính giao dịch và đã được Zalo phê duyệt template.
Trước khi đọc phần code, hãy kiểm tra 2 điều kiện tiên quyết: Zalo OA của bạn đã có tick xác thực (tick vàng hoặc tick xanh) chưa, và doanh nghiệp đã đăng ký dịch vụ ZNS chưa. Nếu chưa, xem hướng dẫn đăng ký Zalo ZNS trước khi tiếp tục.
1. Tổng Quan Kiến Trúc ZNS API

ZNS API hoạt động theo mô hình REST API chuẩn, xác thực qua OAuth 2.0. Mỗi request gửi tin phải kèm access token hợp lệ trong header - token này có thời hạn và cần làm mới định kỳ. Luồng dữ liệu đi theo hướng: hệ thống của bạn gọi API Zalo → Zalo xử lý và đẩy tin → Zalo gọi webhook về server của bạn để thông báo kết quả gửi.
| Thành phần | Mô tả | Ghi chú |
|---|---|---|
| Zalo OA | Official Account đã xác thực | Bắt buộc có tick xác thực |
| Zalo App | Ứng dụng tạo tại developers.zalo.me | Lấy App ID và Secret key |
| Access Token | OAuth 2.0 bearer token | Có hạn, cần refresh |
| ZNS Template | Mẫu nội dung được Zalo duyệt trước | Duyệt trong 1-3 ngày làm việc |
| Webhook URL | Endpoint nhận kết quả gửi tin | Phải dùng HTTPS, port 443 |
Base URL của ZNS API là https://business.openapi.zalo.me. Toàn bộ request phải dùng HTTPS, method POST cho các thao tác gửi tin, và trả về response dạng JSON. Zalo không hỗ trợ HTTP thông thường ở môi trường production.
2. Điều Kiện Tiên Quyết Trước Khi Tích Hợp
Thiếu bất kỳ điều kiện nào dưới đây, toàn bộ request sẽ bị từ chối. Kiểm tra danh sách này trước khi viết một dòng code.
Với bộ phận kỹ thuật (developer): Checklist môi trường
Developer cần chuẩn bị đầy đủ 4 thứ sau để tích hợp thành công ngay lần đầu. Thiếu 1 trong số này, API sẽ trả lỗi xác thực hoặc lỗi permission mà không rõ nguyên nhân.
- Zalo OA đã xác thực: Tài khoản doanh nghiệp có tick xanh hoặc tick vàng. OA chưa xác thực không được phép gọi ZNS API.
- Zalo App với quyền ZNS: Tạo app tại developers.zalo.me, liên kết với OA, và bật permission "Zalo Notification Service" trong phần cấu hình app.
- App ID và App Secret: Hai giá trị này dùng để đổi lấy access token. Lưu ở biến môi trường, không hardcode trong source code.
- Ít nhất 1 template ZNS đã được duyệt: Template phải tạo và nộp phê duyệt trước. Gửi tin với template chưa duyệt → lỗi ngay lập tức.
- Server có IP tĩnh (khuyến nghị): Để cấu hình whitelist và nhận webhook ổn định. Không bắt buộc ở sandbox nhưng cần thiết ở production.
Với bộ phận vận hành (non-dev): Những gì cần chuẩn bị
Với người không viết code, ZNS vẫn hoàn toàn khả dụng qua 2 hướng: (1) dùng tính năng gửi chiến dịch trực tiếp trên Zalo Business Solutions mà không cần API, hoặc (2) kết nối ZNS qua nền tảng trung gian như CRM, marketing automation có sẵn connector ZNS. Phần quan trọng nhất bộ phận vận hành phải xử lý là soạn nội dung template đúng format và phối hợp với Zalo để template qua duyệt - đây là bước bottleneck thực tế trong hầu hết dự án ZNS, không phải phần code.
3. Lấy Access Token - OAuth 2.0 Từng Bước

ZNS API xác thực theo OAuth 2.0 Authorization Code Flow. Quy trình gồm 2 bước: đổi authorization code lấy access token và refresh token, sau đó dùng refresh token để gia hạn khi access token hết hạn. Access token thường có hiệu lực trong 1 giờ (3600 giây); refresh token có hiệu lực dài hơn nhưng cũng cần quản lý cẩn thận.
| Endpoint | Method | Mô tả |
|---|---|---|
https://oauth.zaloapp.com/v4/oa/access_token |
POST | Đổi authorization code lấy access token |
https://oauth.zaloapp.com/v4/oa/access_token |
POST | Refresh access token bằng refresh token |
https://business.openapi.zalo.me/message/template |
POST | Gửi ZNS theo template ID |
https://business.openapi.zalo.me/zns/template/info |
GET | Lấy thông tin template ZNS |
https://business.openapi.zalo.me/zns/template/all-templates |
GET | Danh sách tất cả template |
https://business.openapi.zalo.me/message/log |
GET | Lấy log kết quả gửi tin |
Headers bắt buộc trong mọi request sau khi đã có access token:
Content-Type: application/json
access_token: {your_access_token}
Lưu ý: header xác thực của ZNS dùng key là access_token (không phải Authorization: Bearer như nhiều REST API khác). Đây là điểm dễ nhầm nhất khi lần đầu tích hợp.
4. Gửi ZNS Template - Code Mẫu PHP
Ví dụ dưới đây minh họa luồng hoàn chỉnh: refresh token → gửi ZNS. Trong môi trường production, bạn nên lưu access token vào cache (Redis, Memcached) và chỉ refresh khi token hết hạn - không gọi API refresh trước mỗi lần gửi tin vì sẽ bị rate limit.
<?php
// ZNS API - Gửi tin nhắn template (PHP)
// Yêu cầu: PHP 7.4+, ext-curl, ext-json
class ZaloZNS {
private string $baseUrl = 'https://business.openapi.zalo.me';
private string $oauthUrl = 'https://oauth.zaloapp.com/v4/oa/access_token';
private string $appId;
private string $appSecret;
private string $refreshToken;
public function __construct(string $appId, string $appSecret, string $refreshToken) {
$this->appId = $appId;
$this->appSecret = $appSecret;
$this->refreshToken = $refreshToken;
}
/**
* Lấy access token mới từ refresh token
* Token mới có hiệu lực 3600 giây (1 giờ)
*/
public function getAccessToken(): string {
$payload = http_build_query([
'refresh_token' => $this->refreshToken,
'app_id' => $this->appId,
'grant_type' => 'refresh_token',
]);
$ch = curl_init($this->oauthUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'secret_key: ' . $this->appSecret,
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new RuntimeException("OAuth error: HTTP $httpCode");
}
$data = json_decode($response, true);
if (empty($data['access_token'])) {
throw new RuntimeException('Không nhận được access_token: ' . $response);
}
return $data['access_token'];
}
/**
* Gửi tin ZNS theo template ID
*
* @param string $phone Số điện thoại người nhận (định dạng 84xxxxxxxxx)
* @param string $templateId ID template đã được Zalo duyệt
* @param array $params Các tham số thay thế trong template
* @param string $trackingId ID dùng để đối soát sau khi nhận webhook
*/
public function sendZNS(
string $phone,
string $templateId,
array $params,
string $trackingId = ''
): array {
$accessToken = $this->getAccessToken();
// Chuẩn hóa số điện thoại: 0xxx → 84xxx
$phone = preg_replace('/^0/', '84', $phone);
$body = [
'phone' => $phone,
'template_id' => $templateId,
'template_data' => $params,
'tracking_id' => $trackingId ?: uniqid('zns_'),
];
$ch = curl_init($this->baseUrl . '/message/template');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'access_token: ' . $accessToken,
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
// error = 0 là thành công theo quy ước của Zalo API
if (($result['error'] ?? -1) !== 0) {
error_log("ZNS send failed [{$phone}]: " . $response);
}
return $result;
}
}
// --- Sử dụng ---
$zns = new ZaloZNS(
appId: getenv('ZALO_APP_ID'),
appSecret: getenv('ZALO_APP_SECRET'),
refreshToken: getenv('ZALO_REFRESH_TOKEN')
);
$result = $zns->sendZNS(
phone: '0912345678',
templateId: '123456',
params: [
'customer_name' => 'Nguyễn Văn A',
'order_id' => 'ORD-2026-001',
'order_total' => '1.250.000đ',
],
trackingId: 'order_001_' . time()
);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
Lỗi phổ biến nhất với PHP: quên chuẩn hóa số điện thoại về dạng 84xxxxxxxxx. Zalo không nhận số có dạng 0xxx hoặc +84xxx. Regex preg_replace('/^0/', '84', $phone) trong ví dụ trên xử lý trường hợp này.
5. Gửi ZNS Template - Code Mẫu Python

Ví dụ Python sử dụng thư viện requests (cài bằng pip install requests). Cấu trúc tương tự PHP nhưng tận dụng dataclass và context manager để quản lý session HTTP hiệu quả hơn - giảm overhead kết nối khi gửi số lượng lớn.
"""
ZNS API - Gửi tin nhắn template (Python)
Yêu cầu: Python 3.8+, requests>=2.28.0
Cài đặt: pip install requests
"""
import os
import re
import time
import logging
from dataclasses import dataclass, field
from typing import Any
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
ZALO_OAUTH_URL = "https://oauth.zaloapp.com/v4/oa/access_token"
ZALO_SEND_URL = "https://business.openapi.zalo.me/message/template"
TOKEN_EXPIRE_BUFFER = 300 # Refresh sớm 5 phút trước khi hết hạn
@dataclass
class ZaloZNS:
app_id: str
app_secret: str
refresh_token: str
_access_token: str = field(default="", init=False, repr=False)
_token_expire_at: float = field(default=0.0, init=False, repr=False)
_session: requests.Session = field(default_factory=requests.Session, init=False, repr=False)
def _refresh_access_token(self) -> None:
"""Đổi refresh token lấy access token mới."""
response = self._session.post(
ZALO_OAUTH_URL,
data={
"refresh_token": self.refresh_token,
"app_id": self.app_id,
"grant_type": "refresh_token",
},
headers={"secret_key": self.app_secret},
timeout=30,
)
response.raise_for_status()
data = response.json()
if "access_token" not in data:
raise ValueError(f"Không nhận được access_token: {data}")
self._access_token = data["access_token"]
expires_in = int(data.get("expires_in", 3600))
self._token_expire_at = time.time() + expires_in - TOKEN_EXPIRE_BUFFER
logger.info("Đã làm mới access token, hết hạn sau %d giây", expires_in)
def _get_valid_token(self) -> str:
"""Trả về access token còn hạn, tự động refresh nếu cần."""
if not self._access_token or time.time() >= self._token_expire_at:
self._refresh_access_token()
return self._access_token
@staticmethod
def _normalize_phone(phone: str) -> str:
"""Chuẩn hóa SĐT về dạng 84xxxxxxxxx."""
phone = re.sub(r"\D", "", phone)
if phone.startswith("0"):
phone = "84" + phone[1:]
elif not phone.startswith("84"):
phone = "84" + phone
return phone
def send_zns(
self,
phone: str,
template_id: str,
template_data: dict[str, Any],
tracking_id: str = "",
) -> dict[str, Any]:
"""
Gửi tin ZNS theo template đã duyệt.
Args:
phone: Số điện thoại người nhận (mọi định dạng đều được)
template_id: ID template ZNS đã được Zalo phê duyệt
template_data: Dict các tham số thay thế trong template
tracking_id: ID tự định nghĩa để đối soát webhook
Returns:
dict chứa kết quả từ Zalo API (error=0 là thành công)
"""
phone = self._normalize_phone(phone)
tracking_id = tracking_id or f"zns_{int(time.time() * 1000)}"
token = self._get_valid_token()
payload = {
"phone": phone,
"template_id": template_id,
"template_data": template_data,
"tracking_id": tracking_id,
}
response = self._session.post(
ZALO_SEND_URL,
json=payload,
headers={
"Content-Type": "application/json",
"access_token": token,
},
timeout=30,
)
response.raise_for_status()
result = response.json()
if result.get("error", -1) != 0:
logger.warning("ZNS gửi thất bại [%s]: %s", phone, result)
else:
logger.info("ZNS gửi thành công [%s] tracking=%s", phone, tracking_id)
return result
# --- Sử dụng ---
if __name__ == "__main__":
zns = ZaloZNS(
app_id = os.environ["ZALO_APP_ID"],
app_secret = os.environ["ZALO_APP_SECRET"],
refresh_token = os.environ["ZALO_REFRESH_TOKEN"],
)
result = zns.send_zns(
phone = "0912345678",
template_id = "123456",
template_data = {
"customer_name": "Nguyễn Văn A",
"order_id": "ORD-2026-001",
"order_total": "1.250.000đ",
},
tracking_id = "order_001",
)
import json
print(json.dumps(result, ensure_ascii=False, indent=2))
Điểm khác biệt quan trọng trong Python: dùng requests.Session để tái sử dụng kết nối TCP, giảm latency đáng kể khi gửi hàng trăm tin liên tiếp. Nếu cần gửi song song, dùng concurrent.futures.ThreadPoolExecutor với tối đa 5-10 worker - không nên vượt quá rate limit của tier bạn đang dùng.
6. Xử Lý Webhook và Callback
Webhook là cơ chế Zalo gọi ngược về server của bạn để thông báo kết quả gửi tin - thành công, thất bại, hoặc người dùng đã đọc. Đây là phần quan trọng nhất để đảm bảo hệ thống biết chính xác tin có đến tay khách hàng hay không.
Cấu hình webhook URL
Webhook URL phải là HTTPS, cổng 443, và server phải trả về HTTP 200 trong vòng 5 giây. Nếu server trả về code khác hoặc timeout, Zalo sẽ retry tối đa 3 lần. Đặt webhook URL tại trang quản lý Zalo App trên developers.zalo.me, mục "Webhook".
Cấu trúc payload webhook
Zalo POST một JSON đến webhook URL của bạn với cấu trúc sau:
// Payload webhook ZNS - kết quả gửi tin
{
"app_id": "123456789",
"oa_id": "987654321",
"event_name": "user_received_message", // hoặc "user_seen_message"
"timestamp": "1716600000000",
"sender": {
"id": "oa_id"
},
"recipient": {
"id": "user_zalo_id"
},
"message": {
"msg_id": "zns_msg_id_abc123",
"tracking_id": "order_001", // tracking_id bạn đã gửi lên
"sent_time": "1716600000000"
}
}
// Payload khi gửi thất bại
{
"event_name": "send_zns_failed",
"message": {
"tracking_id": "order_001",
"error_code": "-124",
"error_msg": "User not use Zalo"
}
}
Xử lý webhook trong PHP
<?php
// webhook.php - Endpoint nhận callback từ Zalo ZNS
header('Content-Type: application/json');
// Đọc payload từ body
$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);
if (!$payload) {
http_response_code(400);
exit(json_encode(['error' => 'Invalid JSON']));
}
$eventName = $payload['event_name'] ?? '';
$trackingId = $payload['message']['tracking_id'] ?? '';
$errorCode = $payload['message']['error_code'] ?? null;
// Ghi log để debug
error_log("ZNS Webhook [{$eventName}] tracking={$trackingId}");
switch ($eventName) {
case 'user_received_message':
// Tin đã đến máy người dùng - cập nhật DB
updateOrderZNSStatus($trackingId, 'delivered');
break;
case 'user_seen_message':
// Người dùng đã đọc tin
updateOrderZNSStatus($trackingId, 'read');
break;
case 'send_zns_failed':
// Gửi thất bại - log lỗi, xem xét fallback sang SMS
error_log("ZNS failed tracking={$trackingId} code={$errorCode}");
triggerSMSFallback($trackingId);
break;
}
// Phải trả 200 trong 5 giây - xử lý nặng thì đưa vào queue
http_response_code(200);
echo json_encode(['status' => 'ok']);
Nguyên tắc quan trọng: webhook handler phải trả về HTTP 200 trước khi xử lý logic nghiệp vụ. Nếu cần cập nhật database, gửi email nội bộ, hay trigger workflow khác - đưa vào message queue (Redis, RabbitMQ) và xử lý bất đồng bộ. Webhook timeout 5 giây là cứng, Zalo không chờ lâu hơn.
7. Rate Limit và Xử Lý Lỗi
Vi phạm rate limit là nguyên nhân phổ biến nhất khiến hệ thống ZNS bị gián đoạn ở production. Hiểu rõ giới hạn của từng tier và xây dựng cơ chế retry đúng cách từ đầu sẽ tiết kiệm nhiều thời gian debug sau này.
Bảng rate limit ZNS theo tier
| Tier | Giới hạn gửi | Phù hợp |
|---|---|---|
| Cơ bản | 10 request/giây | SME, dưới 100k tin/tháng |
| Nâng cao | 20-30 request/giây | Doanh nghiệp vừa, ecommerce |
| Enterprise | 50+ request/giây | Tập đoàn, ngân hàng, OTT |
Các mã lỗi quan trọng cần xử lý
| Error Code | Ý nghĩa | Xử lý |
|---|---|---|
0 |
Thành công | Cập nhật trạng thái "sent" |
-124 |
Người dùng không dùng Zalo | Fallback sang SMS; không retry |
-123 |
Người dùng bị block OA | Đánh dấu "blocked"; không retry |
-201 |
Template không tồn tại hoặc chưa duyệt | Kiểm tra lại template ID |
-216 |
Vượt rate limit | Exponential backoff, retry sau 1-5s |
-101 |
Access token không hợp lệ hoặc hết hạn | Refresh token và thử lại 1 lần |
-1000 |
Lỗi hệ thống Zalo | Retry sau 30 giây, tối đa 3 lần |
Chiến lược retry nên theo mô hình exponential backoff: lần 1 sau 1 giây, lần 2 sau 2 giây, lần 3 sau 4 giây. Với lỗi -124 và -123, không bao giờ retry vì sẽ lãng phí credit và không có kết quả khác. Riêng lỗi -216 (rate limit) nên dùng token bucket hoặc queue để điều tiết tốc độ từ đầu thay vì để lỗi xảy ra rồi xử lý.
8. Môi Trường Sandbox vs Production
Zalo cung cấp chế độ sandbox (development mode) trong Zalo App - cho phép gửi tin thử nghiệm đến số điện thoại trong whitelist mà không tốn credit thật. Đây là môi trường bắt buộc phải dùng khi phát triển, không nên test trực tiếp trên production.
| Tiêu chí | Sandbox | Production |
|---|---|---|
| Chi phí gửi tin | Miễn phí | 200-300đ/tin tùy template |
| Số nhận được tin | Chỉ SĐT trong whitelist | Mọi SĐT có tài khoản Zalo |
| Rate limit | Thấp hơn production | Theo tier đã đăng ký |
| Template | Dùng template test (không cần duyệt) | Phải dùng template đã duyệt |
| Webhook | Có, nhưng response chậm hơn | Real-time, độ trễ < 1 giây |
| Bật/tắt | Bật "Development mode" trong App settings | Tắt Development mode |
Một điểm dễ bị bỏ qua: API endpoint của sandbox và production là giống nhau. Chỉ khác ở chỗ App đang bật hay tắt "Development mode". Vì vậy, cần dùng biến môi trường (ZALO_DEV_MODE=true/false) để kiểm soát, tránh trường hợp gửi tin test đến khách hàng thật hoặc ngược lại.
9. Checklist Trước Khi Go-Live
Danh sách này dựa trên các lỗi thực tế hay gặp khi đưa ZNS lên production. Hoàn thành 100% trước khi mở traffic thật.
Với developer: Checklist kỹ thuật
- Tắt "Development mode" trên Zalo App - kiểm tra lại lần cuối
- Access token và refresh token lưu ở biến môi trường, không hardcode trong code hoặc file config được commit lên Git
- Có cơ chế auto-refresh token trước khi hết hạn (buffer tối thiểu 5 phút)
- Webhook URL là HTTPS, server trả về HTTP 200 trong vòng 5 giây
- Đã test toàn bộ event webhook:
user_received_message,user_seen_message,send_zns_failed - Có retry logic với exponential backoff cho lỗi
-216và-1000 - Có fallback sang SMS khi nhận lỗi
-124(người dùng không dùng Zalo) - Log đủ thông tin: tracking_id, phone (mask bớt), error code, timestamp
- Số điện thoại được chuẩn hóa về dạng
84xxxxxxxxxtrước khi gửi - Đã test với số điện thoại không có Zalo để xác nhận fallback hoạt động
Với bộ phận vận hành: Checklist template và nội dung
Bộ phận vận hành thường xuyên bỏ qua phần này, dẫn đến template bị từ chối hoặc bị Zalo cảnh cáo sau khi đã go-live. Kiểm tra kỹ 5 điểm sau.
- Tất cả template đã có trạng thái "Đã duyệt" - không dùng template đang chờ hoặc bị từ chối
- Nội dung template là giao dịch thuần túy (xác nhận đơn, OTP, nhắc lịch) - không chứa nội dung quảng cáo, khuyến mãi, kêu gọi mua hàng
- Tất cả tham số động (biến thay thế) đều có giá trị fallback khi dữ liệu null
- Đã test gửi thử với dữ liệu thật đến ít nhất 5 số điện thoại nội bộ
- Có quy trình theo dõi tỷ lệ gửi thành công hàng ngày - nên duy trì trên 85%
Câu Hỏi Thường Gặp Về Tích Hợp Zalo ZNS API
Tích hợp Zalo ZNS API mất bao lâu?
Thời gian tích hợp thực tế khoảng 3-7 ngày làm việc, trong đó phần code mất 1-2 ngày, nhưng phần chờ duyệt template mất 1-3 ngày. Developer có thể code xong hoàn toàn ngay ngày đầu tiên nhờ sandbox, nhưng không thể go-live cho đến khi ít nhất 1 template được Zalo phê duyệt. Vì vậy, nên nộp template duyệt sớm nhất có thể, song song với việc phát triển.