Sự kiện & Payload Webhook - Danh mục sự kiện và cấu trúc dữ liệu
Trang này dành cho đội kỹ thuật bên nhận webhook: liệt kê toàn bộ sự kiện hệ thống hỗ trợ, cấu trúc dữ liệu (payload) gửi đi và cách xác minh tính toàn vẹn của request.
Tổng quan
Mỗi lần gửi webhook là một HTTP POST với Content-Type: application/json và cấu trúc body chung:
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "order.created",
"data": { "...": "dữ liệu chi tiết theo từng loại sự kiện" },
"timestamp": "2026-07-17T08:30:00.000Z"
}
| Trường | Ý nghĩa |
|---|---|
eventId | Mã định danh duy nhất của lần gửi. Dùng để khử trùng lặp (xem phần Idempotency bên dưới) |
event | Loại sự kiện, ví dụ order.created, customer.phone_updated |
data | Dữ liệu chi tiết của sự kiện — cấu trúc khác nhau theo từng loại |
timestamp | Thời điểm sự kiện được ghi nhận (chuẩn ISO 8601, múi giờ UTC) |
Danh mục sự kiện
Đơn hàng (order.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
order.created | Đơn hàng mới được tạo (từ Mini App, Portal hoặc đồng bộ từ sàn TMĐT) |
order.status_changed | Trạng thái đơn hàng thay đổi |
order.paid | Đơn hàng được ghi nhận đã thanh toán |
order.cancelled | Đơn hàng bị hủy (gửi kèm cùng order.status_changed) |
Các trường chính trong data: orderId, code, platform, sOrderId (mã đơn bên sàn TMĐT, nếu đơn được đồng bộ từ sàn), status, isPaid, price, originPrice, discount, shippingFee, buyer, lineItems, appliedVouchers, createdAt. Với các sự kiện cập nhật (order.status_changed, order.paid, order.cancelled), data chỉ gồm các trường thay đổi chính (orderId, status, isPaid, updatedAt...).
Ví dụ payload — order.created
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "order.created",
"data": {
"orderId": "665f1a2b3c4d5e6f7a8b9c1a",
"code": "DH000123",
"platform": "mini_app",
"sOrderId": "SP-000123",
"status": "pending",
"isPaid": false,
"price": 250000,
"originPrice": 300000,
"discount": 50000,
"shippingFee": 20000,
"buyer": { "fullName": "Nguyễn Văn A", "phone": "0901234567" },
"lineItems": [
{
"itemId": "665f1a2b3c4d5e6f7a8b9c1b",
"name": "Áo thun nam",
"quantity": 2,
"price": 125000
}
],
"appliedVouchers": [],
"createdAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Ví dụ payload — order.status_changed
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "order.status_changed",
"data": {
"orderId": "665f1a2b3c4d5e6f7a8b9c1a",
"status": "confirmed",
"isPaid": false,
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Ví dụ payload — order.paid
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "order.paid",
"data": {
"orderId": "665f1a2b3c4d5e6f7a8b9c1a",
"isPaid": true,
"paidAmount": 250000,
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Ví dụ payload — order.cancelled
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "order.cancelled",
"data": {
"orderId": "665f1a2b3c4d5e6f7a8b9c1a",
"status": "cancelled",
"isPaid": false,
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Khách hàng (customer.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
customer.created | Khách hàng mới được tạo |
customer.phone_updated | Khách hàng cập nhật số điện thoại |
customer.followed_oa | Khách hàng theo dõi Official Account |
customer.updated | Các cập nhật thông tin khác |
Các trường chính trong data: userId, shopId, fullName, phone, email, gender, BOD, avatar, tags, hashTags, createdAt.
Payload khách hàng chỉ chứa các trường được cho phép rõ ràng (whitelist) — các dữ liệu nhạy cảm như mật khẩu, thông tin nội bộ không bao giờ được gửi ra ngoài.
Ví dụ payload — customer.created
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "customer.created",
"data": {
"userId": "665f1a2b3c4d5e6f7a8b9c2a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"fullName": "Nguyễn Văn A",
"phone": "0901234567",
"email": "nguyenvana@example.com",
"gender": "male",
"BOD": "1995-01-01T00:00:00.000Z",
"avatar": "https://example.com/avatar.png",
"tags": "vip,new",
"hashTags": ["665f1a2b3c4d5e6f7a8b9c2b"],
"createdAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
customer.updated dùng chung payload builder với customer.created — cấu trúc data giống hệt ví dụ trên.
Ví dụ payload — customer.phone_updated
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "customer.phone_updated",
"data": {
"userId": "665f1a2b3c4d5e6f7a8b9c2a",
"phone": "0901234567"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Ví dụ payload — customer.followed_oa
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "customer.followed_oa",
"data": {
"userId": "665f1a2b3c4d5e6f7a8b9c2a",
"isFollowOa": true,
"followOaAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Tồn kho (inventory.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
inventory.order_stock_changed | Tồn kho thay đổi do đơn hàng (tạo / giao / hủy đơn) |
inventory.stock_transferred | Tồn kho thay đổi do chuyển kho |
inventory.stock_adjusted | Điều chỉnh tồn kho thủ công |
inventory.stock_imported | Nhập tồn kho hàng loạt |
inventory.stock_deleted | Bản ghi tồn kho bị xóa |
Các trường chính trong data của 4 sự kiện order_stock_changed/stock_transferred/stock_adjusted/stock_imported: inventoryHistoryId, shopId, itemId, warehouseId, skuId, sku, reason, delta, quantityBefore, quantityAfter, createdAt. Riêng inventory.stock_deleted có cấu trúc data khác — xem chi tiết bên dưới.
Cả 4 sự kiện trên dùng chung 1 cấu trúc data — chỉ khác giá trị reason:
| Sự kiện | reason |
|---|---|
inventory.order_stock_changed | order_created / order_delivered / order_cancelled |
inventory.stock_transferred | transfer_out / transfer_in / transfer_in_damaged / transfer_cancelled |
inventory.stock_adjusted | manual_adjustment |
inventory.stock_imported | bulk_import |
Ví dụ payload — inventory.order_stock_changed
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "inventory.order_stock_changed",
"data": {
"inventoryHistoryId": "665f1a2b3c4d5e6f7a8b9c3a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"itemId": "665f1a2b3c4d5e6f7a8b9c1b",
"warehouseId": "665f1a2b3c4d5e6f7a8b9c3b",
"skuId": "665f1a2b3c4d5e6f7a8b9c3c",
"sku": "SP001-DEN-M",
"reason": "order_created",
"delta": -2,
"quantityBefore": 20,
"quantityAfter": 18,
"createdAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Các sự kiện inventory.stock_transferred/stock_adjusted/stock_imported có data cùng cấu trúc — chỉ thay reason theo bảng trên (và delta mang dấu dương/âm tùy chiều biến động).
Sự kiện inventory.stock_deleted có cấu trúc data riêng, không có inventoryHistoryId, reason, delta, quantityBefore, quantityAfter: shopId, itemId, skuId, sku, warehouseId, deletedAt.
Ví dụ payload — inventory.stock_deleted
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "inventory.stock_deleted",
"data": {
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"itemId": "665f1a2b3c4d5e6f7a8b9c1b",
"skuId": "665f1a2b3c4d5e6f7a8b9c3c",
"sku": "SP001-DEN-M",
"warehouseId": "665f1a2b3c4d5e6f7a8b9c3b",
"deletedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Sản phẩm (product.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
product.created | Sản phẩm mới được tạo (từ Portal, hoặc đồng bộ từ hệ thống bán hàng/ERP đã kết nối) |
product.updated | Sản phẩm được cập nhật (bao gồm cả thay đổi giá, trạng thái) |
product.deleted | Sản phẩm bị xóa |
Các trường chính trong data: itemId, shopId, name, slug, price, originPrice, wholesalePrice, status, platform, categoryId, images, deletedAt, createdAt, updatedAt.
Sự kiện sản phẩm bao phủ cả dữ liệu đồng bộ từ hệ thống bán hàng/ERP đã kết
nối (Nhanh, KiotViet, Sapo, Haravan, Odoo, Pancake POS). Dùng trường
platform trong data để biết sản phẩm đến từ hệ thống nào và lọc theo nhu
cầu.
Ví dụ payload — product.created
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "product.created",
"data": {
"itemId": "665f1a2b3c4d5e6f7a8b9c1b",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"name": "Áo thun nam basic",
"slug": "ao-thun-nam-basic",
"price": 199000,
"originPrice": 249000,
"wholesalePrice": 179000,
"status": "active",
"platform": "nhanh",
"categoryId": "665f1a2b3c4d5e6f7a8b9c4a",
"images": ["https://example.com/product.png"],
"deletedAt": null,
"createdAt": "2026-07-17T08:30:00.000Z",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
product.updated dùng chung payload builder với product.created — cấu trúc data giống hệt ví dụ trên, kể cả khi chỉ giá hoặc trạng thái thay đổi.
Ví dụ payload — product.deleted
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "product.deleted",
"data": {
"itemId": "665f1a2b3c4d5e6f7a8b9c1b",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"name": "Áo thun nam basic",
"slug": "ao-thun-nam-basic",
"price": 199000,
"originPrice": 249000,
"wholesalePrice": 179000,
"status": "deleted",
"platform": "nhanh",
"categoryId": "665f1a2b3c4d5e6f7a8b9c4a",
"images": ["https://example.com/product.png"],
"deletedAt": "2026-07-17T08:30:00.000Z",
"createdAt": "2026-07-17T08:20:00.000Z",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Tin nhắn ZNS (zns_message.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
zns_message.status_changed | Trạng thái gửi tin ZNS thay đổi (thành công / thất bại...) |
Các trường chính trong data: messageId, shopId, phone, templateId, status, service, campaignId, actionId, isUid, reason, price, createdAt, updatedAt. campaignId/actionId/reason chỉ xuất hiện khi tin nhắn thuộc chiến dịch / gắn action / có lỗi tương ứng — không có thì bị lược khỏi JSON.
Ví dụ payload — zns_message.status_changed
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "zns_message.status_changed",
"data": {
"messageId": "665f1a2b3c4d5e6f7a8b9c5a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"phone": "0901234567",
"templateId": "665f1a2b3c4d5e6f7a8b9c5b",
"status": "RECEIVED",
"service": "zns",
"isUid": false,
"price": 300,
"createdAt": "2026-07-17T08:29:00.000Z",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Chiến dịch ZNS (zns_campaign.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
zns_campaign.status_changed | Chiến dịch ZNS đổi trạng thái (đang chạy / hoàn thành / thất bại) |
Các trường chính trong data: campaignId, shopId, title, type, status, sentCount, failedCount, reason, createdAt, updatedAt.
Ví dụ payload — zns_campaign.status_changed
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "zns_campaign.status_changed",
"data": {
"campaignId": "665f1a2b3c4d5e6f7a8b9c6a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"title": "Chiến dịch ZNS khuyến mãi tháng 7",
"type": "zns",
"status": "completed",
"sentCount": 120,
"failedCount": 3,
"createdAt": "2026-07-17T07:00:00.000Z",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Yêu cầu tư vấn (inquiry.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
inquiry.created | Khách gửi form yêu cầu tư vấn trên Mini App |
inquiry.status_changed | Admin cập nhật trạng thái xử lý yêu cầu |
inquiry.created—datagồm:inquiryId,shopId,formId,productId,status,data(nội dung form: tên, số điện thoại, dịch vụ, nội dung),createdAt.inquiry.status_changed—datachỉ gồm:inquiryId,status,updatedAt(không cóformId/productId/nội dung form).
Ví dụ payload — inquiry.created
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "inquiry.created",
"data": {
"inquiryId": "665f1a2b3c4d5e6f7a8b9c7a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"formId": "665f1a2b3c4d5e6f7a8b9c7b",
"productId": "665f1a2b3c4d5e6f7a8b9c1b",
"status": "pending",
"data": {
"customerName": "Trần Thị B",
"phone": "0912345678",
"serviceName": "Tư vấn sản phẩm",
"content": "Tôi muốn biết thêm về sản phẩm này",
"inquiryDate": "2026-07-17T08:30:00.000Z"
},
"createdAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Ví dụ payload — inquiry.status_changed
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "inquiry.status_changed",
"data": {
"inquiryId": "665f1a2b3c4d5e6f7a8b9c7a",
"status": "processing",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Đánh giá (review.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
review.created | Khách gửi đánh giá sản phẩm trên Mini App |
review.status_changed | Admin duyệt hoặc từ chối đánh giá |
review.created—datagồm:reviewId,shopId,userId,itemId,orderItemId,rating,content,images,status,createdAt.review.status_changed—datachỉ gồm:reviewId,status,updatedAt(không córating/content/images).
Ví dụ payload — review.created
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "review.created",
"data": {
"reviewId": "665f1a2b3c4d5e6f7a8b9c8a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"userId": "665f1a2b3c4d5e6f7a8b9c2a",
"itemId": "665f1a2b3c4d5e6f7a8b9c1b",
"orderItemId": "665f1a2b3c4d5e6f7a8b9c8b",
"rating": 5,
"content": "Sản phẩm rất tốt, đóng gói cẩn thận",
"images": [],
"status": "pending",
"createdAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Ví dụ payload — review.status_changed
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "review.status_changed",
"data": {
"reviewId": "665f1a2b3c4d5e6f7a8b9c8a",
"status": "active",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
status: active (đã duyệt) hoặc inactive (đã từ chối).
Điểm thưởng (loyalty.*)
| Sự kiện | Khi nào phát sinh |
|---|---|
loyalty.created | Phát sinh giao dịch điểm thưởng mới |
loyalty.status_changed | Giao dịch điểm thưởng đổi trạng thái (chờ xử lý → thành công / hủy) |
Các trường chính trong data: loyaltyHistoryId, shopId, customerId, customerLoyaltyId, orderId, type, status, extraData, createdAt, updatedAt. type là mã số nguyên nội bộ (ví dụ 7 = tích điểm khi mua hàng thành công) — không phải chuỗi.
Ví dụ payload — loyalty.created
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "loyalty.created",
"data": {
"loyaltyHistoryId": "665f1a2b3c4d5e6f7a8b9c9a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"customerId": "665f1a2b3c4d5e6f7a8b9c2a",
"customerLoyaltyId": "665f1a2b3c4d5e6f7a8b9c9b",
"orderId": "665f1a2b3c4d5e6f7a8b9c1a",
"type": 7,
"status": "pending",
"createdAt": "2026-07-17T08:30:00.000Z",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Ví dụ payload — loyalty.status_changed
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "loyalty.status_changed",
"data": {
"loyaltyHistoryId": "665f1a2b3c4d5e6f7a8b9c9a",
"shopId": "665f1a2b3c4d5e6f7a8b9c00",
"customerId": "665f1a2b3c4d5e6f7a8b9c2a",
"customerLoyaltyId": "665f1a2b3c4d5e6f7a8b9c9b",
"orderId": "665f1a2b3c4d5e6f7a8b9c1a",
"type": 7,
"status": "success",
"createdAt": "2026-07-17T08:00:00.000Z",
"updatedAt": "2026-07-17T08:30:00.000Z"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
status: pending → success (thành công) hoặc cancelled (hủy).
Xác minh chữ ký HMAC
Khi webhook dùng phương thức xác thực HMAC, mỗi request được ký bằng HMAC-SHA256 với Secret đã cấu hình. Chữ ký (dạng hex) nằm trong header:
X-Webhook-Signature: 3f9a1b...
Phía nhận xác minh bằng cách ký lại toàn bộ body JSON và so sánh:
const crypto = require("crypto");
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signatureHeader),
);
}
Ký trên chuỗi body gốc (raw body) nhận được, không parse rồi serialize lại — thứ tự key thay đổi sẽ làm sai chữ ký. Luôn từ chối request có chữ ký không khớp.
Idempotency — xử lý trùng lặp
Do cơ chế tự động thử lại, một sự kiện có thể được gửi nhiều hơn 1 lần. Phía nhận nên lưu lại các eventId đã xử lý và bỏ qua request có eventId trùng để tránh xử lý lặp (ví dụ trừ kho 2 lần cho cùng một đơn).
Yêu cầu kỹ thuật phía nhận
| Hạng mục | Yêu cầu |
|---|---|
| Giao thức | HTTPS, URL công khai trên Internet |
| Phản h ồi thành công | Mã HTTP 2xx |
| Thời gian phản hồi | Trong vòng 10 giây (khuyến nghị: trả 200 ngay, xử lý nghiệp vụ bất đồng bộ) |
| Redirect | Không hỗ trợ — request không theo redirect |
| Thử lại khi lỗi | Tự động sau 5 phút → 30 phút → 3 giờ (tối đa 4 lần gửi) |