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

Kiến trúc subscription billing cho SaaS sử dụng PayOS

Thiết kế luồng Customer, Checkout, Payment, Subscription và entitlement trên PayOS mà không nhầm redirect trình duyệt với bằng chứng thanh toán.

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

Bài toán thật nằm sau payment link

PayOS cung cấp payment link, trạng thái giao dịch và webhook. Một sản phẩm SaaS còn phải trả lời thêm: khách hàng đang mua plan nào, kỳ hiện tại kết thúc khi nào, lần thanh toán này có tạo subscription mới hay gia hạn subscription cũ, và backend có nên cấp quyền hay chưa.

Một luồng tối thiểu nên có ranh giới rõ ràng:

Your app → Customer → Product + ProductPrice
         → Checkout → PayOS payment link
         ← verified Payment ← PayOS webhook/reconciliation
         → Order → Subscription → BenefitGrant
         → signed VietBilling webhook → entitlement in your app

Redirect về success_url chỉ giúp trình duyệt hiển thị kết quả. Nó không phải bằng chứng thanh toán vì người dùng có thể đóng tab, sửa URL hoặc quay lại trước khi webhook tới.

Năm thực thể không nên gộp

  1. Customer ánh xạ người dùng của SaaS với danh tính billing.
  2. Product và ProductPrice mô tả thứ đang bán và giá tại thời điểm bán.
  3. Checkout giữ một lần thử thanh toán có thể timeout hoặc được retry.
  4. Payment và Order ghi nhận tiền và nghĩa vụ thương mại đã hoàn tất.
  5. Subscription giữ trạng thái xuyên nhiều kỳ; entitlement là kết quả mà ứng dụng cần áp dụng.

Nếu dùng một bảng payments cho toàn bộ luồng, bạn sẽ sớm gặp các câu hỏi không có đáp án: một payment thất bại có làm subscription hết hiệu lực không, thay giá có sửa lịch sử không, và retry có tạo hai subscription không.

Luồng tạo checkout an toàn

Backend của bạn tạo Customer một lần, lưu customer_id, rồi gửi product_id cùng idempotency key ổn định:

curl -X POST "https://api.vietbilling.com/public/v2/checkouts" \
  -H "Authorization: Bearer vb_live_..." \
  -H "Idempotency-Key: checkout_order_123" \
  -H "Content-Type: application/json" \
  -d '{
    "external_customer_id": "user_42",
    "customer_email": "[email protected]",
    "product_id": "PRODUCT_UUID",
    "success_url": "https://app.example/success",
    "cancel_url": "https://app.example/cancel"
  }'

Chỉ redirect khách hàng tới checkout_url trong response. Nếu request timeout, retry cùng idempotency key hoặc đọc lại Checkout; không phát sinh một logical order mới chỉ vì mạng chập chờn.

Khi nào cấp quyền

Backend chỉ cấp quyền sau một trong hai tín hiệu đáng tin cậy:

Entitlement nên được cập nhật idempotent theo event_idsubscription_id. Redirect từ browser chỉ nên dẫn tới màn hình “đang xác nhận” rồi frontend hỏi backend của chính bạn.

Failure mode cần thiết kế trước

Nếu kiến trúc xử lý đúng năm trường hợp này, phần happy path thường đã tự nhiên đúng.

Nguồn kiểm chứng

Đối chiếu payload, signature và trạng thái payment link với tài liệu API PayOS trước khi đưa thay đổi lên production. VietBilling là phần mềm độc lập và không thay thế tài khoản PayOS của merchant.