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.
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
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.
// 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.
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 | — |