vantezzen/pay
Components

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.json

Create 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

FieldTypeDescription
baseUrlstringPublic URL of your vantezzen/pay instance.
secretKeystringSecret API key starting with pay_sk_. Keep it server-only.
requestTimeoutMsnumberAPI 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);
MethodReturnsDescription
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",
});
MethodReturnsDescription
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" },
});
MethodReturnsDescription
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);
MethodReturnsDescription
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",
});
MethodReturnsDescription
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");
MethodReturnsDescription
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.