Skip to main content

Webhook Events & Payload - Event Catalog and Data Structure

This page is for the technical team on the receiving side: it lists every event the system supports, the data structure (payload) sent, and how to verify the integrity of each request.


Overview​

Every webhook delivery is an HTTP POST with Content-Type: application/json and a common body structure:

{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "order.created",
"data": { "...": "event-specific details" },
"timestamp": "2026-07-17T08:30:00.000Z"
}
FieldMeaning
eventIdUnique identifier of the delivery. Use it for deduplication (see Idempotency below)
eventEvent type, e.g. order.created, customer.phone_updated
dataEvent details — the structure differs per event type
timestampTime the event was recorded (ISO 8601, UTC)

Event catalog​

Orders (order.*)​

EventWhen it fires
order.createdA new order is created (from the Mini App, the Portal, or synced from a marketplace)
order.status_changedThe order status changes
order.paidThe order is marked as paid
order.cancelledThe order is cancelled (sent together with order.status_changed)

Main fields in data: orderId, code, platform, sOrderId (marketplace order ID, when the order is synced from a marketplace), status, isPaid, price, originPrice, discount, shippingFee, buyer, lineItems, appliedVouchers, createdAt. For update events (order.status_changed, order.paid, order.cancelled), data contains only the key changed fields (orderId, status, isPaid, updatedAt...).

Payload example — 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"
}
Payload example — 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"
}
Payload example — 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"
}
Payload example — 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"
}

Sent right after (or together with) an order.status_changed payload carrying the same data.

Customers (customer.*)​

EventWhen it fires
customer.createdA new customer is created
customer.phone_updatedThe customer updates their phone number
customer.followed_oaThe customer follows the Official Account
customer.updatedOther profile updates

Main fields in data: userId, shopId, fullName, phone, email, gender, BOD, avatar, tags, hashTags, createdAt.

Personal data protection

Customer payloads contain only explicitly whitelisted fields — sensitive data such as passwords or internal information is never sent out.

Payload example — 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 uses the same payload builder as customer.created — the data structure is identical to the example above.

Payload example — customer.phone_updated
{
"eventId": "665f1a2b3c4d5e6f7a8b9c0d",
"event": "customer.phone_updated",
"data": {
"userId": "665f1a2b3c4d5e6f7a8b9c2a",
"phone": "0901234567"
},
"timestamp": "2026-07-17T08:30:00.000Z"
}
Payload example — 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"
}

Inventory (inventory.*)​

EventWhen it fires
inventory.order_stock_changedStock changed by an order (created / delivered / cancelled)
inventory.stock_transferredStock changed by a warehouse transfer
inventory.stock_adjustedManual stock adjustment
inventory.stock_importedBulk stock import
inventory.stock_deletedAn inventory record is deleted

Main fields in data: inventoryHistoryId, shopId, itemId, warehouseId, skuId, sku, reason, delta, quantityBefore, quantityAfter, createdAt.

All 5 events share the same data structure — only the reason value differs:

Eventreason
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
inventory.stock_deleteddeleted
Payload example — 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"
}

inventory.stock_transferred/stock_adjusted/stock_imported/stock_deleted share the same data structure — only reason changes per the table above (and delta is positive or negative depending on the direction of the change).

Products (product.*)​

EventWhen it fires
product.createdA new product is created (from the Portal, or synced from a connected sales/ERP system)
product.updatedThe product is updated (including price and status changes)
product.deletedThe product is deleted

Main fields in data: itemId, shopId, name, slug, price, originPrice, wholesalePrice, status, platform, categoryId, images, deletedAt, createdAt, updatedAt.

Telling product data sources apart

Product events also cover data synced from connected sales/ERP systems (Nhanh, KiotViet, Sapo, Haravan, Odoo, Pancake POS). Use the platform field in data to identify which system a product came from and filter as needed.

Payload example — 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 uses the same payload builder as product.created — the data structure is identical to the example above, even when only price or status changed.

Payload example — 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"
}

ZNS messages (zns_message.*)​

EventWhen it fires
zns_message.status_changedThe delivery status of a ZNS message changes (delivered / failed...)

Main fields in data: messageId, shopId, phone, templateId, status, service, campaignId, actionId, isUid, reason, price, createdAt, updatedAt. campaignId/actionId/reason only appear when the message belongs to a campaign / is tied to an action / has a corresponding error — otherwise they are omitted from the JSON.

Payload example — 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"
}

ZNS campaigns (zns_campaign.*)​

EventWhen it fires
zns_campaign.status_changedA ZNS campaign changes status (in progress / completed / failed)

Main fields in data: campaignId, shopId, title, type, status, sentCount, failedCount, reason, createdAt, updatedAt.

Payload example — 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"
}

Inquiries (inquiry.*)​

EventWhen it fires
inquiry.createdA customer submits a consultation request form on the Mini App
inquiry.status_changedAn admin updates the processing status of the request
  • inquiry.created — data includes: inquiryId, shopId, formId, productId, status, data (form content: name, phone, service, message), createdAt.
  • inquiry.status_changed — data only includes: inquiryId, status, updatedAt (no formId/productId/form content).
Payload example — 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"
}
Payload example — 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"
}

Reviews (review.*)​

EventWhen it fires
review.createdA customer submits a product review on the Mini App
review.status_changedAn admin approves or rejects the review
  • review.created — data includes: reviewId, shopId, userId, itemId, orderItemId, rating, content, images, status, createdAt.
  • review.status_changed — data only includes: reviewId, status, updatedAt (no rating/content/images).
Payload example — 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"
}
Payload example — 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 (approved) or inactive (rejected).

Loyalty points (loyalty.*)​

EventWhen it fires
loyalty.createdA new loyalty point transaction is created
loyalty.status_changedA loyalty transaction changes status (pending → success / cancelled)

Main fields in data: loyaltyHistoryId, shopId, customerId, customerLoyaltyId, orderId, type, status, extraData, createdAt, updatedAt. type is an internal integer code (e.g. 7 = points earned for a successful order) — not a string.

Payload example — 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"
}
Payload example — 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 or cancelled.


Verifying the HMAC signature​

When the webhook uses the HMAC authentication method, each request is signed with HMAC-SHA256 using the configured Secret. The signature (hex-encoded) is sent in the header:

X-Webhook-Signature: 3f9a1b...

The receiver verifies it by re-signing the entire JSON body and comparing:

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),
);
}
Verification notes

Sign the raw body string as received; do not parse and re-serialize it — a different key order breaks the signature. Always reject requests whose signature does not match.


Idempotency — handling duplicates​

Because of automatic retries, an event may be delivered more than once. The receiver should store processed eventId values and skip requests with a duplicate eventId to avoid double processing (e.g. deducting stock twice for the same order).


Technical requirements for the receiver​

ItemRequirement
ProtocolHTTPS, publicly reachable URL
Success responseHTTP 2xx code
Response timeWithin 10 seconds (recommended: return 200 immediately, process business logic asynchronously)
RedirectsNot supported — requests do not follow redirects
Retry on failureAutomatic after 5 minutes → 30 minutes → 3 hours (up to 4 attempts in total)