Public REST API
Endpoints used by the client and components.
vantezzen/pay provides a REST API for on-page actions like getting wallet info and starting checkout.
It is highly recommended that you use the React components or the typed clients instead of calling the REST API directly. The components handle wallet creation, balance refresh, and checkout confirmation automatically.
| Need | Use |
|---|---|
| React UI for credits and checkout | Components |
| Custom browser UI | createPayClient |
| Non-React browser app | REST API with a publishable key |
| Existing logged-in users | createPayServerClient with a secret key |
| Promo/support credits | POST /api/v1/credit with a secret key |
| React to payment events | Consumer webhooks |
Key types
| Key | Starts with | Where it can be used | What it can do |
|---|---|---|---|
| Publishable | pay_pk_ | Browser or server | Read catalog, create/read code wallets, deduct, start checkout |
| Secret | pay_sk_ | Server only | Wallets, grants, deduct, checkout, portal, features, and orders |
Never ship a secret key to a browser.
Base URL
All REST endpoints live under:
https://your-pay-domain.com/api/v1The hosted default used in docs is:
https://pay.vantezzen.io/api/v1Use the REST API directly if you are not using the registry client.
Versioning and compatibility
The public REST surface is versioned in the URL. Existing /api/v1 fields and
error codes are stable: new optional fields, endpoints, and enum values may be
added without a version bump, but existing required inputs and documented
responses are not removed or renamed within v1.
If a breaking change is ever needed, it will ship under a new path version with a migration note. Deprecations are announced in the dashboard and documentation before a replacement is required.
OpenAPI
The machine-readable API description is available at:
GET /api/openapi.jsonIt uses a relative /api/v1 server URL, so tooling can import it from either
the hosted service or a self-hosted deployment.
Authentication
All requests use bearer auth:
Authorization: Bearer pay_pk_...Browser calls should use publishable keys. Server-only calls that create external-auth wallets, deduct from them, or grant credits require secret keys - see Server-side users for those endpoints.
Rate limits
Publishable-key requests are limited per API key and client IP to a burst of 30 requests, refilling at 5 requests per second. Invalid recovery-code attempts are limited to 20 per hour; anonymous-wallet creation uses the project limit configured in the dashboard.
When a request is limited, the 429 response includes Retry-After,
RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Browser code
can read those headers because they are exposed through CORS. Retry only after
the reported interval.
Catalog
GET /api/v1/productsReturns active products and active prices for the authenticated project.
{
"products": [
{
"id": "prod_...",
"name": "Image generations",
"description": null,
"type": "credits",
"creditUnit": "images",
"freeGrant": { "credits": 10, "period": "monthly" },
"prices": [
{
"id": "price_...",
"label": null,
"amountCents": 500,
"currency": "USD",
"creditsGranted": 100,
"features": [],
"interval": "one_time"
}
]
}
]
}For a subscription product, each price is a tier - label is the tier name,
interval is month/year, creditsGranted is included per cycle, and
features lists the slugs it unlocks. One-time unlocks carry their features on
a one_time price.
Anonymous wallets
Create a credit-code wallet:
POST /api/v1/wallets
Authorization: Bearer pay_pk_...Returns the wallet's balances plus its derived access (unlocked features and active subscriptions):
{
"code": "SKIP-8F3K-L9PQ-2MVT",
"balances": [],
"features": [],
"subscriptions": []
}Read a wallet (same shape):
GET /api/v1/wallets/{code}A wallet with access looks like:
{
"code": "SKIP-8F3K-L9PQ-2MVT",
"balances": [{ "productId": "prod_...", "balance": 8, "creditUnit": "analyses" }],
"features": ["analytics", "export"],
"subscriptions": [
{
"productId": "prod_...",
"priceId": "price_...",
"label": "Pro",
"status": "active",
"currentPeriodEnd": "2026-08-01T00:00:00.000Z"
}
]
}List immutable credit changes for a wallet with an opaque cursor:
GET /api/v1/wallets/{code}/ledger?limit=25Each entry contains its delta, reason (purchase, deduction,
free_grant, manual, refund, or expiry), product identity, optional
orderId, and timestamp. Pass nextCursor to retrieve the next page.
Deduct credits:
POST /api/v1/wallets/{code}/deduct
Content-Type: application/json
{
"productId": "prod_...",
"amount": 10,
"idempotencyKey": "job_123",
"meta": { "feature": "render" }
}Returns:
{
"balance": 90,
"deducted": true
}Checkout
POST /api/v1/checkout
Content-Type: application/json
{
"priceId": "price_...",
"code": "SKIP-8F3K-L9PQ-2MVT",
"successUrl": "https://your-app.com",
"cancelUrl": "https://your-app.com/billing",
"customerEmail": "[email protected]",
"allowPromotionCodes": true
}Pass code to top up an anonymous wallet, or externalUserId (server-side)
to credit an account wallet - never both. With neither, fulfillment mints a
fresh credit code and returns it on the order.
For a subscription tier, if the wallet already has an active subscription to the
same product, checkout returns 409 already_subscribed - send the user to the
customer portal to switch tiers instead.
With a publishable key, successUrl and cancelUrl must be on one of the
project's allowed origins (when an allowlist is configured) - a publishable
key is public, so unrestricted redirects would let anyone send your buyers to
their own page after payment. Secret-key calls may redirect anywhere.
Use allowPromotionCodes: true when the customer should enter a provider
promotion or discount code during checkout. Use discountCode: "LAUNCH10" when
a specific app button should apply or prefill a code for that checkout.
customerEmail pre-fills the provider checkout email. vantezzen/pay also stores
the email returned by Stripe or Polar webhooks on the order when the provider
sends one, which powers /pay/recover email recovery for paid anonymous
wallets.
Returns:
{
"url": "https://checkout...",
"checkoutId": "cs_...",
"orderId": "ord_..."
}Redirect the browser to url.
Identify your key
Confirm the configured project and key mode when diagnosing an integration:
GET /api/v1/me{
"projectId": "proj_...",
"project": "image-tools",
"mode": "credit_codes",
"keyKind": "publishable"
}Orders
List recent project orders with an opaque cursor:
GET /api/v1/orders?limit=25Pass nextCursor to retrieve the next page. The endpoint returns only orders
for the authenticated project.
An order is pending while the payment provider completes checkout. Checkout
attempts still pending after 24 hours are returned as expired; paid,
failed, and refunded are the other final states. A delayed verified payment
webhook may still transition an expired attempt to paid so a completed payment
is never discarded.
GET /api/v1/orders/{orderIdOrCheckoutId}Returns:
{
"id": "ord_...",
"status": "paid",
"code": "SKIP-8F3K-L9PQ-2MVT",
"balance": 100,
"amountCents": 500,
"currency": "USD",
"createdAt": "2026-07-10T12:00:00.000Z",
"paidAt": "2026-07-10T12:01:00.000Z"
}Customer portal
GET /api/v1/wallets/{code}/portal?returnUrl=https%3A%2F%2Fyour-app.comFor external-auth wallets, use the secret-key endpoint:
POST /api/v1/wallets/external/portal
Authorization: Bearer pay_sk_...
Content-Type: application/json
{
"externalUserId": "user_42",
"returnUrl": "https://your-app.com"
}Both return a provider portal URL for wallets that have completed a purchase. Use it for customer-facing billing management: invoices, payment methods, subscription changes, cancellation, and any refund flow the provider exposes.
{
"url": "https://billing..."
}Features (secret key)
Manually grant or revoke a feature for a wallet - for comps, support, or your own logic. Requires a secret key.
POST /api/v1/features
Authorization: Bearer pay_sk_...
Content-Type: application/json
{
"externalUserId": "user_42",
"feature": "analytics",
"action": "grant"
}Identify the wallet by externalUserId or code. action is grant
(default) or revoke. Returns the wallet's full feature list:
{ "features": ["analytics"] }revoke removes only a manual grant. Access from an active subscription or a
one-time purchase is derived and can't be revoked here - end the subscription or
refund the order instead.