vantezzen/pay
Components

createPayClient

Browser client for custom credit, checkout, and wallet UI.

createPayClient() is the zero-dependency browser client behind the React components. Use it when you want custom UI but still want the same wallet, checkout, balance, and feature logic that PayProvider uses internally.

By default the client uses a publishable key (pay_pk_...) and stores anonymous wallet credit codes in localStorage. For logged-in external-auth apps, configure mode: "external_auth" and expose same-origin bridge routes backed by the server client.

Install

npx shadcn@latest add https://pay.vantezzen.io/r/pay-client.json

Create a client

import { createPayClient } from "@/lib/pay";

export const pay = createPayClient({
  baseUrl: process.env.NEXT_PUBLIC_PAY_URL!,
  publishableKey: process.env.NEXT_PUBLIC_PAY_KEY!,
});

Config

FieldTypeDescription
baseUrlstringPublic URL of your vantezzen/pay instance.
publishableKeystringProject publishable key. Safe for browser use.
mode"credit_codes" | "external_auth"Identity mode. Defaults to credit_codes.
externalUserIdstringStable user id from your app. Required for external auth.
externalApiBasePathstringSame-origin bridge path for external-auth server calls. Defaults to /api/pay.
storageKeystringOptional custom localStorage key for credit codes.
requestTimeoutMsnumberAPI request timeout in milliseconds. Defaults to 15,000.

External-auth bridge routes should validate the current session server-side and use pay_sk_... through createPayServerClient(). Never put a secret key in browser code.

getOrCreateCode() is browser-only because anonymous wallets need local storage. Do not call it during SSR or in server code; use createPayServerClient for server-side wallet work.

Common calls

const products = await pay.getProducts();
const project = await pay.getMe();
const code = await pay.getOrCreateCode();
const wallet = await pay.getWallet();
const result = await pay.deduct(10, { idempotencyKey: "job_123" });

await pay.startCheckout("price_..."); // popup on desktop, redirect on mobile
await pay.startCheckout("price_...", { allowPromotionCodes: true });
await pay.startCheckout("price_...", { discountCode: "LAUNCH10" });

Credit-code methods

MethodReturnsDescription
getCode()string | nullReads the stored browser code.
setCode(code)voidSaves a recovery code, usually after restore or checkout.
clearCode()voidRemoves the stored code. The next wallet call creates a fresh wallet.
getOrCreateCode()Promise<string>Creates a wallet if needed and stores the code.

If the stored code is missing, expired, or deleted server-side, balance, deduct, and checkout calls clear it and create a fresh wallet automatically.

Catalog and wallet

MethodReturnsDescription
getMe()Promise<PayIdentity>Identifies the authenticated project, mode, and key kind.
getProducts()Promise<Product[]>Reads active products and prices.
getWallet(code?)Promise<WalletInfo>Reads balances, features, and subscriptions in one call.
getBalances(code?)Promise<Balance[]>Reads all balances for a code wallet.
getBalance(productId?, code?)Promise<Balance | null>Reads one product balance. Without productId, returns the only product or first product.
getFeatures(code?)Promise<string[]>Reads unlocked feature slugs.
hasFeature(feature, code?)Promise<boolean>Checks whether a feature is unlocked.
hasCredits(amount, opts?)Promise<boolean>Checks whether the wallet has enough credits.

Spending credits

await pay.deduct(10, {
  productId: "prod_...",
  idempotencyKey: "job_123",
  meta: { feature: "render" },
});
MethodReturnsDescription
deduct(amount, opts?)Promise<{ balance: number; deducted: boolean }>Deducts credits from the browser wallet.

Pass a stable idempotencyKey for retryable work, such as a render job id or request id. If you omit it, the client generates one, which is fine for a single button click but not for server retries.

Checkout

For custom purchase UI, prefer startCheckout. Its default flow: "auto" opens a popup on desktop and uses redirect checkout on touch/mobile devices:

await pay.startCheckout("price_...");

Force redirect checkout when your app cannot support popups:

await pay.startCheckout("price_...", { flow: "redirect" });

Let buyers enter a provider promotion or discount code at checkout:

await pay.startCheckout("price_...", { allowPromotionCodes: true });

Or start checkout from a specific offer button with a code already applied or prefilled:

await pay.startCheckout("price_...", { discountCode: "LAUNCH10" });
MethodReturnsDescription
startCheckout(priceId, opts?)Promise<CheckoutResult>One-call checkout. Creates the session, remembers the order, and uses popup or redirect checkout.
createCheckout(priceId, opts?)Promise<CheckoutResult>Lower-level checkout. Returns the provider URL; you handle redirect and resume.
getOrder(ref)Promise<OrderStatus>Polls an order by order id or provider checkout id.

Checkout options:

OptionTypeDescription
flow"auto" | "popup" | "redirect"Checkout window behavior for startCheckout.
successUrlstringProvider success redirect. Defaults to the current page for redirect flow.
cancelUrlstringProvider cancel redirect.
customerEmailstringPre-fills the checkout email when the provider supports it.
allowPromotionCodesbooleanLets the customer enter a provider promotion or discount code.
discountCodestringApplies or pre-fills a specific provider promotion or discount code.
idempotencyKeystringReuses the original checkout when a request is retried.
codestringExplicit credit-code wallet to top up.

CheckoutResult contains:

type CheckoutResult = {
  url: string;
  checkoutId: string;
  orderId: string;
};

Read orders

Use cursor pagination when your app needs a customer-facing purchase history:

const page = await pay.listOrders({ limit: 25 });
const next = page.nextCursor
  ? await pay.listOrders({ cursor: page.nextCursor })
  : null;

Billing portal

Use the portal URL when a wallet has completed at least one purchase and the user wants invoices, refunds, payment methods, or subscription management:

const url = await pay.getPortalUrl({
  returnUrl: window.location.href,
});

window.location.href = url;
MethodReturnsDescription
getPortalUrl(opts?)Promise<string>Creates a payment-provider customer portal URL for the current wallet.

Error handling

import { PayError } from "@/lib/pay";

try {
  await pay.deduct(100);
} catch (err) {
  if (err instanceof PayError && err.isInsufficientCredits) {
    console.log(`Need ${err.requested}, have ${err.balance}`);
  }
}

PayError includes status, optional stable code, and for insufficient credits also balance and requested. See Errors for common status codes.