Kiến trúc tích hợp
API key VietBilling là server secret. Next.js browser không gọi Public API trực tiếp; request đi qua NestJS backend của bạn. Backend lưu mapping user ↔ customer_id, tạo Checkout và nhận webhook.
Next.js browser → NestJS API → VietBilling Public API → PayOS
↑ ↓
└─ verified signed webhook
1. Cấu hình server
VIETBILLING_API_URL=https://api.vietbilling.com/public/v2
VIETBILLING_API_KEY=vb_live_...
VIETBILLING_WEBHOOK_SECRET=...
VIETBILLING_PRODUCT_ID=...
Không đặt các giá trị này dưới prefix NEXT_PUBLIC_. Trong production, dùng secret manager của hosting platform và rotate key nếu từng xuất hiện trong log hoặc client bundle.
2. Tạo client nhỏ trong NestJS
@Injectable()
export class VietBillingClient {
async createCheckout(input: {
userId: string
email: string
successUrl: string
cancelUrl: string
}) {
const response = await fetch(
`${process.env.VIETBILLING_API_URL}/checkouts`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VIETBILLING_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `checkout_${input.userId}_${currentOfferVersion()}`,
},
body: JSON.stringify({
external_customer_id: input.userId,
customer_email: input.email,
product_id: process.env.VIETBILLING_PRODUCT_ID,
success_url: input.successUrl,
cancel_url: input.cancelUrl,
}),
}
)
if (!response.ok)
throw new ServiceUnavailableException("Billing unavailable")
return response.json()
}
}
Chỉ nhận userId và email từ authenticated server context. Không để browser tự chọn external_customer_id hay product_id ngoài allowlist của backend.
3. Endpoint tạo checkout
NestJS controller kiểm tra user, gọi client và chỉ trả checkout_url cùng Checkout ID cần cho polling. Next.js gọi endpoint này khi người dùng bấm nâng cấp rồi redirect.
@Post("billing/checkout")
createCheckout(@CurrentUser() user: User) {
return this.billing.createForUser(user)
}
Nếu client retry do timeout, backend dùng cùng idempotency key. Không tạo key ngẫu nhiên cho mỗi HTTP attempt.
4. Trang kết quả trong Next.js
success_url không tự cấp quyền. Trang kết quả hiển thị trạng thái đang xác nhận và poll backend của bạn bằng Checkout ID. Backend đọc VietBilling Public API; khi subscription active, session hoặc profile được refresh.
export default async function BillingResult({ searchParams }) {
const { checkout_id: checkoutId } = await searchParams
const status = await getCheckoutForCurrentUser(checkoutId)
return <BillingStatus status={status} />
}
Backend phải xác nhận Checkout thuộc current user trước khi trả state.
5. Webhook cập nhật entitlement
Webhook endpoint giữ raw body, xác thực signature, deduplicate event_id, rồi update subscription mapping và entitlement trong transaction. Trả 2xx cho event đã xử lý.
@Post("webhooks/vietbilling")
async receive(@RawBody() body: Buffer, @Headers("x-signature") signature: string) {
const event = this.webhooks.verify(body, signature)
await this.webhooks.processOnce(event)
return { received: true }
}
6. Test end to end
- Tạo user test và Checkout bằng API backend.
- Xác nhận browser chỉ nhận URL, không nhận API key.
- Hoàn tất payment nhỏ qua PayOS.
- Kiểm tra webhook tạo đúng một subscription mapping.
- Gửi lại cùng event và xác nhận không cấp quyền lần hai.
- Tắt webhook tạm thời, chạy reconciliation và xác nhận state tự phục hồi.
Xem thêm payload Public API trong VietBilling Docs và đối chiếu provider behavior với PayOS Docs.