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

Tích hợp VietBilling với Next.js và NestJS đến checkout đầu tiên

Hướng dẫn server-to-server từ API key, Customer và Checkout đến webhook xác thực để kích hoạt subscription trong ứng dụng.

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

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

  1. Tạo user test và Checkout bằng API backend.
  2. Xác nhận browser chỉ nhận URL, không nhận API key.
  3. Hoàn tất payment nhỏ qua PayOS.
  4. Kiểm tra webhook tạo đúng một subscription mapping.
  5. Gửi lại cùng event và xác nhận không cấp quyền lần hai.
  6. 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.