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
- Customer ánh xạ người dùng của SaaS với danh tính billing.
- Product và ProductPrice mô tả thứ đang bán và giá tại thời điểm bán.
- Checkout giữ một lần thử thanh toán có thể timeout hoặc được retry.
- Payment và Order ghi nhận tiền và nghĩa vụ thương mại đã hoàn tất.
- 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:
- webhook VietBilling hợp lệ báo subscription đã được tạo hoặc cập nhật;
- Public API xác nhận Checkout và Subscription đã hoàn tất trong một lần đối soát chủ động.
Entitlement nên được cập nhật idempotent theo event_id và subscription_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
- Tạo payment link thành công nhưng response về ứng dụng bị timeout.
- Webhook tới trước redirect hoặc bị gửi lại nhiều lần.
- Customer thanh toán sau khi Checkout gần hết hạn.
- Product đổi giá trong khi subscription cũ vẫn giữ snapshot giá ban đầu.
- Dịch vụ nhận webhook tạm thời down và phải đối soát lại.
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.