Mục lục thư viện
Thư viện tài liệuTích hợp
Webhook
Webhook đẩy sự kiện của một ứng dụng (license, thiết bị, phiên, người dùng, Protect 24/7, bản phát hành) về server của bạn bằng một HTTP `POST` có chữ ký HMAC, chỉ vài giây sau khi sự việc xảy ra. Bạn dùng nó để đồng bộ CRM, gửi email cho khách, mở ticket hay chạy tự động hoá mà không cần gọi API dò liên tục.
11 phút đọc
Cách webhook hoạt động
- Mỗi webhook là một endpoint gắn với một ứng dụng và chỉ nhận những sự kiện bạn chọn. Một ứng dụng có thể có nhiều endpoint; sự kiện khớp nhiều endpoint thì mỗi endpoint nhận một bản riêng.
- Mỗi lần gửi được ký bằng signing secret riêng của endpoint, tự thử lại khi lỗi trong khoảng 3 ngày và được lưu vào nhật ký để bạn xem lại hoặc gửi lại.
- Mọi thành viên workspace đều xem được trang webhook. Chỉ Quản trị và Chủ sở hữu mới tạo, bật tắt, gửi thử, đổi bí mật, xoá và gửi lại.
Tạo endpoint
- Mở ứng dụng, vào Tích hợp › Webhook.
- Bấm Thêm webhook. Bảng Webhook mới mở ra.
- Điền Địa chỉ nhận: URL đầy đủ của endpoint, tối đa 500 ký tự, ví dụ
https://example.com/clavis. - Điền Ghi chú nếu muốn (tối đa 200 ký tự), ví dụ "Đồng bộ CRM". Ghi chú hiện dưới URL trong danh sách.
- Ở mục Sự kiện, tick những sự kiện cần nhận. Danh sách được nhóm theo tiền tố (
license,device,session…), vàlicense.createdcùngsecurity.incidentđược chọn sẵn. Phải chọn ít nhất một sự kiện. - Bấm Tạo webhook. Hộp Signing secret hiện ra ngay (xem phần dưới).
Yêu cầu với địa chỉ nhận:
- Dùng
https://. Lúc gửi, tên miền phải phân giải ra địa chỉ IP công khai: localhost và các dải nội bộ như10.0.0.0/8,172.16.0.0/12,192.168.0.0/16bị từ chối. - Trả mã 2xx trong vòng 10 giây. Chuyển hướng 3xx không được theo và bị tính là lỗi.
Dashboard chưa có chỗ sửa URL, ghi chú hay danh sách sự kiện của endpoint đã tạo. Muốn thay đổi, bạn xoá endpoint (biểu tượng thùng rác Xoá, xác nhận "Xoá webhook này?") rồi tạo lại. Endpoint mới có signing secret mới, và nhật ký gửi của endpoint cũ bị xoá theo.
Danh mục sự kiện
| Sự kiện | Khi nào | Trường chính trong data |
|---|---|---|
license.created | Tạo license từ dashboard, Seller API hoặc cổng đại lý; cả lô là một sự kiện | targetType, targetId, count |
license.activated | License được kích hoạt lần đầu từ phần mềm | licenseId, deviceId |
license.expiring | License đi vào khoảng báo trước hết hạn của gói license, mỗi kỳ hạn một lần | licenseId, expiresAt |
license.expired | License qua ngày hết hạn cộng số ngày ân hạn | licenseId, expiresAt |
license.suspended | Tạm ngưng license, đơn lẻ hoặc hàng loạt | targetType, targetId |
license.revoked | Thu hồi license, đơn lẻ hoặc hàng loạt | targetType, targetId |
license.extended | Gia hạn license, đơn lẻ hoặc hàng loạt | targetType, targetId |
validation.failed | Server từ chối một yêu cầu của phần mềm | reason (mã lỗi), event, licenseId, ip |
device.added | Một máy mới gắn vào license | licenseId, deviceId, platform |
device.removed | Gỡ máy khỏi license từ dashboard hoặc Seller API | targetType, targetId |
device.blocked | Một máy bị chặn | targetType, targetId |
device.transferred | Khách tự chuyển máy trong phần mềm hoặc ở cổng khách hàng | licenseId, deviceId, by |
session.started | Phần mềm đăng nhập thành công, phiên mới được mở | licenseId, deviceId, sessionId, endUserId |
session.ended | Phần mềm gọi logout() | sessionId, licenseId, deviceId |
session.revoked | Thu hồi phiên từ dashboard hoặc Seller API | targetType, targetId |
user.created | Tạo tài khoản người dùng cuối từ dashboard hoặc Seller API | targetType, targetId |
user.banned | Cấm một tài khoản người dùng cuối | targetType, targetId |
security.incident | Protect 24/7 mở một sự cố mới | incidentId, signal, severity, subjectType, subjectId |
security.action | Protect 24/7 tự xử lý theo Xử lý theo mức rủi ro | actionId, action, subjectType, subjectId, reason |
release.published | Một bản phát hành được công bố | targetType, targetId |
reseller.credit_low | Tín dụng của một đại lý tụt dưới mức đủ cho 5 key của gói đắt nhất họ bán, mỗi lần vượt ngưỡng một lần | resellerId, balance |
webhook.test | Bạn bấm Gửi thử | message |
Vài điều cần biết:
- Thao tác từ dashboard và Seller API luôn có
targetType,targetIdcùng chi tiết của thao tác đó; Seller API thêmapiKeyIdvàapiKeyName. Hãy đọcdatamột cách linh hoạt, chấp nhận trường thiếu hoặc trường mới. - Sự kiện xuất phát từ phần mềm (
license.activated,validation.failed,device.added,device.transferredtrong phần mềm,session.started,session.ended) chỉ được ghi lại khi ứng dụng có ít nhất một webhook đang bật đăng ký chúng. Server nhớ danh sách này khoảng 30 giây, nên sự kiện xảy ra ngay sau khi bạn tạo hoặc bật lại endpoint có thể bị bỏ qua. - Ở phiên bản hiện tại, tài khoản do phần mềm tự đăng ký bằng
register()chưa phátuser.created. - Thời hạn license được kiểm tra mỗi giờ, nên
license.expiringvàlicense.expiredcó thể đến trễ tối đa khoảng một giờ.
Signing secret
Ngay sau khi tạo, hộp Signing secret hiện secret dạng whsec_… kèm dòng "Chỉ hiện một lần, lưu ngay vào nơi an toàn" và sẵn một đoạn mã Node để kiểm tra chữ ký. Sao chép secret vào kho bí mật của server nhận, rồi bấm Đã lưu. ClavisAUH lưu secret ở dạng mã hoá và không có cách nào hiện lại.
Để đổi secret, bấm Đổi bí mật trên thẻ endpoint. Không có bước xác nhận: secret mới thay thế ngay và hiện trong cùng hộp Signing secret. Mọi lần gửi từ lúc đó, kể cả lần thử lại của sự kiện cũ, đều ký bằng secret mới.
Payload và header
Mỗi lần gửi là một POST với body JSON:
{
"id": "evt_…",
"type": "license.activated",
"createdAt": "2026-11-02T08:15:30.000Z",
"appId": "app_…",
"data": { "licenseId": "lic_…", "deviceId": "dev_…" }
}
| Trường | Ý nghĩa |
|---|---|
id | Mã sự kiện. Giữ nguyên qua mọi lần thử lại và gửi lại, giống nhau giữa các endpoint. Dùng nó để chống xử lý trùng. Sự kiện thử có dạng evt_test_… |
type | Tên sự kiện trong danh mục |
createdAt | Thời điểm sự kiện xảy ra, ISO 8601 theo UTC |
appId | Ứng dụng phát sinh sự kiện |
data | Chi tiết, khác nhau theo sự kiện và nguồn |
Header đi kèm:
| Header | Giá trị |
|---|---|
Content-Type | application/json |
User-Agent | ClavisAUH-Webhooks/1 |
Clavis-Signature | t=<unix>,v1=<hex> |
Clavis-Event | Tên sự kiện, giống type |
Clavis-Delivery | Mã lần gửi trong nhật ký. Giữ nguyên qua các lần thử lại tự động, đổi khi bạn bấm Gửi lại |
Xác minh Clavis-Signature
v1 là HMAC-SHA256 dạng hex của chuỗi <t>.<body>, trong đó t là thời điểm ký tính bằng giây Unix, body là body thô đúng như nhận được, và khoá là toàn bộ chuỗi whsec_…. t được tính lại ở mỗi lần gửi, nên kiểm tra độ lệch 5 phút vẫn đúng với cả các lần thử lại.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
// Clavis-Signature: t=<unix>,v1=<hex>
export function verifyClavis(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; // lệch tối đa 5 phút
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
const received = Buffer.from(parts.v1 ?? "", "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}
const app = express();
// Giữ body thô để kiểm tra chữ ký trước khi parse JSON
app.post("/clavis", express.raw({ type: "application/json" }), async (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyClavis(raw, req.get("clavis-signature") ?? "", process.env.CLAVIS_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(raw);
await saveOnce(event.id, event); // ghi theo event.id, bỏ qua nếu đã có; việc nặng làm ở job nền
res.sendStatus(204);
});
Nguyên tắc cho server nhận:
- Kiểm tra chữ ký trên body thô, trước khi parse JSON. Parse rồi stringify lại sẽ làm sai chữ ký.
- Trả 2xx thật nhanh, xử lý nặng ở nền. Quá 10 giây bị tính là lỗi.
- Chống trùng bằng
id: cùng một sự kiện có thể đến nhiều lần do thử lại hoặc gửi lại. - Đừng dựa vào thứ tự đến. Các lần gửi chạy song song và lần thử lại có thể đến sau sự kiện mới hơn; dùng
createdAtkhi cần sắp xếp. - Trả 2xx cả cho sự kiện bạn cố ý bỏ qua. Mọi mã khác, kể cả 4xx, đều bị thử lại.
Thử lại và backoff
Lần gửi thành công khi endpoint trả mã 2xx. Mã khác, quá 10 giây, lỗi mạng hay địa chỉ bị từ chối đều là thất bại và được thử lại tự động:
- Tối đa 15 lần gửi: lần đầu và 14 lần thử lại.
- Khoảng cách nhân đôi sau mỗi lần lỗi: 1 phút, 2 phút, 4 phút, 8 phút… cho tới mức trần 12 giờ giữa hai lần. Tổng cộng khoảng 3 ngày.
- Trong lúc còn lượt thử, nhật ký hiện trạng thái Đang chờ kèm lỗi gần nhất và số lần đã gửi. Trạng thái Lỗi chỉ xuất hiện sau lần thử cuối.
Tạm dừng sau lỗi liên tục
Khi một lần gửi thất bại ở lượt thử cuối cùng, endpoint bị tạm dừng: công tắc trên thẻ tắt đi, dòng "Đã tạm dừng vì lỗi liên tục, bật lại khi đích đã ổn" hiện lên, và mọi Chủ sở hữu và Quản trị của workspace nhận một email (hiện bằng tiếng Anh) có nút mở trang webhook.
Trong lúc tạm dừng:
- Sự kiện mới không được ghi lại cho endpoint này, và cũng không gửi bù được về sau.
- Những lần gửi còn đang chờ thử lại chuyển sang Lỗi với lỗi
webhook_disabled.
Để chạy lại, sửa endpoint rồi bật công tắc trên thẻ. Bộ đếm lỗi được đặt về 0. Sau đó mở nhật ký và Gửi lại những lần gửi bị lỗi. Tự tắt công tắc cũng có tác dụng y như tạm dừng, chỉ là không có email.
Gửi thử
Bấm Gửi thử trên thẻ endpoint. Thông báo "Đã gửi sự kiện thử" nghĩa là sự kiện đã vào hàng đợi, chưa chắc đã đến nơi; kết quả nằm trong nhật ký sau vài giây. Sự kiện webhook.test đi qua đúng đường ký và ghi log như sự kiện thật, dù bạn có tick webhook.test hay không, với data là { "message": "Signed and delivered by ClavisAUH" }.
Nhật ký gửi và gửi lại
Mỗi thẻ endpoint hiện số lần gửi trong 24 giờ qua: "… thành công", "… lỗi", và "… đang chờ" khi có. Bấm Nhật ký gửi để mở bảng Lịch sử gửi của endpoint đó, tự làm mới mỗi 5 giây và hiện 50 lần gửi gần nhất. Mỗi dòng có tên sự kiện, trạng thái (Đang chờ, Thành công, Lỗi), mã HTTP, thời gian phản hồi, số lần đã gửi (×3) và thời điểm tạo.
Bấm vào một dòng để xem lỗi gần nhất, payload và tối đa 1.000 ký tự đầu của phản hồi từ server bạn. Nút Gửi lại tạo một lần gửi mới với đúng payload cũ (cùng id); thông báo "Đã xếp hàng gửi lại" hiện lên và dòng cũ vẫn giữ trong nhật ký. Nếu endpoint đang tạm dừng, hãy bật lại trước, nếu không lần gửi lại báo ngay webhook_disabled.
| Lỗi trong nhật ký | Nguyên nhân |
|---|---|
HTTP 500 (hoặc mã khác) | Endpoint trả mã không phải 2xx |
webhook_disabled | Endpoint đang tắt hoặc tạm dừng lúc gửi |
endpoint resolves to a private address | Tên miền trỏ về địa chỉ nội bộ |
only https endpoints are allowed | URL không dùng https |
unreadable_secret | Server không giải mã được secret đã lưu; thử lại không giúp được, hãy bấm Đổi bí mật và cập nhật bên nhận |
Nhật ký gửi được dọn theo thời gian lưu log của gói, trừ những lần gửi còn đang chờ.
Hạn mức theo gói
Số webhook được tính trên toàn workspace, cộng mọi ứng dụng. Theo mặc định, gói Basic cho 1 webhook và Premium cho 50; con số áp dụng cho bạn nằm ở Tài khoản & tổ chức › Gói & sử dụng. Khi ứng dụng đã đủ hạn mức, nút Thêm webhook bị khoá và trang hiện dòng "Gói hiện tại cho tối đa N webhook" với N là hạn mức của bạn. Nếu phần hạn mức còn lại đã được ứng dụng khác dùng, việc tạo bị từ chối với mã plan_limit (HTTP 402). Xem Gói Basic, Premium và hạn mức và bảng giá.
Quản lý qua Seller API
Khoá Seller API có quyền webhooks:read hoặc webhooks:write quản lý được webhook từ hệ thống của bạn:
GET /apps/{appId}/webhooks: liệt kê endpoint của ứng dụng.POST /apps/{appId}/webhooks: tạo endpoint vớiurl,eventsvàdescriptiontuỳ chọn. Secret trả về một lần trongdata.secret. Truyền"events": ["*"]để nhận mọi sự kiện, kể cả sự kiện thêm vào sau này; dashboard không có lựa chọn này.DELETE /webhooks/{id}: xoá endpoint.POST /webhooks/{id}/test: gửiwebhook.test.
Đổi secret, bật tắt, xem nhật ký và gửi lại hiện chỉ làm trong dashboard. Hạn mức gói áp dụng như trên. Chi tiết trong tham chiếu Seller API.