Capabilities
How the client advertises what the configured provider can do, and why an option it cannot honor throws unsupported instead of being silently dropped.
Providers are not interchangeable, so revenue-sdk publishes what the configured one can do as plain
data on client.capabilities and throws rather than quietly dropping an option it cannot honor.
Branch on the data before you call:
// 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 });
Every field of RevenueCapabilities, and its value on all five providers, lives 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.
How the gating works
- A boolean capability that is
falsemakes the operation or the option it guards throwunsupported. For a whole namespace —usage.report,licenseKeys.*,subscriptions.pause— that is the call itself; for an option —successUrl,metadata,customerId,returnUrl— it is only triggered by passing the option. Omitting it is always safe. - A list capability —
pauseBehaviors,prorationBehaviors— throws when the value you pass is not in the list. Omitting the value uses the provider’s own default, so portable code can leave it out. - Adapters raise
unsupportedtoo, for the finer-grained limits a boolean can’t express: a Polar checkout withquantity > 1, a Lemon Squeezy checkout with more than one item or acustomerId, a Polar or Lemon Squeezy plan change withquantity > 1. unsupportedis notvalidation.validationmeans the arguments are malformed and would be wrong on every provider — an empty ID, a negativelimit.unsupportedmeans they are well-formed but this provider has nowhere to put them. See Errors.
hostedCheckout changes your architecture
Most capabilities change one argument. hostedCheckout changes the shape of your integration: 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',
});