Capabilities
RevenueCapabilities field by field, the fail-loud philosophy behind the unsupported error, and how to check capabilities before you call.
Billing providers are not interchangeable. Paddle has no API-hosted checkout; Lemon Squeezy can’t revoke
a subscription immediately; Dodo Payments can’t end a trial early; Polar has no “switch now, bill
nothing” proration mode. Rather than hide these differences and silently drop options, revenue-sdk
advertises each provider’s capabilities and gates on them.
RevenueCapabilities
Every provider exposes a capabilities object, also surfaced on client.capabilities:
cancellationReason?boolean
Whether subscriptions.cancel forwards reason/comment to the provider. false for Lemon Squeezy and Paddle.
booleancheckoutStatus?boolean
Whether Checkout.status is populated. false for Lemon Squeezy, whose checkouts carry no lifecycle status.
booleancheckoutSuccessUrl?boolean
Whether checkouts.create accepts successUrl. false for Paddle — configure the redirect in Paddle.js instead.
booleanendTrial?boolean
Whether subscriptions.endTrial is supported. false for Dodo Payments.
booleanhostedCheckout?boolean
Whether checkouts.create returns a ready-to-use provider-hosted URL. false for Paddle, which needs your own Paddle.js page.
booleanlistSubscriptionsByCustomer?boolean
Whether subscriptions.list accepts a customerId filter. false for Lemon Squeezy.
booleanportalReturnUrl?boolean
Whether customerPortal.createSession accepts returnUrl. false for Lemon Squeezy and Paddle.
booleanprorationBehaviors?('prorate' | 'invoice_now' | 'none')[]
The proration behaviors subscriptions.changePlan accepts on this provider.
('prorate' | 'invoice_now' | 'none')[]revoke?boolean
Whether subscriptions.revoke (cancel immediately) is supported. false for Lemon Squeezy.
booleanuncancel?boolean
Whether a scheduled cancellation can be reverted via subscriptions.uncancel. true everywhere today.
booleanconst client = createClient({ provider: paddle({ apiKey: process.env.PADDLE_API_KEY! }) });
client.capabilities.hostedCheckout; // false
client.capabilities.checkoutSuccessUrl; // false
client.capabilities.prorationBehaviors; // ['invoice_now', 'none', 'prorate']
The complete five-provider table is in the capability matrix.
Fail loud, never silently drop
When you pass an option the provider can’t honor, the client throws a RevenueError with code
unsupported before making a request:
// Paddle configures success redirects in Paddle.js, not in the API.
await client.checkouts.create({
items: [{ product: priceId }],
successUrl: 'https://example.com/thanks',
});
// → RevenueError { code: 'unsupported', provider: 'paddle' }
The alternative — quietly ignoring successUrl — would ship a checkout that strands customers on the
provider’s page with no way back, and you’d find out from a support ticket. A thrown error surfaces the
difference at integration time, in your own test suite.
The client gates on:
checkouts.create({ successUrl })— requirescheckoutSuccessUrlsubscriptions.list/listAll({ customerId })— requireslistSubscriptionsByCustomersubscriptions.cancel({ reason | comment })— requirescancellationReasonsubscriptions.uncancel— requiresuncancelsubscriptions.endTrial— requiresendTrialsubscriptions.revoke— requiresrevokesubscriptions.changePlan({ prorationBehavior })— the value must be inprorationBehaviorscustomerPortal.createSession({ returnUrl })— requiresportalReturnUrl
Adapters raise unsupported for the finer-grained limits a boolean can’t express — a Polar checkout
with quantity > 1, a Lemon Squeezy checkout with more than one item or a customerId, a Polar or
Lemon Squeezy plan change with quantity > 1.
Check before you call
Because capabilities are plain data, you can branch on them to build a UI that adapts to whichever provider is configured:
// Only offer a "cancel immediately" button where it exists.
if (client.capabilities.revoke) {
showRevokeButton();
}
// Only render a "why are you leaving?" survey where the reason is forwarded.
const reason = client.capabilities.cancellationReason ? survey.reason : undefined;
await client.subscriptions.cancel({ id, reason });
// Pick a proration behavior the provider actually has.
const proration = client.capabilities.prorationBehaviors.includes('none') ? 'none' : 'prorate';
await client.subscriptions.changePlan({ id, product, prorationBehavior: proration });
hostedCheckout is the one capability that changes your architecture rather than an argument: when it
is false you must render your own Paddle.js page instead of redirecting to the returned URL. Decide
that at build time, not per request.
if (client.capabilities.hostedCheckout) {
redirect(checkout.url); // Polar, Lemon Squeezy, Stripe, Dodo Payments
} else {
// Paddle: hand checkout.id / the transaction to Paddle.js on your own page.
renderPaddleCheckout(checkout.id);
}
Simulating capability gaps in tests
The in-memory provider defaults to every capability enabled, and accepts a Partial<RevenueCapabilities>
override so you can assert your fallbacks:
import { createClient } from 'revenue-sdk';
import { createInMemoryProvider } from 'revenue-sdk/testing';
const provider = createInMemoryProvider({}, { name: 'paddle', capabilities: { revoke: false } });
const client = createClient({ provider });
await expect(client.subscriptions.revoke({ id: 'sub-1' })).rejects.toMatchObject({
code: 'unsupported',
});