Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
Zero dependencies · Workers-ready

One billing API.
Five providers.

A unified, normalized TypeScript API over Polar, Lemon Squeezy, Stripe, Paddle, and Dodo Payments. Write your checkout, subscription, and webhook logic once — not five times.

$ npm install revenue-sdk
import { createClient } from 'revenue-sdk';
import { polar } from 'revenue-sdk/polar';

const client = createClient({
  provider: polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }),
});

const checkout = await client.checkouts.create({
  items: [{ product: 'PRODUCT_ID' }],
  customerEmail: 'ada@example.com',
  successUrl: 'https://example.com/thanks',
});
console.log(checkout.url);

Works with

PolarLemon SqueezyStripePaddleDodo Payments
Node.js ≥ 20Cloudflare WorkersDenoBun

Features

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 (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

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

Start here