Webhook
Nhận sự kiện realtime từ NextX bằng HTTP push thay vì poll. Mỗi sự kiện gửi POST JSON tới URL của bạn, ký HMAC-SHA256 để bạn xác thực nguồn gốc.
1. Tổng quan
Webhook hoạt động như của Meta: mỗi workspace có đúng một webhook (một URL) cho mỗi dịch vụ — CRM, Sales, Tài khoản & Chi nhánh, Office, Chat đa kênh. Bạn chọn dịch vụ, gắn một URL HTTPS, rồi bật từng sự kiện của chính dịch vụ đó. Khi sự kiện xảy ra trong workspace, NextX gửi request có signature tới URL. Endpoint phải trả 2xx trong vòng 15 giây; nếu không, hệ thống giao lại — tối đa 8 lần giao (lần đầu + 7 lần retry, trải ~24 giờ), giãn cách tăng dần (xem mục Giao & retry).
Authorization: Bearer <USER_ACCESS_TOKEN> của người có quyền webhook.endpoints.manage (xem danh sách cần webhook.endpoints.view, nhật ký giao cần webhook.deliveries.view / webhook.deliveries.manage). Token OAuth của ứng dụng (client_credentials) và API key không gọi được các API này: module webhook không được cấp cho ứng dụng. App/agent có thể hướng dẫn khách, hoặc khách tự cấu hình trong app.2. Cấu hình webhook theo dịch vụ
Trong app: Thiết lập › Ứng dụng & API › Webhooks → chọn dịch vụ → dán URL → Xác minh và lưu → bật từng sự kiện cần nhận. Các bước dưới đây là cùng luồng đó qua API.
Bước 1 — Liệt kê dịch vụ và sự kiện
curl https://api.nextx.vn/api/v1/webhook/services \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>"Trả về services[]: mỗi dịch vụ có mã service (CRM, SALES, IAM, OFFICE, COMM), displayName, danh sách events[] (eventType, displayName, description) và endpoint — webhook đang có, hoặc null nếu chưa cấu hình. legacy[] liệt kê endpoint kiểu cũ (tạo không kèm dịch vụ) chưa xoá.
Bước 2 — Xác minh URL (nên làm)
curl -X POST https://api.nextx.vn/api/v1/webhook/endpoints/verify \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-app.com/webhooks/nextx/crm", "verifyToken": "my-random-token" }'NextX gửi một POST có type: "endpoint.verification", chuỗi ngẫu nhiên challenge và verify_token của bạn. Máy chủ của bạn phải trả 2xx và có chuỗi challenge ở đâu đó trong body phản hồi. Lệnh này không lưu gì; kết quả gồm success, responseStatus, durationMs, challenge, responseBody, errorMessage và failureReason (0 đạt · 1 không gọi tới được — DNS/TLS/quá hạn/IP nội bộ · 2 máy chủ trả error code · 3 trả 2xx nhưng không trả lại challenge).
// Node.js (Express) — answer the verification ping BEFORE checking the signature
app.post("/webhooks/nextx/crm", express.raw({ type: "application/json" }), (req, res) => {
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "endpoint.verification") {
// optional: compare event.verify_token with the verifyToken you sent
return res.status(200).json({ challenge: event.challenge });
}
// ...verify X-NextX-Signature-256 (section 6), then process the event
res.sendStatus(200);
});Gói xác minh được ký bằng secret dùng một lần (lúc này chưa có secret thật), nên nó sẽ không qua bước verify signature của bạn — hãy xử lý endpoint.verification trước, như ví dụ trên. Trả lại challenge không làm lộ gì secret.
Bước 3 — Đặt URL cho dịch vụ (tạo hoặc sửa)
curl -X PUT https://api.nextx.vn/api/v1/webhook/services/CRM \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/nextx/crm",
"subscribedEvents": ["contact.created", "contact.updated"]
}'PUT /api/v1/webhook/services/{service} với body { url, subscribedEvents?, customHeaders? }. Lần gọi đầu tạo webhook (201, created: true) và trả signingSecret (whk_sec_…) duy nhất lần này — lưu kỹ để verify signature:
HTTP/1.1 201 Created
{
"id": "<endpoint_id>",
"service": "CRM",
"url": "https://your-app.com/webhooks/nextx/crm",
"status": "Active",
"subscribedEvents": ["contact.created", "contact.updated"],
"created": true,
"signingSecret": "whk_sec_3f9a...c21",
"createdAt": "2026-10-08T03:12:56Z",
"updatedAt": "2026-10-08T03:12:56Z"
}Gọi lại là sửa URL (200, created: false, không trả secret). Khi sửa, bỏ trống subscribedEvents / customHeaders = giữ nguyên; khi tạo, danh sách rỗng hoặc bỏ trống = chưa nhận sự kiện nào — webhook im lặng cho tới khi bạn bật sự kiện.
Bước 4 — Bật/tắt từng sự kiện, hoặc xoá webhook
# Turn one event on (or off with "subscribed": false)
curl -X PUT https://api.nextx.vn/api/v1/webhook/services/CRM/events/deal.won \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "subscribed": true }'
# Remove the service's webhook (it can be created again right away with PUT)
curl -X DELETE https://api.nextx.vn/api/v1/webhook/services/CRM \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>"- Mã dịch vụ viết HOA và so khớp chính xác (
CRM, không phảicrm). Mã không có sự kiện nào đang chào bán →404 webhook.service.not_found. - Sự kiện thuộc dịch vụ khác →
422 webhook.event.not_in_service(vdorder.completedtrênCRM); sự kiện không tồn tại → cũng422. - Bật/tắt sự kiện hoặc xoá khi dịch vụ chưa có webhook →
404 webhook.service.not_configured. - Đường cũ
POST /api/v1/webhook/endpointsvẫn chạy nhưng nay bắt buộcservice; dịch vụ đã có webhook →409 webhook.endpoint.service_exists. Body trộn sự kiện CRM với Sales mà không cóservicesẽ bị từ chối — hãy dùng mỗi dịch vụ một lệnhPUT services/{service}.
Các thao tác theo id vẫn áp dụng cho webhook của dịch vụ (id nằm ở endpoint.id): GET /endpoints/{id}, POST /endpoints/{id}/disable / /enable, POST /endpoints/{id}/rotate-secret, POST /endpoints/{id}/test, và nhật ký giao tại /endpoints/{id}/deliveries.
3. Danh mục sự kiện
33 sự kiện thuộc 8 dịch vụ. Cột Nguồn là mã dịch vụ — webhook của dịch vụ nào chỉ bật được sự kiện của dịch vụ đó.
CRM · CRMSALES · SalesIAM · Tài khoản & Chi nhánhOFFICE · OfficeCOMM · Chat đa kênhHRM · Nhân sựPAYMENT · Thanh toánEINVOICE · Hoá đơn điện tửcontact.identifiedCOMMPROKhách vừa nhắn số điện thoại trong chat. Kèm nguồn hội thoại (quảng cáo / UTM) và nhân viên đang phụ trách — đây là mốc ghép hội thoại với đơn hàng ngoài NextX.
| Trường | Kiểu |
|---|---|
data.object.email | string (email) |
data.object.phone | string |
data.object.agentId | string (uuid) |
data.object.attribution | object |
data.object.channelType | string |
data.object.displayName | string |
data.object.workspaceId | string (uuid) |
data.object.crmContactId | string (uuid) |
data.object.identifiedAt | string (date-time) |
data.object.conversationId | string (uuid) |
data.object.channelAccountId | string (uuid) |
data.object.channelContactId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "contact.identified",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"email": "khach@example.com",
"phone": "string",
"agentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"attribution": {
"adId": "string",
"adName": null,
"adsetId": null,
"utmTerm": null,
"adsetName": null,
"utmMedium": null,
"utmSource": null,
"adPlatform": "string",
"landingUrl": null,
"utmContent": null,
"referrerUrl": null,
"utmCampaign": null,
"adCampaignId": null,
"adCampaignName": null,
"attributionMethod": "string"
},
"channelType": "string",
"displayName": "string",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"identifiedAt": "2026-10-27T09:00:00+07:00",
"conversationId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"channelAccountId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"channelContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}conversation.attribution_resolvedCOMMPROCây quảng cáo (nhóm QC, chiến dịch) của hội thoại vừa lấy được từ Meta, thường vài phút sau sự kiện trên. Cùng conversationId — cập nhật đè, không phải bản ghi mới.
| Trường | Kiểu |
|---|---|
data.object.resolvedAt | string (date-time) |
data.object.attribution | object |
data.object.workspaceId | string (uuid) |
data.object.conversationId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "conversation.attribution_resolved",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"resolvedAt": "2026-10-27T09:00:00+07:00",
"attribution": {
"adId": "string",
"adName": "string",
"adsetId": "string",
"utmTerm": null,
"adsetName": "string",
"utmMedium": null,
"utmSource": null,
"adPlatform": "string",
"landingUrl": null,
"utmContent": null,
"referrerUrl": null,
"utmCampaign": null,
"adCampaignId": "string",
"adCampaignName": "string",
"attributionMethod": "string"
},
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"conversationId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}contact.createdCRMPROMột liên hệ mới được tạo trong CRM — thủ công, nhập từ file, hoặc tự sinh từ chat/web form.
| Trường | Kiểu |
|---|---|
data.object.code | string |
data.object.name | string |
data.object.type | string |
data.object.email | string |
data.object.phone | string |
data.object.source | string |
data.object.branchId | string (uuid) |
data.object.contactId | string (uuid) |
data.object.createdAt | string (date-time) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "contact.created",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"code": "string",
"name": "string",
"type": "string",
"email": "string",
"phone": "string",
"source": "string",
"branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"contactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"createdAt": "2026-10-27T09:00:00+07:00"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}contact.deletedCRMPROLiên hệ bị xoá khỏi CRM.
| Trường | Kiểu |
|---|---|
data.object.contactId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "contact.deleted",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"contactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}contact.updatedCRMPROThông tin liên hệ được sửa. Payload là trạng thái SAU khi sửa, không phải phần thay đổi.
| Trường | Kiểu |
|---|---|
data.object.city | string |
data.object.code | string |
data.object.ward | string |
data.object.email | string |
data.object.phone | string |
data.object.address | string |
data.object.taxCode | string |
data.object.branchId | string (uuid) |
data.object.fullName | string |
data.object.sourceId | string (uuid) |
data.object.contactId | string (uuid) |
data.object.assigneeId | string (uuid) |
data.object.sourceName | string |
data.object.changedFields | string[] |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "contact.updated",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"city": "string",
"code": "string",
"ward": "string",
"email": "string",
"phone": "string",
"address": "string",
"taxCode": "string",
"branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"fullName": "string",
"sourceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"contactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"assigneeId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"sourceName": "string",
"changedFields": [
"string"
]
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}deal.wonCRMPROCơ hội bán hàng chuyển sang trạng thái thắng. Không bắn cho cơ hội thua hoặc bị huỷ.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "deal.won",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}invoice.cancelledEINVOICEPROHoá đơn điện tử bị huỷ — huỷ trên NextX hoặc đồng bộ thấy đã huỷ ở nhà cung cấp. Payload kèm lý do huỷ.
| Trường | Kiểu |
|---|---|
data.object.status | string |
data.object.cqtCode | null |
data.object.issuedAt | null |
data.object.invoiceId | string (uuid) |
data.object.referenceId | string (uuid) |
data.object.cancelReason | string |
data.object.errorMessage | null |
data.object.invoiceNumber | null |
data.object.referenceCode | string |
data.object.referenceType | string |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "invoice.cancelled",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"status": "string",
"cqtCode": null,
"issuedAt": null,
"invoiceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"cancelReason": "string",
"errorMessage": null,
"invoiceNumber": null,
"referenceCode": "string",
"referenceType": "string"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}invoice.failedEINVOICEPROPhát hành hoá đơn điện tử thất bại. Payload gồm chứng từ nguồn và thông báo lỗi nhà cung cấp hoá đơn trả về.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "invoice.failed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}invoice.issuedEINVOICEPROHoá đơn điện tử phát hành thành công. Payload gồm chứng từ nguồn (referenceType / referenceId / referenceCode), số hoá đơn, mã cơ quan thuế (nếu có) và thời điểm phát hành.
| Trường | Kiểu |
|---|---|
data.object.status | string |
data.object.cqtCode | string |
data.object.issuedAt | string (date-time) |
data.object.invoiceId | string (uuid) |
data.object.referenceId | string (uuid) |
data.object.cancelReason | null |
data.object.errorMessage | null |
data.object.invoiceNumber | string |
data.object.referenceCode | string |
data.object.referenceType | string |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "invoice.issued",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"status": "string",
"cqtCode": "string",
"issuedAt": "2026-10-27T09:00:00+07:00",
"invoiceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"cancelReason": null,
"errorMessage": null,
"invoiceNumber": "string",
"referenceCode": "string",
"referenceType": "string"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}employee.createdHRMPRONhân viên mới được tạo trong HRM. Payload gồm mã, họ tên, loại nhân viên, ngày vào làm, phòng ban, chức danh và loại hợp đồng đầu tiên.
| Trường | Kiểu |
|---|---|
data.object.code | string |
data.object.fullName | string |
data.object.joinDate | string (date) |
data.object.orgUnitId | string (uuid) |
data.object.employeeId | string (uuid) |
data.object.positionId | string (uuid) |
data.object.employeeType | string |
data.object.initialContractType | string |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "employee.created",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"code": "string",
"fullName": "string",
"joinDate": "2026-10-27",
"orgUnitId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"employeeId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"positionId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"employeeType": "string",
"initialContractType": "string"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}employee.terminatedHRMPRONhân viên được cho nghỉ việc. Payload gồm mã, họ tên, ngày nghỉ và số quyết định; không kèm lý do nghỉ.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "employee.terminated",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}employee.updatedHRMPROHồ sơ nhân viên được sửa. Payload là trạng thái SAU khi sửa (mã, họ tên, email, phòng ban, chức danh).
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "employee.updated",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}labor_contract.evaluatedHRMPROHợp đồng lao động được đánh giá. Payload gồm loại hợp đồng, quyết định, tổng điểm / điểm trung bình và hợp đồng kế tiếp nếu có.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "labor_contract.evaluated",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}leave.approvedHRMPROĐơn nghỉ phép được duyệt xong ở bước cuối. Payload gồm nhân viên, loại nghỉ, ký hiệu, từ ngày – đến ngày; không kèm ghi chú duyệt.
| Trường | Kiểu |
|---|---|
data.object.toDate | string (date) |
data.object.leaveId | string (uuid) |
data.object.fromDate | string (date) |
data.object.leaveType | string |
data.object.approvedAt | string (date-time) |
data.object.employeeId | string (uuid) |
data.object.leaveSymbol | string |
data.object.workspaceId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "leave.approved",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"toDate": "2026-10-27",
"leaveId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"fromDate": "2026-10-27",
"leaveType": "string",
"approvedAt": "2026-10-27T09:00:00+07:00",
"employeeId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"leaveSymbol": "string",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}leave.rejectedHRMPROĐơn nghỉ phép bị từ chối. Payload gồm nhân viên, loại nghỉ, từ ngày – đến ngày; không kèm lý do từ chối.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "leave.rejected",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}payroll.lockedHRMPROBảng lương một tháng được chốt. Payload gồm năm, tháng, thời điểm chốt và người chốt; không kèm số tiền.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "payroll.locked",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}payslip.acknowledgedHRMPRONhân viên ký xác nhận phiếu lương. Payload gồm phiếu lương, nhân viên, tháng / năm và thời điểm ký.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "payslip.acknowledged",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}recruitment.hiredHRMPROỨng viên được tuyển. Payload gồm hồ sơ ứng tuyển, ứng viên, tin tuyển dụng, ngày tuyển và nhân viên được tạo (nếu có).
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "recruitment.hired",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}branch.createdIAMPROChi nhánh mới được tạo.
| Trường | Kiểu |
|---|---|
data.object.code | string |
data.object.name | string |
data.object.address | string |
data.object.branchId | string (uuid) |
data.object.parentId | null |
data.object.managerId | null |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "branch.created",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"code": "string",
"name": "string",
"address": "string",
"branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"parentId": null,
"managerId": null
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}branch.updatedIAMPROThông tin chi nhánh được sửa.
| Trường | Kiểu |
|---|---|
data.object.branchId | string (uuid) |
data.object.changedFields | string[] |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "branch.updated",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"changedFields": [
"string"
]
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}user.createdIAMPRONgười dùng mới được thêm vào workspace.
| Trường | Kiểu |
|---|---|
data.object.email | string |
data.object.userId | string (uuid) |
data.object.fullName | string |
data.object.createdAt | string (date-time) |
data.object.workspaceId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "user.created",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"email": "string",
"userId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"fullName": "string",
"createdAt": "2026-10-27T09:00:00+07:00",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}workspace.createdIAMPROWorkspace mới được tạo.
| Trường | Kiểu |
|---|---|
data.object.code | string |
data.object.name | string |
data.object.locale | string |
data.object.ownerId | string (uuid) |
data.object.timezone | string |
data.object.createdAt | string (date-time) |
data.object.ownerEmail | string (email) |
data.object.workspaceId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "workspace.created",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"code": "string",
"name": "string",
"locale": "string",
"ownerId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"timezone": "string",
"createdAt": "2026-10-27T09:00:00+07:00",
"ownerEmail": "khach@example.com",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}approval.instance.status_changedOFFICEPROPhiếu trình duyệt đổi trạng thái (chờ duyệt / đã duyệt / từ chối / thu hồi).
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "approval.instance.status_changed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}approval.task.completedOFFICEPRONgười duyệt đã xử lý xong bước duyệt của mình.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "approval.task.completed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}approval.task.createdOFFICEPROMột bước duyệt được giao cho người duyệt.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "approval.task.created",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}payment.completedPAYMENTPRONgười mua trả tiền thành công qua cổng thanh toán online (QR, thẻ, ví, trả sau…) hoặc khoản thu tay được xác nhận. Payload gồm mã giao dịch, chứng từ nguồn (referenceType / referenceId — đơn bán, hợp đồng gym…), cổng, số tiền và thời điểm trả.
| Trường | Kiểu |
|---|---|
data.object.amount | integer |
data.object.paidAt | string (date-time) |
data.object.gateway | string |
data.object.payerId | string (uuid) |
data.object.currency | string |
data.object.metadata | object |
data.object.netAmount | integer |
data.object.payerType | string |
data.object.referenceId | string (uuid) |
data.object.workspaceId | string (uuid) |
data.object.eInvoiceHint | null |
data.object.providerCode | string |
data.object.referenceType | string |
data.object.transactionId | string |
data.object.dbTransactionId | string (uuid) |
data.object.paymentIntentId | string (uuid) |
data.object.paymentInstrument | string |
data.object.providerApprovalCode | string |
data.object.providerTransactionId | string |
data.object.paymentInstrumentDetail | string |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "payment.completed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"amount": 0,
"paidAt": "2026-10-27T09:00:00+07:00",
"gateway": "string",
"payerId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"currency": "string",
"metadata": {},
"netAmount": 0,
"payerType": "string",
"referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"eInvoiceHint": null,
"providerCode": "string",
"referenceType": "string",
"transactionId": "string",
"dbTransactionId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"paymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"paymentInstrument": "string",
"providerApprovalCode": "string",
"providerTransactionId": "string",
"paymentInstrumentDetail": "string"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}payment.failedPAYMENTPROLượt thanh toán online thất bại (cổng từ chối, khách huỷ, hết hạn). Payload gồm chứng từ nguồn, cổng và lý do cổng trả về.
| Trường | Kiểu |
|---|---|
data.object.reason | string |
data.object.failedAt | string (date-time) |
data.object.referenceId | string (uuid) |
data.object.workspaceId | string (uuid) |
data.object.providerCode | string |
data.object.referenceType | string |
data.object.paymentIntentId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "payment.failed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"reason": "string",
"failedAt": "2026-10-27T09:00:00+07:00",
"referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"providerCode": "string",
"referenceType": "string",
"paymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}payment.recurring_chargedPAYMENTPROCổng tự trừ thành công một kỳ của hợp đồng thanh toán định kỳ. Payload có số hợp đồng định kỳ và số kỳ (periodNum) — cổng có thể báo lại cùng một kỳ nhiều lần, hãy chống trùng theo cặp này.
| Trường | Kiểu |
|---|---|
data.object.amount | integer |
data.object.paidAt | string (date-time) |
data.object.payerId | string (uuid) |
data.object.currency | string |
data.object.metadata | object |
data.object.payerType | string |
data.object.periodNum | integer |
data.object.referenceId | string (uuid) |
data.object.workspaceId | string (uuid) |
data.object.providerCode | string |
data.object.referenceType | string |
data.object.dbTransactionId | string (uuid) |
data.object.paymentIntentId | string (uuid) |
data.object.paymentInstrument | string |
data.object.recurringContractNo | string |
data.object.providerApprovalCode | null |
data.object.providerTransactionId | string |
data.object.originalPaymentIntentId | string (uuid) |
data.object.paymentInstrumentDetail | string |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "payment.recurring_charged",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"amount": 0,
"paidAt": "2026-10-27T09:00:00+07:00",
"payerId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"currency": "string",
"metadata": {},
"payerType": "string",
"periodNum": 0,
"referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"providerCode": "string",
"referenceType": "string",
"dbTransactionId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"paymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"paymentInstrument": "string",
"recurringContractNo": "string",
"providerApprovalCode": null,
"providerTransactionId": "string",
"originalPaymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"paymentInstrumentDetail": "string"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}payment.recurring_failedPAYMENTPROCổng tự trừ một kỳ thanh toán định kỳ nhưng thất bại. Payload gồm số hợp đồng định kỳ, số kỳ, mã đơn của lần trừ và lý do.
| Trường | Kiểu |
|---|---|
data.object.reason | string |
data.object.failedAt | string (date-time) |
data.object.periodNum | integer |
data.object.referenceId | string (uuid) |
data.object.workspaceId | string (uuid) |
data.object.providerCode | string |
data.object.referenceType | string |
data.object.providerOrderRef | string |
data.object.recurringContractNo | string |
data.object.originalPaymentIntentId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "payment.recurring_failed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"reason": "string",
"failedAt": "2026-10-27T09:00:00+07:00",
"periodNum": 0,
"referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"providerCode": "string",
"referenceType": "string",
"providerOrderRef": "string",
"recurringContractNo": "string",
"originalPaymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}payment.refundedPAYMENTPROMột khoản hoàn tiền đã hoàn tất. Payload gồm mã hoàn, giao dịch gốc, chứng từ nguồn, số tiền và cách hoàn.
Danh sách trường đang được ghi lại từ sự kiện thật — sẽ hiện ở đây sau lần phát đầu tiên. Ví dụ dưới là phong bì chuẩn.
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "payment.refunded",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}order.cancelledSALESPROĐơn hàng bị huỷ.
| Trường | Kiểu |
|---|---|
data.object.total | integer |
data.object.orderId | string (uuid) |
data.object.orderCode | string |
data.object.cancelledAt | string (date-time) |
data.object.cancelledBy | string (uuid) |
data.object.channelCode | null |
data.object.cancelReason | string |
data.object.crmContactId | string (uuid) |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "order.cancelled",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"total": 0,
"orderId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"orderCode": "string",
"cancelledAt": "2026-10-27T09:00:00+07:00",
"cancelledBy": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"channelCode": null,
"cancelReason": "string",
"crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}order.completedSALESPROĐơn hàng hoàn tất — đã giao và đã thanh toán xong.
| Trường | Kiểu |
|---|---|
data.object.items | object[] |
data.object.total | string |
data.object.orderId | string (uuid) |
data.object.vatMode | string |
data.object.branchId | string (uuid) |
data.object.subtotal | string |
data.object.orderCode | string |
data.object.orderType | string |
data.object.taxAmount | string |
data.object.assignedTo | string (uuid) |
data.object.paidAmount | string |
data.object.vatPercent | null |
data.object.channelCode | string |
data.object.completedAt | string (date-time) |
data.object.shippingFee | string |
data.object.vatIncluded | boolean |
data.object.crmContactId | string (uuid) |
data.object.customerName | string |
data.object.customerType | string |
data.object.customerEmail | string |
data.object.customerPhone | string |
data.object.paymentMethod | string |
data.object.discountAmount | string |
data.object.customerAddress | string |
data.object.customerTaxCode | string |
data.object.customerWardCode | null |
data.object.sourceContractId | string (uuid) |
data.object.sourceQuotationId | null |
data.object.customerDistrictCode | null |
data.object.customerProvinceCode | null |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "order.completed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"items": [
{
"total": "string",
"barcode": "string",
"serials": null,
"taxRate": "string",
"quantity": "string",
"subtotal": "string",
"unitCode": "string",
"unitName": "string",
"lotNumber": "string",
"productId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"taxAmount": "string",
"unitPrice": "string",
"expiryDate": "2026-10-27",
"productSku": "string",
"productName": "string",
"variantName": null,
"discountAmount": "string",
"warrantyMonths": 0
}
],
"total": "string",
"orderId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"vatMode": "string",
"branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"subtotal": "string",
"orderCode": "string",
"orderType": "string",
"taxAmount": "string",
"assignedTo": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"paidAmount": "string",
"vatPercent": null,
"channelCode": "string",
"completedAt": "2026-10-27T09:00:00+07:00",
"shippingFee": "string",
"vatIncluded": false,
"crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"customerName": "string",
"customerType": "string",
"customerEmail": "string",
"customerPhone": "string",
"paymentMethod": "string",
"discountAmount": "string",
"customerAddress": "string",
"customerTaxCode": "string",
"customerWardCode": null,
"sourceContractId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"sourceQuotationId": null,
"customerDistrictCode": null,
"customerProvinceCode": null
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}order.status_changedSALESPROTrạng thái đơn đổi. Bắn cho MỌI lần đổi, kể cả lần đổi sang hoàn tất hoặc huỷ.
| Trường | Kiểu |
|---|---|
data.object.total | integer |
data.object.orderId | string (uuid) |
data.object.branchId | string (uuid) |
data.object.changedAt | string (date-time) |
data.object.changedBy | string (uuid) |
data.object.newStatus | string |
data.object.orderCode | string |
data.object.orderType | string |
data.object.assignedTo | string (uuid) |
data.object.channelCode | string |
data.object.crmContactId | string (uuid) |
data.object.previousStatus | string |
{
"id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "order.status_changed",
"api_version": "v1",
"created_at": "2026-10-27T02:00:00+00:00",
"workspace_id": "0199a1b2-0000-7000-8000-000000000001",
"livemode": true,
"data": {
"object": {
"total": 0,
"orderId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"changedAt": "2026-10-27T09:00:00+07:00",
"changedBy": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"newStatus": "string",
"orderCode": "string",
"orderType": "string",
"assignedTo": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"channelCode": "string",
"crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"previousStatus": "string"
}
},
"metadata": {
"source_service": "string",
"correlation_id": "string"
}
}Chỉ liệt kê sự kiện thực sự được giao. Các tên đã ngừng (deal.created, deal.updated, deal.stage_changed, member.updated, user.updated, order.created, order.confirmed, product.created, product.updated) không bật mới được nữa; endpoint còn giữ chúng vẫn tắt đi được.
4. Cấu trúc payload
Mỗi request có body JSON theo envelope chuẩn:
{
"id": "evt_018f...d2", // ID sự kiện (idempotency)
"type": "contact.created", // = X-NextX-Event-Type
"api_version": "v1",
"created_at": "2026-06-16T03:00:00Z",
"workspace_id": "<workspace_guid>",
"livemode": true, // false nếu là test
"data": {
"object": { "id": "...", "...": "..." } // payload của entity
}
}data.object chứa đối tượng của sự kiện. Sự kiện thử có livemode: false. Hiện chưa có tuỳ chọn payload rút gọn: mọi request đều mang đầy đủ data.object.
5. Headers
| Header | Ý nghĩa |
|---|---|
X-NextX-Event-Id | ID sự kiện (GUID trần) — dùng để chống trùng (idempotency). Trường id trong body có dạng evt_ + cùng GUID đó |
X-NextX-Event-Type | Loại event, vd contact.created |
X-NextX-Timestamp | Unix seconds lúc ký — kiểm để chống replay |
X-NextX-Signature-256 | sha256=<hex> — HMAC-SHA256 của `timestamp.body` bằng secret hiện tại |
X-NextX-Signature-256-Previous | Chỉ có trong 7 ngày sau khi xoay secret: sha256=<hex>, cùng timestamp + body nhưng ký bằng secret CŨ |
X-NextX-Delivery-Attempt | Lần giao thứ mấy (1 = lần đầu) |
X-NextX-Webhook-Id | ID của webhook (endpoint) đã đăng ký |
webhook-id | Standard Webhooks — mã tin nhắn, giữ nguyên qua các lần giao lại (= X-NextX-Event-Id). Chỉ có khi secret dạng whsec_ |
webhook-timestamp | Standard Webhooks — Unix seconds lúc ký (= X-NextX-Timestamp) |
webhook-signature | Standard Webhooks — v1,<base64> = HMAC-SHA256 của `id.timestamp.body`, khoá = phần sau whsec_ giải mã base64. Lúc xoay secret có 2 signature cách nhau dấu cách |
User-Agent: NextX-Webhook/1.0. Header tuỳ chỉnh đặt trên webhook (customHeaders) cũng được gửi kèm.
6. Xác thực signature
Cách nhanh nhất — Standard Webhooks: secret whsec_… theo chuẩn mở Standard Webhooks (standardwebhooks.com), mỗi request có thêm webhook-id, webhook-timestamp, webhook-signature. Dùng thư viện standardwebhooks (Node, Python, Go, Java, C#, PHP, Ruby…) — nó tự verify signature, độ lệch giờ và cả signature kép lúc xoay secret. Webhook tạo trước 14/10/2026 (secret whk_sec_…) chỉ có header X-NextX-*: xoay secret một lần là có thêm header chuẩn.
// Node.js — npm i standardwebhooks
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.NEXTX_WEBHOOK_SECRET); // "whsec_..."
// Ném lỗi nếu chữ ký sai hoặc timestamp lệch quá 5 phút. headers = header thô của request.
const event = wh.verify(req.rawBody, req.headers);# Python — pip install standardwebhooks
import os
from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["NEXTX_WEBHOOK_SECRET"]) # "whsec_..."
event = wh.verify(raw_body, headers) # raise nếu signature/timestamp saiHoặc tự kiểm bằng HMAC — dùng được với mọi secret, kể cả whk_sec_…:
Ký = HMAC-SHA256(secret, `{ts}.{raw_body}`), xuất hex thường, gắn vào X-NextX-Signature-256: sha256=<hex>. Tính lại bằng raw body (chưa parse JSON) và X-NextX-Timestamp, so khớp; nên từ chối nếu timestamp lệch quá ~5 phút (chống replay).
// Node.js (Express, cần raw body)
import crypto from "crypto";
function verify(req, secret) {
const ts = req.header("X-NextX-Timestamp");
const sig = req.header("X-NextX-Signature-256"); // "sha256=..."
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(ts + "." + req.rawBody) // timestamp.body
.digest("hex");
const ok = sig?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return ok && fresh;
}# Python
import hmac, hashlib, time
def verify(headers, raw_body, secret):
ts = headers["X-NextX-Timestamp"]
sig = headers["X-NextX-Signature-256"] # "sha256=..."
expected = "sha256=" + hmac.new(
secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(sig, expected) and abs(time.time() - int(ts)) < 3007. Xoay secret (rotate)
Xoay secret (POST /api/v1/webhook/endpoints/{id}/rotate-secret) trả về newSecret và oldSecretValidUntil. Secret cũ vẫn dùng được trong 7 ngày ân hạn: tới mốc đó mọi request mang hai signature trên cùng timestamp.body — X-NextX-Signature-256 ký bằng secret mới và X-NextX-Signature-256-Previous ký bằng secret cũ, cùng định dạng sha256=<hex>.
curl -X POST https://api.nextx.vn/api/v1/webhook/endpoints/<endpoint_id>/rotate-secret \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>"
{
"newSecret": "whsec_Mf9KQr8GKYqr...",
"oldSecretValidUntil": "2026-10-15T03:12:56Z"
}Nhờ vậy đổi secret không gián đoạn: giữ cả hai secret phía bạn, chấp nhận request khi một trong hai header khớp một trong hai secret, rồi bỏ secret cũ sau khi đã triển khai secret mới (chậm nhất trước oldSecretValidUntil):
// Node.js — accept a request if ANY known secret matches ANY NextX signature header
function verifyDuringRotation(req, secrets /* [newSecret, oldSecret] */) {
const ts = req.header("X-NextX-Timestamp");
const sigs = [
req.header("X-NextX-Signature-256"), // signed with the NEW secret
req.header("X-NextX-Signature-256-Previous"), // signed with the OLD secret (7-day grace only)
].filter(Boolean);
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
const ok = secrets.filter(Boolean).some((secret) => {
const expected = "sha256=" + crypto
.createHmac("sha256", secret).update(ts + "." + req.rawBody).digest("hex");
return sigs.some((sig) => sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
});
return ok && fresh;
}Hết ân hạn, header -Previous không còn được gửi và chỉ secret mới verify được. Xoay tiếp khi đang trong ân hạn sẽ mở cửa sổ 7 ngày mới với secret đang dùng lúc đó làm secret "cũ" — secret cũ hơn nữa mất hiệu lực ngay.
8. Giao & retry
- Thành công = endpoint trả
2xxtrong 15s. - Tối đa 8 lần giao (lần 1 ngay lập tức, rồi retry sau 1 phút, 5 phút, 30 phút, 2 giờ, 4 giờ, 8 giờ, 12 giờ — tổng khoảng 24 giờ) nếu lỗi/timeout; xem
X-NextX-Delivery-Attempt(1 = lần đầu). Hết 8 lần mà vẫn lỗi thì delivery dừng ở trạng thái thất bại. - Idempotency: cùng
id(evt_…) có thể đến nhiều lần — khử trùng theoid. - Circuit breaker: endpoint lỗi liên tiếp từ 100 lần trở lên (ngưỡng mặc định) sẽ bị tự động tắt. Hệ thống không tự bật lại và hiện chưa gửi thông báo khi tắt — bạn cần bật lại bằng
POST /endpoints/{id}/enablesau khi sửa lỗi phía bạn. - SSRF guard: URL trỏ IP nội bộ/private bị chặn — chỉ nhận HTTPS công khai.
- Có thể retry thủ công 1 delivery:
POST /endpoints/{id}/deliveries/{deliveryId}/retry.
9. Test
Gửi 1 sự kiện thử tới endpoint để kiểm tích hợp:
curl -X POST https://api.nextx.vn/api/v1/webhook/endpoints/<endpoint_id>/test \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "eventType": "contact.created" }'Payload test có livemode: false và data.object giữ chỗ (id: "test_object_id"), không phải dữ liệu thật. Tên sự kiện nào cũng được chấp nhận.