Server-side users
Use secret keys when your app already has accounts.
Use these endpoints from your server only. They require a pay_sk_ key.
This is the endpoint reference. For the typed helper, see createPayServerClient. For the mental model and worked examples (better-auth, Clerk, Auth.js), see External auth.
Get or create a wallet
Create a wallet tied to your own user id:
POST /api/v1/wallets/external
Authorization: Bearer pay_sk_...
Content-Type: application/json
{
"externalUserId": "user_123"
}Returns:
{
"walletId": "wal_...",
"externalUserId": "user_123",
"balances": [],
"features": [],
"subscriptions": []
}This endpoint is idempotent by externalUserId. You can call it whenever a
user opens billing or starts a paid workflow.
Read a wallet without creating it
Use this when a missing wallet should remain missing (for example, an account status check):
GET /api/v1/wallets/external?externalUserId=user_123
Authorization: Bearer pay_sk_...It returns the same wallet shape as the create-or-read endpoint, or 404 with
code: "wallet_not_found". It never creates a wallet.
For credit history without creating a wallet, use:
GET /api/v1/wallets/external/ledger?externalUserId=user_123&limit=25
Authorization: Bearer pay_sk_...The response has entries and an opaque nextCursor. Each entry includes the
signed delta, reason, product identity, optional order id, and timestamp.
Grant credits
Grant credits to a code wallet or external user:
POST /api/v1/credit
Authorization: Bearer pay_sk_...
Content-Type: application/json
{
"externalUserId": "user_123",
"productId": "prod_...",
"amount": 50,
"idempotencyKey": "support-ticket-456"
}Or by recovery code:
{
"code": "SKIP-8F3K-L9PQ-2MVT",
"productId": "prod_...",
"amount": 50,
"idempotencyKey": "promo-summer-2026"
}Use manual grants for support adjustments, promotions, migrations, or server workflows that should add credits without provider checkout.
Deduct credits
Deduct credits when an external user runs paid work. This endpoint is secret-key only - the user id is the wallet address, so a browser key would let anyone spend anyone's credits.
POST /api/v1/wallets/external/deduct
Authorization: Bearer pay_sk_...
Content-Type: application/json
{
"externalUserId": "user_123",
"productId": "prod_...",
"amount": 10,
"idempotencyKey": "job_9f3k",
"meta": { "feature": "render" }
}Returns the new balance:
{
"balance": 90,
"deducted": true
}productId is optional when the project has one product. On insufficient
credits the response is 402 with the current balance and requested
amount - see Errors.
Start a checkout for a user
Pass externalUserId to checkout and the purchase credits that user's wallet
when the payment webhook arrives - no code handling involved:
POST /api/v1/checkout
Authorization: Bearer pay_sk_...
Content-Type: application/json
{
"priceId": "price_...",
"externalUserId": "user_123",
"successUrl": "https://your-app.com/billing?status=success",
"customerEmail": "[email protected]",
"allowPromotionCodes": true
}Redirect the user to the returned url. The wallet is created on the fly if it
doesn't exist yet.
Use discountCode: "LAUNCH10" when this server-side checkout should apply or
prefill a specific provider promotion or discount code.
Open the billing portal
Create a provider portal URL for an external user:
POST /api/v1/wallets/external/portal
Authorization: Bearer pay_sk_...
Content-Type: application/json
{
"externalUserId": "user_123",
"returnUrl": "https://your-app.com/settings/billing"
}Returns:
{
"url": "https://billing.stripe.com/..."
}The wallet must already have a provider customer from a completed purchase.
Features and access
The wallet returned by getOrCreateWallet includes features (unlocked slugs)
and subscriptions (active plans), so you can gate server logic directly:
if (await pay.hasFeature("user_123", "export")) {
// allow the export
}Grant or revoke a feature manually (comps, support). Revoke removes only the manual grant - subscription and one-time-purchase access is derived:
await pay.grantFeature("user_123", "analytics");
await pay.revokeFeature("user_123", "analytics");See Access & features for the full model.
Idempotency
Always pass a stable idempotencyKey for server grants and deductions. If your
server retries the same logical operation, vantezzen/pay applies it once.
Good keys:
welcome-user_123support-ticket-456render-job-9f3k
Avoid random keys for retryable operations because every retry would look like a new grant or a new charge.