The unified billing SDK for TypeScript
revenue-sdk is an open-source TypeScript SDK that wraps Stripe, Polar, Paddle, Lemon Squeezy, and Dodo Payments in one normalized API — products, checkouts, subscriptions, customers, and webhooks included.
revenue-sdk at a glance
- Providers
- Stripe, Polar, Paddle, Lemon Squeezy, Dodo Payments
- Runtime dependencies
- Zero — fetch and Web Crypto only
- Runs on
- Node.js ≥ 22, Cloudflare Workers, Deno, Bun
- Language
- TypeScript, fully typed, ESM only
- License
- MIT — free for commercial use
import { createClient } from 'revenue-sdk';
import { polar } from 'revenue-sdk/polar';
const client = createClient({
provider: polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }),
});
const [product] = (await client.products.list()).items;
const checkout = await client.checkouts.create({
items: [{ product: product.prices[0].checkoutRef }],
customerEmail: '[email protected]',
successUrl: 'https://example.com/thanks',
});
console.log(checkout.url);Switch billing providers in two lines
The provider factory is the only provider-specific code in your integration. Swap it out and every call, every type, and every webhook handler keeps working — revenue-sdk normalizes what the providers spell differently.
See what changes when you switch import { createClient } from 'revenue-sdk';
-import { lemonSqueezy } from 'revenue-sdk/lemon-squeezy';
+import { stripe } from 'revenue-sdk/stripe';
const client = createClient({
- provider: lemonSqueezy({
- apiKey: process.env.LEMON_SQUEEZY_API_KEY!,
- storeId: process.env.LEMON_SQUEEZY_STORE_ID!,
- }),
+ provider: stripe({ secretKey: process.env.STRIPE_SECRET_KEY! }),
});
// Everything below stays exactly the same.
const checkout = await client.checkouts.create({
items: [{ product: price.checkoutRef }],
successUrl: 'https://example.com/thanks',
});Why revenue-sdk?
Because billing providers agree on almost nothing. revenue-sdk gives you one set of models, statuses, errors, and events across all five — and tells you exactly where providers differ instead of papering over it.
- One unified API
- Products, checkouts, subscriptions, customers, and webhooks over five providers.
- Zero dependencies
- Just fetch and Web Crypto. Nothing to audit, nothing to bloat your bundle.
- Workers-ready
- No node:* imports. Runs on Cloudflare Workers without nodejs_compat.
- One status model
- Seven statuses across all providers, with cancelAtPeriodEnd split out.
- Webhook verify & parse
- Standalone helpers that take a Web-standard Request — no client needed.
- Tree-shakable subpaths
- Each provider on its own import path, so unused providers never bundle.
- Capability gating
- Providers differ. Unsupported options throw instead of being silently dropped.
- In-memory testing provider
- Exercise your billing integration without a sandbox account or a test card.
One subscription status model
Every provider spells cancellation differently, and most overload one status to mean both “ended” and “still paid up until the period ends”. revenue-sdknormalizes all of it into seven statuses pluscancelAtPeriodEnd, socanceled always means terminal.
// canceled is terminal. A scheduled cancellation keeps the status
// and sets cancelAtPeriodEnd — the same on all five providers.
const entitled = subscription.status === 'active' || subscription.status === 'trialing';
if (entitled && subscription.cancelAtPeriodEnd) {
banner(`Your plan ends on ${subscription.endsAt?.toLocaleDateString()}.`);
}import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/stripe';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const headers = request.headers;
const body = await request.text();
const valid = await verifyWebhook({ headers, body, secret: env.STRIPE_WEBHOOK_SECRET });
if (!valid) return new Response('invalid signature', { status: 401 });
const event = await parseWebhookEvent({ headers, body });
if (await seen(event.idempotencyKey)) return new Response(null, { status: 204 });
if (event.type === 'subscription.updated') {
// A scheduled cancellation arrives here, not as subscription.canceled.
console.log(event.subscription.status, event.subscription.cancelAtPeriodEnd);
}
return new Response(null, { status: 204 });
},
};Webhooks without the ceremony
verifyWebhook andparseWebhookEvent are standalone helpers that take a Web-standard Request — no client needed. They know each provider's signature scheme, including the two that look identical and aren't.
License keys without shipping your secret key
Validate, activate, and deactivate license keys straight from your desktop or mobile app. The standalone helpers take no merchant credential, so your API key never leaves the server — supported on Polar, Lemon Squeezy, and Dodo Payments.
Read the license key guide// In the shipped app — no merchant credential required.
import { validateLicenseKey } from 'revenue-sdk/polar';
const { valid } = await validateLicenseKey({
key: enteredKey,
organizationId: 'YOUR_ORG_ID',
});
// On your server, with the merchant credential.
const licenseKey = await client.licenseKeys.get({ id: event.licenseKeyId });// Meter AI tokens, API calls, seats — anything countable.
await client.usage.report({
customerId: 'cus_123',
eventName: 'api_request',
value: 25,
idempotencyKey: 'req_8f3a2c', // safe to replay, never double-billed
});Usage-based billing in one call
Report metered usage with one validated call. revenue-sdk never retries usage events on its own, so a flaky network cannot double-bill your customers — supported on Polar, Stripe, and Dodo Payments.
Read the usage guideTest billing flows without a sandbox
revenue-sdk/testing ships a seedable in-memory provider that implements the full provider interface, plussignWebhook to exercise your webhook handler with genuinely valid signatures.
import { createClient } from 'revenue-sdk';
import { createInMemoryProvider } from 'revenue-sdk/testing';
const provider = createInMemoryProvider({
products: [{ id: 'pro', name: 'Pro', prices: [{ amount: 2900, interval: 'month' }] }],
customers: [{ id: 'cus-1', email: '[email protected]' }],
subscriptions: [{ id: 'sub-1', customerId: 'cus-1', productId: 'pro', status: 'active' }],
});
// No account, no API keys, no test cards.
const client = createClient({ provider });Providers differ. revenue-sdk tells you how.
Every provider ships a typed capability object, and the client throwsunsupported instead of silently dropping options.
| Capability | Polar | Lemon Squeezy | Stripe | Paddle | Dodo Payments |
|---|---|---|---|---|---|
| Provider-hosted checkout | — | ||||
| Checkout success URL | — | ||||
| Cancel immediately | — | ||||
| End trial early | — |
Frequently asked questions
- Which billing providers does revenue-sdk support?
- Stripe, Polar, Paddle, Lemon Squeezy, and Dodo Payments — each behind the same unified API, each on its own import subpath so unused providers never reach your bundle. You can also plug in any other provider with a custom adapter.Write a custom adapter
- Can I switch billing providers without a rewrite?
- Yes. The provider factory is the only provider-specific line in your code — swap lemonSqueezy() for stripe() and every call, type, and webhook handler stays the same. Real differences are surfaced through a typed capability object instead of silent behavior changes.See how capabilities work
- Does revenue-sdk run on Cloudflare Workers?
- Yes. revenue-sdk has zero runtime dependencies and no node:* imports, so it runs on Cloudflare Workers without the nodejs_compat flag — and on Node.js 22+, Deno, and Bun.Read the Workers guide
- How does revenue-sdk handle webhooks?
- Each provider subpath exports standalone verifyWebhook and parseWebhookEvent helpers that check the provider’s signature scheme and normalize events into one typed union — no client instance required.Build a webhook handler
- How do I test a billing integration without a sandbox account?
- Use the in-memory provider from revenue-sdk/testing: seed products, customers, and subscriptions, then run every client call against it in your test runner — no API keys, no fetch stubs, no test cards.Explore the testing provider
- Is revenue-sdk free to use?
- Yes. revenue-sdk is open source under the MIT license and free for commercial use; you pay only your billing provider’s regular fees.View the source on GitHub
- Is revenue-sdk a payment processor?
- No. revenue-sdk is an open-source TypeScript SDK that talks to your billing provider’s API — you keep your own Stripe, Polar, Paddle, Lemon Squeezy, or Dodo Payments account, and money always flows through the provider.Start the quickstart
Start here
Ship billing once, keep every provider open
Install revenue-sdk, wire up the provider you use today, and keep the freedom to change your mind tomorrow.