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.createddata includes: inquiryId, shopId, formId, productId, status, data (form content: name, phone, service, message), createdAt.
  • inquiry.status_changeddata 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.createddata includes: reviewId, shopId, userId, itemId, orderItemId, rating, content, images, status, createdAt.
  • review.status_changeddata 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: pendingsuccess 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)