Partner API

Webhooks

Nhận callback đơn payment-gateway qua webhook URL (hoặc partnerCallbackUrl theo đơn). Mọi delivery đều được ký HMAC-SHA256 — kiểm tra chữ ký trước khi xử lý.

Đăng ký webhook URL

PUT /api/v2/partners/update-webhook
{ "webhookUrl": "https://partner.example.com/zenith/callback" }
  • Cần scope webhook.
  • URL bắt buộc HTTPS, không chứa credential.
  • Protocol, host và port phải khớp chính xác một trong các domain Zenith đã đăng ký cho partner — sai sẽ nhận invalid_webhook_url.
  • partnerCallbackUrl theo đơn (set lúc tạo) sẽ thắng webhookUrl đã đăng ký.

Định dạng delivery

Zenith POST một JSON một dòng (key xếp theo bảng chữ cái) khi đơn checkout tới trạng thái cuối:

Header và body của callback
POST {callback url của bạn}
Content-Type: application/json
X-Data-Signature: <64 ký tự hex>
X-Data-Timestamp: 2026-10-23T15:04:06.123Z

{"amount":"1000000","currency":"vnd","eventId":"3f9a1c0e5b7d4e2a8c6b1d9f0a2e4c7b:paid","kind":"paid","orderId":"3f9a1c0e5b7d4e2a8c6b1d9f0a2e4c7b","paidAt":"2026-10-23T15:04:05.000Z","reference":"INV-2026-0001","status":"paid"}
TrườngÝ nghĩa
eventId<orderId>:<kind> — giữ nguyên qua mọi lần gửi lại của cùng sự kiện; dùng làm khoá chống trùng.
kindpaid đã trả · failed thất bại · due hết hạn chưa trả.
orderIdMã order do Zenith cấp (32 ký tự hex) — khớp orderId trả về lúc tạo đơn.
referenceReference bạn gửi lúc tạo đơn.
amount / currencyChuỗi số tiền và mã tiền tệ viết thường.
statusTrạng thái order lúc gửi: paid, failed hoặc expired.
paidAtThời điểm thanh toán ISO-8601, null khi chưa trả.

Kiểm tra chữ ký

Mọi callback đều được ký bằng webhook secret của bạn — Zenith không bao giờ gửi callback không chữ ký. Bạn cần lấy webhook secret từ Zenith để kiểm tra chữ ký. Tính lại và so sánh trước khi đụng vào body:

X-Data-Signature = hex( HMAC-SHA256( webhookSecret, X-Data-Timestamp + rawBody ) )
  • rawBody là chuỗi body nguyên bản nhận được — không parse rồi stringify lại, lệch một khoảng trắng hay thứ tự key là chữ ký sai.
  • Nối X-Data-Timestamp trực tiếp trước rawBody, không ký tự phân cách.
  • So sánh bằng hàm so sánh hằng thời gian.
  • Từ chối callback có timestamp lệch quá 5 phút so với giờ server — mỗi lần gửi lại đều ký với timestamp mới nên không mất callback.
Ví dụ Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyZenithCallback(rawBody, headers, webhookSecret) {
  const timestamp = headers["x-data-timestamp"];
  const signature = headers["x-data-signature"];
  if (!timestamp || !signature) return false;
  if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60 * 1000) return false;

  const expected = createHmac("sha256", webhookSecret)
    .update(timestamp + rawBody)
    .digest();
  const received = Buffer.from(signature, "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}

Ngữ nghĩa delivery

  • Phản hồi 2xx nhanh — timeout 10 giây; chậm hoặc non-2xx bị tính là delivery hỏng.
  • Delivery hỏng được sweeper gửi lại mỗi ~15 phút cho tới khi ghi nhận 2xx.
  • Delivery là at-least-once — dedupe theo eventId, không dùng orderId (một đơn có thể nhận due rồi paid nếu payer trả muộn).
  • Chữ ký sai thì trả non-2xx và không xử lý body.

Đối chiếu trước khi giao hàng

Sau callback paid hợp lệ, xác nhận đơn từ server bằng GET /api/v2/payment-gateway/public/orders/{orderId} — kiểm status, amount, currency và reference khớp bản ghi của bạn trước khi giao hàng.