createPayServerClient
Server-only client for external auth, grants, deductions, and checkout.
createPayServerClient() is the zero-dependency server client for apps that
already have their own user accounts. It uses a secret key (pay_sk_...) and
maps your user ids to vantezzen/pay wallets through externalUserId.
Use it only in server code: route handlers, server actions, API routes, background jobs, and support tooling. Never import it into browser code.
Install
npx shadcn@latest add https://pay.vantezzen.io/r/pay-server.jsonCreate a server client
import { createPayServerClient } from "@/lib/pay/server";
export const payServer = createPayServerClient({
baseUrl: process.env.NEXT_PUBLIC_PAY_URL!,
secretKey: process.env.PAY_SECRET_KEY!,
});Pass an idempotencyKey to createCheckout() when the initiating request can
be retried. Repeating the same key returns the original checkout instead of
creating another provider session.
Config
| Field | Type | Description |
|---|---|---|
baseUrl | string | Public URL of your vantezzen/pay instance. |
secretKey | string | Secret API key starting with pay_sk_. Keep it server-only. |
requestTimeoutMs | number | API request timeout in milliseconds. Defaults to 15,000. |
When to use it
Use the server client when:
- Your app already has accounts.
- Credits should follow the logged-in account across devices.
- You want to grant credits from support, migrations, or promotions.
- Paid work happens on the server and must be deducted securely.
- You need checkout to credit a user account instead of a browser code wallet.
For anonymous browser wallets, use the browser client or React components directly. For external-auth React components, back their same-origin bridge routes with this server client.
Wallets
Create or read a wallet for your own user id:
const wallet = await payServer.getOrCreateWallet(user.id);| Method | Returns | Description |
|---|---|---|
getOrCreateWallet(externalUserId) | Promise<ExternalWallet> | Gets or creates the wallet tied to your user id. Idempotent by externalUserId. |
getWallet(externalUserId) | Promise<ExternalWallet | null> | Reads an existing wallet without creating one. |
getProducts() | Promise<Product[]> | Lists active products and purchasable prices. |
getOrder(ref) | Promise<OrderStatus> | Reads an order by its vantezzen/pay or provider checkout id. |
listOrders({ cursor?, limit? }) | Promise<OrderPage> | Lists project orders with cursor pagination. |
getMe() | Promise<PayIdentity> | Inspects the configured project and key mode. |
Call this on billing pages, after signup, or before showing account credit state. It returns balances, unlocked features, and subscriptions in one response.
Grant credits
Grant credits from server-side workflows such as welcome credits, migrations, promotions, or support adjustments:
await payServer.grantCredits({
externalUserId: user.id,
productId: "prod_...",
amount: 50,
idempotencyKey: `welcome-${user.id}`,
meta: { source: "signup" },
});You can also grant credits to an anonymous recovery code:
await payServer.grantCredits({
code: "SKIP-8F3K-L9PQ-2MVT",
amount: 50,
idempotencyKey: "support-ticket-456",
});| Method | Returns | Description |
|---|---|---|
grantCredits(input) | Promise<GrantResult> | Adds credits to an external user wallet or code wallet. |
productId is optional when the project has one product. Use a stable
idempotencyKey whenever a grant can be retried.
Deduct credits
Deduct credits when a logged-in user runs paid work:
await payServer.deduct({
externalUserId: user.id,
productId: "prod_...",
amount: 10,
idempotencyKey: job.id,
meta: { feature: "render" },
});| Method | Returns | Description |
|---|---|---|
deduct(input) | Promise<DeductResult> | Deducts credits from an external user's wallet. |
Always pass a stable idempotencyKey, such as a job id or request id. If the
same operation is retried, vantezzen/pay applies it once.
Checkout
Start checkout for a logged-in user:
const { url } = await payServer.createCheckout("price_...", {
externalUserId: user.id,
successUrl: "https://your-app.com/billing?status=success",
cancelUrl: "https://your-app.com/billing?status=cancelled",
customerEmail: user.email,
allowPromotionCodes: true,
});
return Response.redirect(url);| Method | Returns | Description |
|---|---|---|
createCheckout(priceId, input) | Promise<CheckoutResult> | Creates a provider checkout that credits the external user's wallet when paid. |
The wallet is created automatically if it does not exist yet. The payment
webhook credits the wallet tied to externalUserId, so you do not need browser
credit-code handling.
Checkout input also accepts discountCode for a specific provider promotion or
discount code. Use allowPromotionCodes: true when the customer should enter
their own code in checkout.
Billing portal
Create a provider portal URL for a logged-in user:
const url = await payServer.createPortalUrl(user.id, {
returnUrl: "https://your-app.com/settings/billing",
});| Method | Returns | Description |
|---|---|---|
createPortalUrl(externalUserId, input?) | Promise<string> | Creates a provider portal URL for subscriptions, invoices, and payment methods. |
The wallet must have completed at least one purchase, because that is when Stripe or Polar customer ids are attached.
Features
Check feature access in server code:
if (await payServer.hasFeature(user.id, "analytics")) {
return runAnalyticsExport();
}Manually grant or revoke feature access for support, comps, or promos:
await payServer.grantFeature(user.id, "analytics");
await payServer.revokeFeature(user.id, "analytics");| Method | Returns | Description |
|---|---|---|
hasFeature(externalUserId, feature) | Promise<boolean> | Checks whether the user currently has a feature. |
grantFeature(externalUserId, feature) | Promise<FeatureResult> | Adds a manual feature grant. Idempotent. |
revokeFeature(externalUserId, feature) | Promise<FeatureResult> | Removes a manual feature grant. Derived subscription or purchase access remains. |
Full paid action
import { PayError } from "@/lib/pay/server";
import { payServer } from "@/lib/pay/server";
export async function renderVideo(userId: string, jobId: string) {
try {
await payServer.deduct({
externalUserId: userId,
amount: 10,
idempotencyKey: jobId,
});
} catch (err) {
if (err instanceof PayError && err.isInsufficientCredits) {
const { url } = await payServer.createCheckout("price_...", {
externalUserId: userId,
successUrl: "https://your-app.com/billing?status=success",
});
return { needsPayment: url };
}
throw err;
}
return startRenderJob(jobId);
}Error handling
import { PayError } from "@/lib/pay/server";
try {
await payServer.deduct({
externalUserId: user.id,
amount: 100,
idempotencyKey: requestId,
});
} 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.
Raw endpoints
The server client wraps the secret-key REST endpoints. If you are building a non-TypeScript integration or need raw HTTP examples, see Server-side users.