The real problem starts after the payment link
PayOS provides payment links, transaction status, and webhooks. A SaaS product must also know which plan a customer bought, when the current period ends, whether a payment starts or renews a subscription, and when the application may grant access.
Your app → Customer → Product + ProductPrice
→ Checkout → PayOS payment link
← verified Payment ← PayOS webhook/reconciliation
→ Order → Subscription → BenefitGrant
→ signed VietBilling webhook → entitlement in your app
A redirect to success_url is a browser experience, not payment evidence. The user can close the tab, alter the URL, or return before the webhook arrives.
Keep five concepts separate
- Customer maps your SaaS user to a billing identity.
- Product and ProductPrice describe what is sold and its price at purchase time.
- Checkout represents an attempt that can time out or be retried.
- Payment and Order record money movement and the completed commercial obligation.
- Subscription carries state across periods; entitlement is the result your app applies.
Using one payments table for everything leaves basic questions unanswered: should a failed payment disable an existing subscription, should a price change rewrite history, and can a retry create two subscriptions?
Create Checkout safely
Create the Customer once, store customer_id, and send a stable idempotency key with product_id:
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"
}'
Redirect to the returned checkout_url. After a timeout, retry with the same key or retrieve the Checkout instead of creating another logical order.
Grant access from trusted state
Grant entitlement only after a valid VietBilling subscription webhook or an explicit Public API reconciliation confirms completion. Process events idempotently by event_id and subscription_id. The browser result page should show a pending state and query your own backend.
Design for failure first
- The provider creates a payment link but your response times out.
- A webhook arrives before the redirect or is delivered repeatedly.
- A customer pays close to Checkout expiry.
- A Product price changes while an old subscription retains its snapshot.
- Your webhook service is unavailable and later reconciles state.
If the architecture handles these cases, the happy path usually follows naturally.
Verification source
Check payment-link payloads, signatures, and statuses against the current PayOS API documentation before production changes. VietBilling is independent software and does not replace the merchant's PayOS account.