Zalo Official Account (OA) API cho phép doanh nghiệp gửi tin nhắn tự động đến khách hàng qua Zalo — kênh messaging phổ biến nhất Việt Nam với 70+ triệu người dùng. Bài này hướng dẫn integrate Zalo OA API vào NestJS.
1. Thiết lập Zalo OA
- Đăng ký Official Account tại oa.zalo.me
- Vào Zalo for Developers → Tạo ứng dụng
- Lấy:
App ID,Secret Key,OA Access Token - Cấu hình Webhook URL (nhận tin nhắn từ user)
2. Authentication — Lấy Access Token
// src/zalo/zalo-auth.service.ts
import { Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
@Injectable()
export class ZaloAuthService {
private readonly logger = new Logger(ZaloAuthService.name);
private accessToken: string = '';
private tokenExpiry: number = 0;
// OA Access Token (lấy từ Zalo for Developers console)
// Token có thời hạn ~90 ngày, cần refresh định kỳ
getOAToken(): string {
return process.env.ZALO_OA_ACCESS_TOKEN!;
}
// User Access Token — lấy sau khi user cho phép
async getUserAccessToken(code: string): Promise<string> {
const res = await axios.post('https://oauth.zaloapp.com/v4/access_token', null, {
params: {
app_id: process.env.ZALO_APP_ID,
app_secret: process.env.ZALO_SECRET_KEY,
code,
grant_type: 'authorization_code',
},
});
return res.data.access_token;
}
// Lấy profile user từ access token
async getUserProfile(accessToken: string): Promise<{
id: string;
name: string;
picture: { data: { url: string } };
}> {
const res = await axios.get('https://graph.zalo.me/v2.0/me', {
params: {
access_token: accessToken,
fields: 'id,name,picture',
},
});
return res.data;
}
}
3. Gửi tin nhắn
// src/zalo/zalo-messaging.service.ts
import { Injectable, Logger } from '@nestjs/common';
import axios, { AxiosInstance } from 'axios';
@Injectable()
export class ZaloMessagingService {
private readonly logger = new Logger(ZaloMessagingService.name);
private readonly api: AxiosInstance;
private readonly BASE_URL = 'https://openapi.zalo.me/v3.0/oa';
constructor(private authService: ZaloAuthService) {
this.api = axios.create({ baseURL: this.BASE_URL });
}
private getHeaders() {
return {
access_token: this.authService.getOAToken(),
'Content-Type': 'application/json',
};
}
// Gửi tin nhắn text đến follower
async sendTextMessage(userId: string, text: string): Promise<void> {
await this.api.post('/message/cs', {
recipient: { user_id: userId },
message: { text },
}, { headers: this.getHeaders() });
this.logger.log(`Sent text to ${userId}`);
}
// Gửi tin nhắn với nút bấm
async sendButtonMessage(userId: string, options: {
text: string;
buttons: Array<{ title: string; payload?: string; url?: string }>;
}): Promise<void> {
await this.api.post('/message/cs', {
recipient: { user_id: userId },
message: {
attachment: {
type: 'template',
payload: {
template_type: 'request_user_info',
elements: [{
title: options.text,
buttons: options.buttons.map(btn => ({
title: btn.title,
type: btn.url ? 'oa.open.url' : 'oa.query.show',
payload: btn.payload ?? btn.url,
})),
}],
},
},
},
}, { headers: this.getHeaders() });
}
// Gửi hình ảnh
async sendImageMessage(userId: string, imageUrl: string): Promise<void> {
await this.api.post('/message/cs', {
recipient: { user_id: userId },
message: {
attachment: {
type: 'template',
payload: {
template_type: 'media',
elements: [{ media_type: 'image', url: imageUrl }],
},
},
},
}, { headers: this.getHeaders() });
}
// Transaction message (ZNS) — gửi cho người chưa follow OA
async sendTransactionMessage(phone: string, templateId: string, templateData: Record<string, string>): Promise<void> {
await axios.post('https://business.openapi.zalo.me/message/template', {
phone,
template_id: templateId,
template_data: templateData,
tracking_id: Date.now().toString(),
}, {
headers: {
access_token: this.authService.getOAToken(),
'Content-Type': 'application/json',
},
});
}
}
4. ZNS — Zalo Notification Service
ZNS cho phép gửi tin nhắn đến số điện thoại người dùng (không cần follow OA):
// Gửi OTP
async sendOTP(phone: string, otp: string): Promise<void> {
await this.sendTransactionMessage(phone, process.env.ZALO_OTP_TEMPLATE_ID!, {
otp,
expiry: '5 phút',
app_name: 'ShopXYZ',
});
}
// Xác nhận đơn hàng
async sendOrderConfirmation(phone: string, order: any): Promise<void> {
await this.sendTransactionMessage(phone, process.env.ZALO_ORDER_TEMPLATE_ID!, {
order_id: order.id,
customer_name: order.customerName,
total: order.total.toLocaleString('vi-VN') + 'đ',
delivery_date: order.estimatedDelivery,
});
}
// Thông báo giao hàng
async sendDeliveryNotification(phone: string, trackingCode: string): Promise<void> {
await this.sendTransactionMessage(phone, process.env.ZALO_DELIVERY_TEMPLATE_ID!, {
tracking_code: trackingCode,
tracking_url: `https://shopxyz.com/track/${trackingCode}`,
});
}
5. Webhook — Nhận tin nhắn từ user
// src/zalo/zalo-webhook.controller.ts
import { Controller, Post, Get, Body, Query, Res } from '@nestjs/common';
import { Response } from 'express';
@Controller('zalo/webhook')
export class ZaloWebhookController {
constructor(private messagingService: ZaloMessagingService) {}
// Verify webhook
@Get()
verify(
@Query('challenge') challenge: string,
@Res() res: Response,
) {
// Zalo gửi GET request để verify — trả về challenge
res.send(challenge);
}
@Post()
async handleEvent(@Body() body: any) {
const { event_name, sender, message } = body;
switch (event_name) {
case 'user_send_text':
await this.handleTextMessage(sender.id, message.text);
break;
case 'user_send_image':
await this.messagingService.sendTextMessage(
sender.id,
'Cảm ơn bạn đã gửi ảnh! Chúng tôi sẽ xem xét và phản hồi sớm.',
);
break;
case 'follow':
await this.handleNewFollower(sender.id, sender.display_name);
break;
case 'unfollow':
// Log hoặc cập nhật DB
break;
}
return { status: 'ok' };
}
private async handleTextMessage(userId: string, text: string) {
const textLower = text.toLowerCase();
if (textLower.includes('đơn hàng') || textLower.includes('order')) {
await this.messagingService.sendButtonMessage(userId, {
text: 'Bạn muốn kiểm tra đơn hàng? Nhấn nút bên dưới:',
buttons: [
{ title: '🔍 Tra cứu đơn hàng', url: 'https://shopxyz.com/orders' },
{ title: '📞 Liên hệ CSKH', url: 'https://shopxyz.com/contact' },
],
});
} else if (textLower.includes('xin chào') || textLower === 'hi') {
await this.messagingService.sendTextMessage(userId,
'Xin chào! Tôi là trợ lý ShopXYZ. Tôi có thể giúp bạn:\n' +
'• Tra cứu đơn hàng\n' +
'• Chính sách đổi trả\n' +
'• Khuyến mãi hiện tại\n\n' +
'Bạn cần hỗ trợ gì?'
);
} else {
await this.messagingService.sendTextMessage(userId,
'Cảm ơn bạn đã liên hệ ShopXYZ! CSKH của chúng tôi sẽ phản hồi trong vòng 15 phút.'
);
}
}
private async handleNewFollower(userId: string, name: string) {
await this.messagingService.sendTextMessage(
userId,
`Xin chào ${name}! Cảm ơn bạn đã quan tâm ShopXYZ OA.\n\n` +
'Tại đây bạn sẽ nhận được:\n' +
'✅ Thông báo đơn hàng\n' +
'✅ Khuyến mãi độc quyền\n' +
'✅ Hỗ trợ CSKH nhanh chóng'
);
}
}
6. Tích hợp vào Order Flow
// src/orders/orders.service.ts
@Injectable()
export class OrdersService {
constructor(
private zaloMessaging: ZaloMessagingService,
) {}
async createOrder(dto: CreateOrderDto, user: User) {
const order = await this.orderModel.create({ ...dto, userId: user.id });
// Gửi ZNS nếu có số điện thoại
if (user.phone) {
await this.zaloMessaging.sendOrderConfirmation(user.phone, order)
.catch(err => this.logger.warn('ZNS failed:', err.message));
}
return order;
}
async updateStatus(orderId: string, status: string) {
const order = await this.orderModel.findByIdAndUpdate(orderId, { status }, { new: true });
const user = await this.usersModel.findById(order.userId);
if (status === 'shipped' && user?.phone) {
await this.zaloMessaging.sendDeliveryNotification(user.phone, order.trackingCode);
}
return order;
}
}
7. Biến môi trường
ZALO_APP_ID=123456789
ZALO_SECRET_KEY=your_secret_key
ZALO_OA_ACCESS_TOKEN=your_oa_token
ZALO_OTP_TEMPLATE_ID=template_id_1
ZALO_ORDER_TEMPLATE_ID=template_id_2
ZALO_DELIVERY_TEMPLATE_ID=template_id_3
8. Kết luận
- OA Message: Gửi cho follower — text, button, image
- ZNS: Gửi cho bất kỳ số điện thoại Việt Nam — phù hợp OTP, đơn hàng
- Webhook: Nhận tin nhắn từ user, xử lý chatbot logic
- Follow event: Gửi welcome message khi có follower mới
- ZNS có phí theo gói — cần đăng ký template được Zalo duyệt trước
Zalo OA + ZNS là bộ đôi hiệu quả cho ecommerce Việt Nam: ZNS cho transactional, OA message cho customer service.