VietBilling
Tất cả hướng dẫn
VietBilling Engineering Guides

Webhook PayOS an toàn: signature, idempotency và đối soát

Xây webhook có thể retry, không cấp quyền hai lần và vẫn phục hồi đúng trạng thái khi request hoặc callback bị gián đoạn.

VietBilling EngineeringKiểm chứng 2026-08-3111 phút đọc

Webhook là message, không phải callback chạy đúng một lần

Một webhook production có thể tới muộn, tới lặp, tới sai thứ tự hoặc không tới trong lúc dịch vụ của bạn gián đoạn. Handler đúng không dựa vào giả định “PayOS gọi đúng một lần”. Nó xác thực message, ghi nhận message và áp dụng thay đổi theo cách có thể chạy lại.

receive → verify signature → reserve event_id → apply transaction
        → return 2xx       ↘ already processed → return 2xx

Bước 1: giữ raw payload và xác thực chữ ký

Không đọc trạng thái thanh toán trước khi signature hợp lệ. Checksum key là secret server-side; không đưa vào frontend, log hoặc analytics. Khi thay key, triển khai quy trình rotation có thời gian chuyển tiếp rõ ràng.

async function handleWebhook(rawBody: string, signature: string) {
  const event = verifyWebhook(rawBody, signature, process.env.WEBHOOK_SECRET!)
  await processOnce(event)
  return { ok: true }
}

Hãy dùng đúng thuật toán và canonical payload theo tài liệu nhà cung cấp thay vì tự đoán cách sort hoặc stringify dữ liệu. PayOS mô tả signature và endpoint xác nhận webhook trong API reference.

Bước 2: deduplicate bằng unique constraint

Check-then-insert trong hai query riêng biệt vẫn có race condition. Tạo unique constraint trên event_id, insert trong transaction, rồi coi conflict là “đã xử lý”.

create table processed_webhook_events (
  event_id uuid primary key,
  event_type text not null,
  processed_at timestamptz not null default now()
);

Trong cùng transaction, cập nhật subscription và entitlement. Chỉ commit event marker nếu thay đổi domain cũng commit. Nếu handler crash, retry có thể tiếp tục mà không mất sự kiện.

Bước 3: trả status có chủ đích

Không trả 2xx trước khi transaction bền vững. Ngược lại, đừng trả lỗi cho event đã xử lý vì điều đó tạo retry vô ích.

Đối soát đóng khoảng trống

Webhook không thể là cơ chế phục hồi duy nhất. Một job định kỳ nên đọc các Checkout/Payment đang pending quá lâu, hỏi lại provider và hoàn tất chúng bằng cùng billing engine như webhook.

pending checkout older than threshold
  → GET provider payment request
  → map provider state
  → complete through idempotent billing transaction
  → emit signed domain webhook

Điểm quan trọng là webhook và reconciliation không có hai bộ business logic khác nhau. Cả hai gọi chung một completion service để tránh trạng thái lệch.

Checklist production