Cài đặt Webhook
Chức năng Webhook trong Mini Gift cho phép hệ thống của bạn nhận thông báo theo thời gian thực mỗi khi có sự kiện xảy ra — ví dụ: người dùng nhận quà, hoàn thành khảo sát, tích điểm... Thay vì phải liên tục gọi API để kiểm tra, webhook sẽ tự động gửi dữ liệu đến URL mà bạn cấu hình.
Tại đây, bạn có thể:
- Tạo và cấu hình webhook với URL nhận dữ liệu
- Chọn phương thức bảo mật: HMAC-SHA256, API Key hoặc không xác thực
- Bật / tắt webhook và tự động thử lại khi gửi thất bại
- Quản lý danh sách event — bật/tắt và gửi test từng event
- Xem nhật ký các request đã gửi
Webhook là cơ chế gửi dữ liệu tự động từ Mini Gift đến hệ thống bên ngoài (server của bạn) thông qua HTTP POST request mỗi khi có sự kiện xảy ra. Bạn chỉ cần cung cấp một URL endpoint để nhận dữ liệu.
1. Truy cập trang Webhook
Từ thanh menu bên trái:
- Chọn Quản lý cửa hàng
- Nhấn vào Quản lý liên kết
- Chuyển sang tab Webhook

2. Tạo Webhook mới
Điền các thông tin sau để tạo webhook:
| Trường | Mô tả | Bắt buộc |
|---|---|---|
| URL Webhook | Địa chỉ endpoint nhận dữ liệu (HTTP POST) | ✅ |
| Phương thức bảo mật | Chọn cách xác thực request: HMAC-SHA256, API Key hoặc Không xác thực | ✅ |
| Mô tả | Ghi chú mục đích sử dụng webhook | Không |
Sau khi điền đầy đủ, nhấn Tạo webhook.

Webhook Secret
Ngay sau khi tạo thành công, hệ thống sẽ hiển thị Webhook Secret (chữ ký số). Đây là lần duy nhất bạn có thể xem giá trị đầy đủ — hãy sao chép và lưu lại ngay.

Webhook Secret chỉ hiển thị đầy đủ một lần duy nhất sau khi tạo. Nếu bạn quên sao chép, bạn sẽ cần tạo lại chữ ký số mới (xem mục 5).
3. Cài đặt Webhook
Sau khi tạo webhook thành công, hệ thống hiển thị thêm các tuỳ chọn cài đặt.
3.1. Trạng thái & Tự động thử lại
| Cài đặt | Mô tả |
|---|---|
| Trạng thái | Bật / tắt webhook — khi tắt, toàn bộ event sẽ ngừng gửi |
| Tự động thử lại | Khi bật, hệ thống sẽ tự động gửi lại request nếu lần gửi trước thất bại |

Nên bật Tự động thử lại để đảm bảo không bỏ sót event quan trọng khi server của bạn tạm thời gặp sự cố.
3.2. Phương thức bảo mật
Mini Gift hỗ trợ 3 phương thức để bảo vệ webhook của bạn:
HMAC-SHA256
Hệ thống sử dụng Webhook Secret để tạo chữ ký số (HMAC-SHA256) cho mỗi request. Chữ ký được ký vào body của request — server của bạn dùng secret để xác minh rằng request thực sự đến từ Mini Gift.

HMAC-SHA256 là phương thức bảo mật an toàn nhất — đảm bảo cả tính xác thực và toàn vẹn dữ liệu của mỗi request.
Cách xác minh chữ ký trên server của bạn:
Khi Mini Gift gửi webhook, request sẽ kèm theo header chứa chữ ký (signature). Phía server của bạn cần thực hiện các bước sau để xác minh request là hợp lệ:
- Lấy chữ ký từ header của request nhận được
- Tính lại chữ ký bằng cách dùng HMAC-SHA256 với Webhook Secret (đã lưu ở bước tạo webhook) để ký vào raw body của request
- So sánh chữ ký vừa tính với chữ ký nhận được từ header — nếu trùng khớp, request là hợp lệ và đến từ Mini Gift
Ví dụ xác minh bằng Node.js:
const crypto = require("crypto");
function verifyWebhookSignature(secret, signature, rawBody) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
// Sử dụng trong handler
app.post("/webhook", (req, res) => {
const signature = req.headers["x-webhook-signature"];
const isValid = verifyWebhookSignature(
"YOUR_WEBHOOK_SECRET",
signature,
req.rawBody,
);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
// Xử lý event...
res.status(200).send("OK");
});
- Luôn dùng
timingSafeEqual(hoặc hàm so sánh constant-time tương đương) thay vì===để so sánh chữ ký — tránh tấn công timing attack. - Dùng raw body (chuỗi gốc) chứ không phải parsed JSON, vì bất kỳ thay đổi nhỏ nào trong body sẽ tạo ra chữ ký khác. - Không hardcode secret trong code — hãy lưu trong biến môi trường (environment variable).
API Key (Header tuỳ chỉnh)
Bạn cấu hình một header tuỳ chỉnh chứa API Key. Mỗi request webhook sẽ gửi kèm header này — server của bạn kiểm tra giá trị header để xác thực.

Không xác thực
Không sử dụng bất kỳ phương thức bảo mật nào. Request webhook được gửi trực tiếp mà không kèm chữ ký hay API Key.

Chỉ nên chọn Không xác thực khi đang test hoặc trong môi trường nội bộ. Trong production, hãy sử dụng HMAC-SHA256 hoặc API Key để bảo vệ endpoint.
4. Danh sách Event
Webhook hỗ trợ 15 event thuộc 4 nhóm chính. Mỗi event có nút Bật / Tắt riêng và nút Gửi test ngay bên cạnh để kiểm tra kết nối.


Nhấn nút Gửi test bên cạnh mỗi event để gửi một request mẫu đến URL webhook. Hãy kiểm tra server của bạn nhận được dữ liệu đúng định dạng trước khi bật event chính thức.
Format chung
Tất cả webhook đều gửi request với body JSON theo cấu trúc chung:
{
"event": "tên_event",
"data": { ... },
"sentAt": "2026-06-30T02:53:24.167Z"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
event | string | Tên event đã xảy ra (ví dụ: gift.claimed) |
data | object | Dữ liệu chi tiết — khác nhau tuỳ từng event (xem bên dưới) |
sentAt | string | Thời điểm gửi webhook (ISO 8601) |
4.1. Quà tặng
gift.created · gift.updated.info · gift.deleted
Khi quà tặng được tạo mới, cập nhật hoặc xóa, data chứa toàn bộ thông tin quà tặng:
{
"event": "gift.created",
"data": {
"gift": {
"_id": "6a326d720806a3b1217f413c",
"name": "Voucher giảm 50K",
"giftCode": "2358360026902",
"status": "active",
"deliveryMethod": "in_store",
"integrationType": "direct_claim",
"redeemMethod": "self_confirm",
"image": "",
"winDisplayImage": "",
"shopId": "68f0b11278b98d763e096fe2",
"createdAt": "2026-06-17T09:48:34.538Z",
"updatedAt": "2026-06-29T09:10:36.182Z"
}
},
"sentAt": "2026-06-29T09:10:36.204Z"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
gift._id | string | ID quà tặng |
gift.name | string | Tên quà tặng |
gift.giftCode | string | Mã quà tặng |
gift.status | string | Trạng thái (active, ...) |
gift.deliveryMethod | string | Phương thức giao quà (in_store, ...) |
gift.integrationType | string | Loại tích hợp (direct_claim, ...) |
gift.redeemMethod | string | Phương thức đổi quà (self_confirm, ...) |
gift.image | string | URL hình ảnh quà tặng |
gift.shopId | string | ID cửa hàng |
gift.createdAt | string | Thời điểm tạo (ISO 8601) |
gift.updatedAt | string | Thời điểm cập nhật gần nhất (ISO 8601) |
gift.claimed
Khi người dùng nhận quà từ chiến dịch:
{
"event": "gift.claimed",
"data": {
"campaignId": "6a3b3dc828ddc2af709ea379",
"userGiftId": "6a432fa44c50719e9de15b2d",
"userId": "6a1429a02779e58b50191906"
},
"sentAt": "2026-06-30T03:00:00.000Z"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
campaignId | string | ID chiến dịch |
userGiftId | string | ID bản ghi nhận quà |
userId | string | ID người dùng nhận quà |
gift.redeemed
Khi người dùng đổi quà:
{
"event": "gift.redeemed",
"data": {
"campaignId": "6a3b3dc828ddc2af709ea379",
"userGiftId": "6a432fa44c50719e9de15b2d",
"userId": "6a1429a02779e58b50191906"
},
"sentAt": "2026-06-30T03:00:00.000Z"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
campaignId | string | undefined | ID chiến dịch (có thể không có nếu đổi quà ngoài chiến dịch) |
userGiftId | string | ID bản ghi nhận quà |
userId | string | ID người dùng đổi quà |
gift.claimGiftByExchangePoints
Khi người dùng đổi điểm lấy quà:
{
"event": "gift.claimGiftByExchangePoints",
"data": {
"userGiftId": "6a432fa44c50719e9de15b2d",
"userId": "6a1429a02779e58b50191906"
},
"sentAt": "2026-06-30T03:00:00.000Z"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
userGiftId | string | ID bản ghi nhận quà |
userId | string | ID người dùng đổi điểm |
4.2. Khảo sát
survey.created · survey.updated.info · survey.deleted
Khi khảo sát được tạo mới, cập nhật hoặc xóa, data chứa toàn bộ thông tin khảo sát bao gồm danh sách câu hỏi:
{
"event": "survey.created",
"data": {
"survey": {
"_id": "6a3e4b66e66444b49090df99",
"name": "Khảo sát trải nghiệm khách hàng",
"status": "active",
"coverImage": "",
"questions": [
{
"id": "c3d508ab-4b80-4f4e-814f-8c34db60a5bb",
"question": "Bạn đánh giá dịch vụ như thế nào?",
"type": "single-select",
"options": ["Tốt", "Trung bình", "Kém"],
"required": true,
"allowOther": false
},
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"question": "Góp ý thêm",
"type": "text",
"options": [],
"required": false,
"allowOther": false
}
],
"shopId": "68f0b11278b98d763e096fe2",
"createdAt": "2026-06-26T09:50:30.487Z",
"updatedAt": "2026-06-26T10:14:39.413Z"
}
},
"sentAt": "2026-06-30T02:58:28.828Z"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
survey._id | string | ID khảo sát |
survey.name | string | Tên khảo sát |
survey.status | string | Trạng thái (active, ...) |
survey.coverImage | string | URL ảnh bìa |
survey.questions | array | Danh sách câu hỏi (xem bảng bên dưới) |
survey.shopId | string | ID cửa hàng |
survey.createdAt | string | Thời điểm tạo (ISO 8601) |
survey.updatedAt | string | Thời điểm cập nhật gần nhất (ISO 8601) |
Cấu trúc mỗi câu hỏi trong questions:
| Trường | Kiểu | Mô tả |
|---|---|---|
id | string | ID câu hỏi |
question | string | Nội dung câu hỏi |
type | string | Loại câu hỏi: text, single-select, multi-select, ... |
options | array | Danh sách lựa chọn (rỗng nếu type là text) |
required | boolean | Câu hỏi bắt buộc hay không |
allowOther | boolean | Cho phép nhập câu trả lời khác |
survey.completed
Khi người dùng hoàn thành khảo sát, data chứa danh sách câu trả lời:
{
"event": "survey.completed",
"data": {
"campaignId": "6a3b3dc828ddc2af709ea379",
"surveyId": "6a3a3a496ef2413c751b9877",
"userId": "6a1429a02779e58b50191906",
"answers": [
{
"questionId": "8a04c4af-da98-4da8-92ed-17ff21dd2505",
"questionText": "Vị trí công tác của Anh/Chị?",
"questionType": "single-select",
"answer": ["Chủ doanh nghiệp"]
},
{
"questionId": "ad96995a-aab1-4fdd-9405-3bdd57b7dc58",
"questionText": "Anh/Chị quan tâm nhóm sản phẩm nào?",
"questionType": "multi-select",
"answer": ["Đồ gia dụng", "Nội thất"]
}
]
},
"sentAt": "2026-06-30T03:22:42.992Z"
}
| Trường | Kiểu | Mô tả |
|---|---|---|
campaignId | string | ID chiến dịch |
surveyId | string | ID khảo sát |
userId | string | ID người dùng |
answers | array | Danh sách câu trả lời (xem bảng bên dưới) |
Cấu trúc mỗi câu trả lời trong answers:
| Trường | Kiểu | Mô tả |
|---|---|---|
questionId | string | ID câu hỏi |
questionText | string | Nội dung câu hỏi |
questionType | string | Loại câu hỏi (text, single-select, multi-select, ...) |
answer | string[] | Mảng câu trả lời |
Trường answer luôn là mảng — kể cả với câu hỏi single-select hay
text (mảng 1 phần tử). Điều này giúp format thống nhất khi xử lý.