Chuyển tới nội dung chính

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
eventIdMã đị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)
eventLoại sự kiện, ví dụ order.created, customer.phone_updated
dataDữ liệu chi tiết của sự kiện — cấu trúc khác nhau theo từng loại
timestampThờ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ệnKhi 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_changedTrạ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ệnKhi nào phát sinh
customer.createdKhách hàng mới được tạo
customer.phone_updatedKhách hàng cập nhật số điện thoại
customer.followed_oaKhách hàng theo dõi Official Account
customer.updatedCá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.

Bảo vệ dữ liệu cá nhân

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ệnKhi nào phát sinh
inventory.order_stock_changedTồn kho thay đổi do đơn hàng (tạo / giao / hủy đơn)
inventory.stock_transferredTồn kho thay đổi do chuyển kho
inventory.stock_adjustedĐiều chỉnh tồn kho thủ công
inventory.stock_importedNhập tồn kho hàng loạt
inventory.stock_deletedBả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ệnreason
inventory.order_stock_changedorder_created / order_delivered / order_cancelled
inventory.stock_transferredtransfer_out / transfer_in / transfer_in_damaged / transfer_cancelled
inventory.stock_adjustedmanual_adjustment
inventory.stock_importedbulk_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_importeddata 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ệnKhi nào phát sinh
product.createdSả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.updatedSản phẩm được cập nhật (bao gồm cả thay đổi giá, trạng thái)
product.deletedSả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.

Phân biệt nguồn dữ liệu sản phẩm

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ệnKhi nào phát sinh
zns_message.status_changedTrạ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ệnKhi nào phát sinh
zns_campaign.status_changedChiế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ệnKhi nào phát sinh
inquiry.createdKhách gửi form yêu cầu tư vấn trên Mini App
inquiry.status_changedAdmin cập nhật trạng thái xử lý yêu cầu
  • inquiry.createddata gồ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_changeddata chỉ 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ệnKhi nào phát sinh
review.createdKhách gửi đánh giá sản phẩm trên Mini App
review.status_changedAdmin duyệt hoặc từ chối đánh giá
  • review.createddata gồm: reviewId, shopId, userId, itemId, orderItemId, rating, content, images, status, createdAt.
  • review.status_changeddata chỉ 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ệnKhi nào phát sinh
loyalty.createdPhát sinh giao dịch điểm thưởng mới
loyalty.status_changedGiao 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: pendingsuccess (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),
);
}
Lưu ý khi xác minh

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ụcYêu cầu
Giao thứcHTTPS, URL công khai trên Internet
Phản hồi thành côngMã HTTP 2xx
Thời gian phản hồiTrong vòng 10 giây (khuyến nghị: trả 200 ngay, xử lý nghiệp vụ bất đồng bộ)
RedirectKhông hỗ trợ — request không theo redirect
Thử lại khi lỗiTự động sau 5 phút → 30 phút → 3 giờ (tối đa 4 lần gửi)