PayKitDocs
Live demoDashboardGet started
Documentation

Billing for AI apps,
without building billing.

Drop in a meter call, gate Pro features, sell credit packs. PayKit runs the ledger and Stripe takes the money — no billing tables, no webhook plumbing, no portal to build.

QuickstartAPI reference
Getting started

Introduction

Charging for an AI app means metering every model call, selling credits, gating premium features, and reconciling all of it with Stripe. That's a database, a webhook handler, a customer portal — and a week you don't have.

PayKit gives you one function — meter() — plus a <Paywall> and a hosted billing API. You call meter() before each AI call; PayKit deducts a credit, tells you if the user is out, and handles buy-more. Subscriptions and credit packs settle through Stripe Checkout and land back in the ledger automatically.

i
PayKit does not run your AI model. Throughout these docs image_gen is just an example event name — your label for one billable action. Name it whatever your app does: chat_message, transcription,render. PayKit only counts and bills.
Getting started

Quickstart

Pick the path that matches your stack. All three hit the same API.

1 · No-code — any website, 30 seconds

Paste one line. Renders a live credits meter + Buy button and wires every data-paykit-* element. Works on Webflow, Wix, WordPress, plain HTML.

html
<div id="paykit"></div>
<script src="https://paykit-two.vercel.app/embed.js" data-key="pk_live_…"></script>
 
<!-- spend a credit on click -->
<button data-paykit-meter="image_gen">Generate</button>
 
<!-- show only to Pro users -->
<div data-paykit-plan="pro">Pro-only content</div>

2 · React

tsx
import { PayKitProvider, Paywall, usePayKit } from "@paykit/react"
 
export default function App({ user }) {
return (
<PayKitProvider userId={user.id}>
<ImageStudio />
<Paywall plan="pro" fallback={<UpgradeCard />}>
<HDUpscale /> {/* gated — Pro only */}
</Paywall>
</PayKitProvider>
)
}
 
function ImageStudio() {
const { meter, account } = usePayKit()
async function generate() {
const { blocked } = await meter("image_gen") // −1 credit
if (blocked) return openBuyCredits()
runYourModel()
}
return <button onClick={generate}>Generate ({account?.credits} left)</button>
}

3 · Any backend (REST)

Meter from your server, where it's safe — never trust the client for billing.

bash
curl -X POST https://paykit-two.vercel.app/api/v1/meter \
-H "x-paykit-key: sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "userId": "user_123", "event": "image_gen" }'
# → { "ok": true, "remaining": 4 }
Concepts

Core concepts

ConceptWhat it is
AccountOne of your users, identified by a userId you choose. Holds plan, credits, entitlements. Created on first touch with 5 free credits.
CreditA unit of usage. meter() deducts credits; packs and grants add them. Deduction is atomic — never goes negative.
Planfree or pro. A plan maps to entitlements (pro → ["pro"]). Drives <Paywall> and hasAccess().
ProjectYour tenant boundary. Each has a publishable key (pk_live_…) and a secret key (sk_live_…).
Demo projectpk_live_demo / sk_live_demo — an open public sandbox. Try the API with zero setup; never ship it.

Keys — and where they go

KeyUse it…Never…
pk_live_Browser, embed script, access/meter reads
sk_live_Server-side only: granting credits, secure meteringin client code or a public repo

PayKit reads the key from (in order): the x-paykit-key header, a key field in the JSON body, or a ?key= query param. No key → the demo project. An unknown key → 401.

Concepts

Metering & credits

Deduction is atomic and stops at zero — a user can't go negative. When credits run out, meter() returns blocked: true (REST: 402) and your code decides what to do. This is the safe default: you never give away unpaid usage.

ts
const { blocked } = await meter("image_gen")
if (blocked) return openBuyCredits() // don't run the model
runYourModel()

Per-call pricing

Pass a cost to charge different amounts per action:

ts
await meter("hd_upscale", 4) // costs 4 credits

Secure metering

By default /meter accepts the publishable key (handy for client/embed use). FlipsecureMetering on a project (PATCH /projects) to require the secret key, so only your server can spend credits. Recommended once you're past the demo.

Concepts

Billing & Stripe

text
User clicks Buy ──▶ POST /checkout ──▶ Stripe Checkout ──▶ payment
ledger updated ◀── POST /webhook ◀── checkout.session.completed
(credits granted / plan = pro)
ProductDetail
creditsOne-time $9 for a 100-credit pack.
pro$19/mo subscription. Cancelling reverts the user to free.

The userId rides along in Stripe metadata, so the webhook credits the exact account. For local testing: stripe listen --forward-to localhost:3000/api/v1/webhook.

Reference

React SDK

<PayKitProvider userId>

Wrap your app (or the authed part). Loads the account on mount and exposes the context. The one required prop is userId — your stable id for the current user.

usePayKit()

ts
const {
account, // { userId, plan, credits, entitlements } | null
loading, // boolean — true until the first load resolves
refresh, // () => Promise<void> — re-fetch the account
meter, // (event, cost=1) => Promise<{ ok, remaining, blocked? }>
buyCredits, // (amount) => Promise<void> — local grant (no Stripe; dev/demo)
upgrade, // (plan) => Promise<void> — local plan change (no Stripe; dev/demo)
checkout, // (kind: "credits" | "pro") => Promise<void> — real Stripe, redirects
hasAccess, // (plan) => boolean — entitlement check, sync
} = usePayKit()
MethodNotes
meter(event, cost?)Deduct cost credits (default 1). Returns { ok, remaining } or { blocked: true } when insufficient.
checkout(kind)Opens Stripe Checkout and redirects. Throws if Stripe isn't configured.
buyCredits / upgradeLocal stand-ins that change the ledger with no payment. Dev & demo only.

<Paywall plan fallback>

tsx
<Paywall plan="pro" fallback={<UpgradeCard />}>
<PremiumFeature />
</Paywall>
!
<Paywall> and hasAccess() are client-side UX gates, not security. Always re-check entitlements on your server before doing privileged work.
Reference

Embed script

One script tag, configured with attributes:

AttributeDefaultPurpose
data-keyYour publishable key. Identifies the project.
data-userper-browser idYour logged-in user's id. Omit → one is generated & stored in localStorage.
data-accent#34d399Brand colour for the meter widget.
data-basescript originAPI origin, if you self-host the API elsewhere.

Declarative attributes

html
<button data-paykit-meter="image_gen">Generate</button> <!-- spends 1 credit -->
<div data-paykit-plan="pro">Pro-only content</div> <!-- hidden unless Pro -->

Imperative API — window.PayKit

js
PayKit.meter("image_gen") // Promise<{ ok, remaining, blocked? }>, repaints meter
PayKit.buy() // start Stripe Checkout for a credit pack
PayKit.refresh() // re-fetch the account
PayKit.account() // the cached account object
PayKit.user // the resolved user id
Reference

REST API

Base URL https://paykit-two.vercel.app/api/v1. Auth via the x-paykit-key header, a key body field, or ?key=.

GET/access

Read an account. Safe with the publishable key.

bash
curl "…/access?userId=user_123&key=pk_live_…"
# → { "userId": "user_123", "plan": "free", "credits": 4, "entitlements": [] }
POST/meter

Deduct credits for one billable action. Call from your server.

json
// body: { "userId": "user_123", "event": "image_gen", "cost": 1 }
// → 200 { "ok": true, "remaining": 3 }
// → 402 { "ok": false, "blocked": true, "remaining": 0 } (out of credits)

Errors: missing fields 400 · invalid key 401 · project requires secret key 403.

POST/credits

Grant credits and/or change plan — server-side, authoritative. Requires the secret key for real projects.

json
// body: { "userId": "user_123", "amount": 100, "plan": "pro" }
// → { "userId": "user_123", "plan": "pro", "credits": 103, "entitlements": ["pro"] }
POST/checkout

Create a Stripe Checkout Session (inline prices — no dashboard setup). Redirect the user to the returned url.

json
// body: { "userId": "user_123", "kind": "pro", "key": "pk_live_…" }
// → { "url": "https://checkout.stripe.com/c/pay/cs_test_…" }
// no STRIPE_SECRET_KEY → 501
POST/webhook

Stripe's endpoint — you don't call this. Point a Stripe webhook at it and set STRIPE_WEBHOOK_SECRET. It verifies the signature, then grants credits / sets the plan on checkout.session.completed and reverts to free on customer.subscription.deleted.

GET/analytics · /accounts

Dashboard data, scoped by key. /analytics returns usage series + top events + stats { total, pro, mrr, creditsOutstanding } (MRR = pro × $19). /accounts returns the account list + stats.

GET · POST · PATCH/projects

Manage tenants (Clerk-authed for create/list). POST { name? } creates a project and returns its keys; PATCH { secureMetering, key: sk_… } toggles secure metering.

Operate

Self-hosting

PayKit runs with zero config (in-memory store + simulate buttons). Add env vars to go real:

VariableRequired forNotes
DATABASE_URLpersistencepostgres://… (e.g. Neon). Tables auto-create & migrate. Without it → in-memory.
STRIPE_SECRET_KEYcheckout + webhooksk_test_… / sk_live_…
STRIPE_WEBHOOK_SECRETwebhookfrom stripe listen or your dashboard endpoint.
NEXT_PUBLIC_BASE_URLredirect URLsyour public origin. Falls back to the request origin.
CLERK_*multi-tenant authoptional — without it, ownership falls back to a shared demo owner.
bash
cp .env.example .env.local
npm install
npm run dev # http://localhost:3000

Stack: Next.js 15 (App Router) · React 19 · Postgres (atomic deduct) · Stripe 17 · Clerk (optional).

Operate

Security

RuleWhy
Secret key server-side onlypk_live_ is browser-safe; sk_live_ never ships to the client.
Enforce access on the server<Paywall> / hasAccess() are UX gates, not security.
Turn on secure meteringSo only your backend can spend credits.
Verify webhooksKeep STRIPE_WEBHOOK_SECRET set; the endpoint checks the Stripe signature.
Don't ship the demo projectpk_live_demo / sk_live_demo is a shared open sandbox.
Operate

FAQ

Does PayKit generate images / run my model?
No. It meters and bills. image_gen is just an example event name — call your events whatever you like.
What's a credit worth?
Whatever you decide. One meter() call deducts cost credits (default 1); price your packs to match your model costs.
How many free credits?
New accounts start with 5.
Do I need Stripe to try it?
No — the in-memory store + buyCredits/upgrade stand-ins let you build the whole flow before adding a single key.
Ready to charge for your AI app?

Start with the demo project — no keys, no setup.

Back to Quickstart