VietBilling
All guides
VietBilling Engineering Guides

Integrating VietBilling with Next.js and NestJS end to end

A server-to-server guide from API keys, Customers, and Checkouts to verified webhooks that activate subscriptions in your app.

VietBilling EngineeringVerified 2026-08-3114 min read

Integration boundary

The VietBilling API key is a server secret. A Next.js browser should call your NestJS backend, which maps users to Customers, creates Checkouts, and receives signed webhooks.

Next.js browser → NestJS API → VietBilling Public API → PayOS
                       ↑                ↓
                       └─ verified signed webhook

Configure server-only values

VIETBILLING_API_URL=https://api.vietbilling.com/public/v2
VIETBILLING_API_KEY=vb_live_...
VIETBILLING_WEBHOOK_SECRET=...
VIETBILLING_PRODUCT_ID=...

Never use a NEXT_PUBLIC_ prefix. Store production values in the hosting platform's secret manager and rotate a key if it appears in logs or a client bundle.

Build a small NestJS client

@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()
  }
}

Read user identity from authenticated server context. Never let the browser freely choose external_customer_id or a Product outside the backend allowlist.

Create and display Checkout

A NestJS controller calls the client and returns only the Checkout ID and URL needed for redirect or polling. If an HTTP attempt times out, reuse the same idempotency key.

The Next.js result page must not grant access from success_url. Show a confirming state and ask your backend for Checkout status. The backend must verify ownership before returning it.

export default async function BillingResult({ searchParams }) {
  const { checkout_id: checkoutId } = await searchParams
  const status = await getCheckoutForCurrentUser(checkoutId)
  return <BillingStatus status={status} />
}

Apply signed webhook state

Preserve the raw body, verify the signature, deduplicate event_id, and update Subscription mapping plus entitlement in one transaction. Return 2xx for an event already processed.

@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 }
}

End-to-end verification

Create a test user and Checkout, confirm the browser never sees the API key, complete a small PayOS payment, verify one Subscription mapping, replay the event, then test reconciliation after a temporarily unavailable webhook endpoint.

See request shapes in VietBilling Docs and verify provider behavior against PayOS Docs.