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.jsonCreate 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
| Field | Type | Description |
|---|---|---|
baseUrl | string | Public URL of your vantezzen/pay instance. |
publishableKey | string | Project publishable key. Safe for browser use. |
mode | "credit_codes" | "external_auth" | Identity mode. Defaults to credit_codes. |
externalUserId | string | Stable user id from your app. Required for external auth. |
externalApiBasePath | string | Same-origin bridge path for external-auth server calls. Defaults to /api/pay. |
storageKey | string | Optional custom localStorage key for credit codes. |
requestTimeoutMs | number | API 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
| Method | Returns | Description |
|---|---|---|
getCode() | string | null | Reads the stored browser code. |
setCode(code) | void | Saves a recovery code, usually after restore or checkout. |
clearCode() | void | Removes 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
| Method | Returns | Description |
|---|---|---|
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" },
});| Method | Returns | Description |
|---|---|---|
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" });| Method | Returns | Description |
|---|---|---|
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:
| Option | Type | Description |
|---|---|---|
flow | "auto" | "popup" | "redirect" | Checkout window behavior for startCheckout. |
successUrl | string | Provider success redirect. Defaults to the current page for redirect flow. |
cancelUrl | string | Provider cancel redirect. |
customerEmail | string | Pre-fills the checkout email when the provider supports it. |
allowPromotionCodes | boolean | Lets the customer enter a provider promotion or discount code. |
discountCode | string | Applies or pre-fills a specific provider promotion or discount code. |
idempotencyKey | string | Reuses the original checkout when a request is retried. |
code | string | Explicit 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;| Method | Returns | Description |
|---|---|---|
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.