A unified, normalized, zero-dependency, edge-compatible TypeScript SDK over Polar, Lemon Squeezy, Stripe, Paddle, and Dodo Payments. Write your products, checkout, subscription, customer-portal, billing-history, usage and webhook logic once against one normalized API β built on raw fetch and Web Crypto, so it runs on Node, Cloudflare Workers, and other Web-standard runtimes.
- One API, eight namespaces β products, checkouts, customers, subscriptions, customer portal, orders, usage, and license keys behind a single interface.
- Capability gating β providers differ. The client reports what the active provider supports and throws
unsupportedinstead of silently dropping an option. - One subscription status model β seven statuses across all five providers, with
cancelAtPeriodEndsplit out socanceledalways means terminal. - Webhook verify & parse β standalone per-provider helpers that take a Web-standard
Request. No client, no credentials. - Zero dependencies β
fetchand Web Crypto only. Nonode:*imports, so it runs on Cloudflare Workers withoutnodejs_compat. - Tree-shakable and typed β each provider ships on its own subpath, so unused providers never reach your bundle. Every model is normalized (IDs as strings, dates as
Date, amounts in integer minor units) and keeps the untouched provider payload onraw.
npm install revenue-sdkCreate a client from the package root and a provider factory from its subpath.
import { createClient } from 'revenue-sdk';
import { polar } from 'revenue-sdk/polar';
const client = createClient({
provider: polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }),
});Create a checkout and redirect the customer to it.
const { items: products } = await client.products.list({ limit: 10 });
const price = products[0]!.prices[0]!;
const checkout = await client.checkouts.create({
items: [{ product: price.checkoutRef }],
customerEmail: 'ada@example.com',
successUrl: 'https://example.com/thanks',
metadata: { userId: 'user_123' },
});
console.log(checkout.url);Verify and handle the webhook. Read the raw body once, verify it, then parse it.
import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/polar';
export async function handleWebhook(request: Request, secret: string): Promise<Response> {
const headers = request.headers;
const body = await request.text();
if (!(await verifyWebhook({ headers, body, secret }))) {
return new Response('invalid signature', { status: 401 });
}
const event = await parseWebhookEvent({ headers, body });
// Providers retry for days: deduplicate on event.idempotencyKey, which is always set.
switch (event.type) {
case 'subscription.created':
case 'subscription.updated':
case 'subscription.canceled':
// Upsert by event.subscription.id.
break;
case 'order.paid':
// Money received β including renewals.
break;
}
return new Response(null, { status: 204 });
}Check entitlement. A subscription grants access while its status is active or trialing; a scheduled cancellation keeps the status unchanged and sets cancelAtPeriodEnd.
const subscription = await client.subscriptions.get({ id: 'SUBSCRIPTION_ID' });
const entitled = subscription.status === 'active' || subscription.status === 'trialing';Switching providers means swapping the provider argument β the rest of your code stays the same:
-import { polar } from 'revenue-sdk/polar';
+import { stripe } from 'revenue-sdk/stripe';
const client = createClient({
- provider: polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }),
+ provider: stripe({ secretKey: process.env.STRIPE_SECRET_KEY! }),
});| Namespace | Methods |
|---|---|
products |
list Β· listAll Β· get |
checkouts |
create Β· get |
customers |
list Β· listAll Β· get Β· create Β· update |
subscriptions |
list Β· listAll Β· get Β· cancel Β· uncancel Β· changePlan Β· endTrial Β· pause Β· resume Β· revoke |
customerPortal |
createSession |
orders |
list Β· listAll Β· get Β· getInvoiceUrl |
usage |
report |
licenseKeys |
list Β· listAll Β· get Β· update |
Every list returns an opaque cursor and has a listAll async generator that walks the pages for you. Every method accepts a signal for cancellation. Reads are retried once on a rate limit or a transient network error; writes are never replayed.
License keys have a second surface: validateLicenseKey, activateLicenseKey, and deactivateLicenseKey are exported from the provider subpaths and take no credential, so they are safe to call from a customer's machine.
| Provider | Import | Notes |
|---|---|---|
| Polar | revenue-sdk/polar |
Merchant of record. Organization access token, separate sandbox. |
| Lemon Squeezy | revenue-sdk/lemon-squeezy |
Merchant of record. Store-scoped; variants are the purchasable unit. |
| Stripe | revenue-sdk/stripe |
Payment processor. Pinned API version, optional Managed Payments. |
| Paddle | revenue-sdk/paddle |
Merchant of record. Checkout runs on your own domain via Paddle.js. |
| Dodo Payments | revenue-sdk/dodo-payments |
Merchant of record. Live and test mode. |
| Testing | revenue-sdk/testing |
Seedable in-memory provider for your test suite. No API keys. |
Every factory accepts an optional baseUrl (self-hosted gateways and proxies) and an injectable fetch. Providers do not support the same feature set β client.capabilities tells you what the active one can do, and the full breakdown lives in the capability matrix.
revenue-sdk/testing ships a seedable in-memory provider you hand to createClient like any other β no account, no API keys, no fetch stubs, no test cards.
import { createClient } from 'revenue-sdk';
import { createInMemoryProvider } from 'revenue-sdk/testing';
const client = createClient({
provider: createInMemoryProvider({
products: [{ id: 'pro', name: 'Pro', prices: [{ amount: 2900, interval: 'month' }] }],
customers: [{ id: 'cus-1', email: 'ada@example.com' }],
subscriptions: [{ id: 'sub-1', customerId: 'cus-1', productId: 'pro', status: 'active' }],
}),
});It reports every capability enabled by default, and a second options argument simulates a specific provider's identity and capability gaps β so code that branches on client.capabilities is testable. signWebhook produces headers that a provider's own verifyWebhook accepts, so webhook handlers are testable too.
Every failure is a RevenueError with a closed code union: unauthorized, forbidden, not_found, conflict, rate_limited, payment_required, validation, unsupported, provider_error, network_error.
import { RevenueError } from 'revenue-sdk';
try {
await client.subscriptions.revoke({ id: 'SUBSCRIPTION_ID' });
} catch (error) {
if (error instanceof RevenueError && error.code === 'unsupported') {
// This provider cannot cancel immediately β cancel at period end instead.
}
}Errors also carry provider, status, retryAfter and retryable. cause is the underlying JS Error; the provider's parsed body is on responseBody, installed non-enumerable so console.error, JSON.stringify and error reporters cannot pick up customer data by accident. Secrets are never logged or embedded in messages.
Node.js β₯ 22, Cloudflare Workers (without nodejs_compat), Deno, Bun, Vercel Edge, and any other runtime with fetch, Web Crypto and TextEncoder. ESM only.
Full documentation lives at revenue-sdk.dev (docs):
- Quickstart
- Concepts β clients, checkouts, subscriptions, orders, webhooks, pagination, errors
- Guides β pricing page, checkout flow, webhook handler, subscription management, Cloudflare Workers, custom providers
- Capability matrix
- Webhook events
- Error codes
Prerequisites: Node >= 22.
npm install| Script | Description |
|---|---|
npm run build |
Build with tsdown |
npm test |
Run the unit + provider contract test suite |
npm run test:watch |
Run tests in watch mode |
npm run test:live |
Gated live provider tests (read-only) |
npm run typecheck |
Type-check without emitting |
npm run lint |
Lint the codebase |
npm run fmt |
Format with prettier |
Docs site:
npm run docs:devβ run the docs dev servernpm run docs:buildβ build the static site to.blume-dist/
Releases are automated with release-please, driven by Conventional Commits. While pre-1.0.0, breaking changes bump the minor version and features bump the patch version. Merging the release pull request publishes to npm.
revenue-sdk is developed and maintained by Genz IT Solutions GmbH. It powers Capawesome, a cloud platform for mobile apps.