Payments

Multi-provider checkout and webhooks for the Nuxt template — Creem, Stripe, and the ZPay demo.

Payments are an end-to-end billing flow you can build on. A provider factory registers each provider only when its env vars are present. Creem and Stripe are the env-selectable providers; ZPay is a China-oriented (Alipay) demo provider with its own webhook. Checkout, the orders and billing pages, subscription state, and the Creem/Stripe webhooks (including the subscription lifecycle and refunds) all run against real data.

Pick a provider

The provider factory

server/payment/index.ts exposes getPaymentProviderFactory() (a singleton). The factory registers a provider only when its env vars are present:

  • ZPay when NUXT_ZPAY_URL, NUXT_ZPAY_MERCH_ID, and NUXT_ZPAY_MERCH_KEY are all set.
  • Creem when NUXT_CREEM_API_KEY is set.
  • Stripe when NUXT_STRIPE_API_KEY is set.

The default provider used by the checkout flow comes from getPaymentProvider()creem unless NUXT_PUBLIC_PAYMENT_PROVIDER=stripe. (ZPay is reachable by its own webhook but is not selectable through NUXT_PUBLIC_PAYMENT_PROVIDER.)

Each provider implements the interface in server/payment/types.ts:

interface PaymentProvider {
  createPayment(request: CreatePaymentRequest): Promise<CreatePaymentResponse>;
  handleCallback(callbackParams: unknown): Promise<CallbackVerificationResult>;
}

Providers live in server/payment/providers/{creem,stripe,zpay}-provider.ts.

Config sources

FileDrivesNotes
app/config/payment-config.tsThe checkout API's product/amount resolutiongetPaymentProvider + getPaymentProducts; Creem or Stripe IDs from runtimeConfig.public
app/config/price-config.tsThe pricing UI displayFree / Pro (monthly + yearly) / Lifetime copy and amounts
shared/config/payment.tsThe ZPay demo chargeAn EXAMPLE single hardcoded product + amountCNY; replace before production

app/config/payment-config.ts is authoritative for the create route's recorded amount/currency. shared/config/payment.ts is only a sample used by the ZPay flow — see the ZPay amount caveat.

Checkout flow

Create the order

POST /api/payment/create (server/api/payment/create.post.ts):

  • Requires login (requireAuth) — guest checkout is not supported (401).
  • Zod-validates { productId }; the email comes from the session.
  • Resolves amount/currency via getPaymentProduct(productId) (400 on unknown).
  • Builds an orderNo, inserts a payment row, and calls the active provider's createPayment.
  • Returns { orderNo, thirdOrderNo, checkoutUrl }.

Redirect and poll

The client sends the user to the provider checkoutUrl, which returns to /pay-success?orderId=<orderNo>. That page can poll GET /api/payment/query?orderId=<orderNo> (query.get.ts) for the order status.

Receive the webhook

The provider calls its matching webhook, which verifies the signature and updates the payment (and for subscriptions, subscription) tables.

Webhook paths

ProviderPath & methodVerification
CreemPOST /api/payment/notify/creemHMAC-SHA256 over the raw body vs. the creem-signature header (NUXT_CREEM_WEBHOOK_SECRET)
StripePOST /api/payment/notify/stripeconstructEvent over the raw body vs. the stripe-signature header (NUXT_STRIPE_WEBHOOK_SECRET)
ZPayGET /api/payment/notify/zpayMD5 sign over sorted params + merchant key

Creem and Stripe handle the full subscription lifecycle and refunds (see their pages). ZPay, as a demo, only handles its success/failure callback.

What's wired up

  • The orders page (account/order.vue) lists the user's real payment history via GET /api/payment/orders.
  • The subscription page (account/subscription.vue) shows live plan, status, period, and cancel state.
  • The billing page (setting/billing.vue) renders the plan summary and billing history from GET /api/user/billing, and links Stripe customers to the Billing Portal.
  • The order-status query (query.get.ts) is authenticated and scoped to the signed-in user.

On this page