Xác thực và kiểm tra
Xác thực nội dung yêu cầu gốc, xử lý trùng lặp và kiểm tra bên nhận một cách an toàn.
Xác thực mọi webhook trước khi sử dụng payload, sau đó gửi một lần kiểm tra để xác nhận bên nhận của bạn hoạt động từ đầu đến cuối.
Trước khi bắt đầu
- Một địa chỉ đích đã lưu và signing secret (khóa ký) của địa chỉ đó,
twhsec_.... Không dùng Server Secret Token để xác thực webhook. - Một route bên nhận có thể đọc nội dung yêu cầu gốc trước khi bất kỳ bộ phân tích JSON nào thay đổi nội dung đó.
- Một kho lưu trữ có ràng buộc duy nhất trên
idcủa sự kiện, để các bản trùng lặp được xử lý an toàn.
Nhận webhook
Đọc nội dung gốc
Tapaya gửi một yêu cầu POST JSON với các header sau. Giữ nội dung ở dạng byte gốc; nội dung đã được phân tích rồi tuần tự hóa lại sẽ không còn khớp với chữ ký.
Content-Type: application/json
X-Tapaya-Event-Id: 019a0000-0000-7000-8000-000000000001
X-Tapaya-Signature: t=1793361600,v1=<lowercase hexadecimal HMAC-SHA256>- Với Express, sử dụng
express.raw({ type: "application/json" })cho route này, trước bộ phân tích JSON toàn cục.
Kiểm tra chữ ký
Tính HMAC-SHA256 trên dấu thời gian, một dấu chấm và nội dung gốc, rồi so sánh kết quả với v1 bằng phép so sánh có thời gian không đổi.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyTapayaWebhook(rawBody, signatureHeader, secret, now = Date.now()) {
if (!Buffer.isBuffer(rawBody) || typeof signatureHeader !== "string") {
throw new Error("Missing raw body or signature");
}
if (typeof secret !== "string" || !secret.startsWith("twhsec_")) {
throw new Error("Missing signing secret");
}
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader);
if (!match) throw new Error("Malformed signature");
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp) || Math.abs(now / 1000 - timestamp) > 300) {
throw new Error("Expired signature");
}
const expected = createHmac("sha256", Buffer.from(secret, "utf8"))
.update(`${match[1]}.`, "utf8")
.update(rawBody)
.digest();
const received = Buffer.from(match[2], "hex");
if (!timingSafeEqual(expected, received)) throw new Error("Invalid signature");
return JSON.parse(rawBody.toString("utf8"));
}- Dùng toàn bộ giá trị bí mật
twhsec_...làm khóa UTF-8. Không giải mã giá trị bí mật và không xóa tiền tố của nó. - Từ chối dấu thời gian lệch quá năm phút so với đồng hồ của bạn và giữ đồng hồ luôn đồng bộ.
- Trong khi bạn xoay vòng signing secret, hãy thử secret mới trước, sau đó đến secret cũ, cho đến khi hàng đợi đã được xử lý hết.
Lưu và xác nhận
Chỉ chấp nhận sự kiện sau khi đã lưu an toàn, để lỗi phía bạn sẽ kích hoạt việc thử lại.
import express from "express";
import { verifyTapayaWebhook } from "./verify-tapaya-webhook.mjs";
import { saveEventOnce } from "./events.mjs"; // your store, unique on event id
const app = express();
app.post(
"/webhooks/tapaya",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = verifyTapayaWebhook(
req.body,
req.get("X-Tapaya-Signature"),
process.env.TAPAYA_WEBHOOK_SECRET,
);
} catch {
return res.sendStatus(400);
}
if (event.organizationId !== process.env.TAPAYA_ORGANIZATION_ID) {
return res.sendStatus(400);
}
await saveEventOnce(event); // skips an id it already stored
res.sendStatus(204);
},
);- Trả về
2xxcho sự kiện bạn đã lưu. Các lần thử lại giữ nguyênidvà nội dung, với dấu thời gian và chữ ký mới. - Trả về phản hồi khác
2xxkhi bạn không thể lưu một sự kiện mới, để việc gửi được thử lại. - Xử lý các sự kiện đã lưu từ hàng đợi của riêng bạn. Thứ tự gửi không được đảm bảo, vì vậy hãy đối soát các bản lưu cũ hơn với trạng thái hiện tại.
Gửi webhook kiểm tra
Thao tác kiểm tra của Platform gửi một yêu cầu webhook.test có chữ ký đến URL đã lưu. Thao tác này hoạt động ngay cả khi webhook bị tắt hoặc loại sự kiện không có trong đăng ký của bạn, và không tạo khoản thanh toán, khoản hoàn tiền hay merchant nào.
- Mở API Keys của tổ chức và lưu địa chỉ đích.
- Dùng thao tác kiểm tra webhook.
- Kiểm tra kết quả, thời lượng và JSON đã gửi được hiển thị trong Platform.
{
"id": "019a0000-0000-7000-8000-000000000010",
"type": "webhook.test",
"apiVersion": "1",
"organizationId": "019a0000-0000-7000-8000-000000000002",
"merchantId": null,
"createdAt": "2026-10-30T12:00:00.000000Z",
"test": true,
"data": {
"message": "This is a test webhook from Tapaya. No payment was created."
}
}Bên nhận cục bộ và bên nhận tự động hóa
Hướng webhook đến máy phát triển hoặc công cụ không cần code khi máy chủ production của bạn chưa sẵn sàng.