Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
Products · Checkouts · Subscriptions · Webhooks

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.

$ npm install revenue-sdk

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.

Read the lifecycle guide
// 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.

Read the webhook guide

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 guide

Test 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.

Explore the testing provider
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.

CapabilityPolarLemon SqueezyStripePaddleDodo Payments
Provider-hosted checkout
Checkout success URL
Cancel immediately
End trial early
See the full capability matrix

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.

$ npm install revenue-sdk