Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

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 false makes the operation or the option it guards throw unsupported. 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 capabilitypauseBehaviors, 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 unsupported too, 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.
  • unsupported is not validation. validation means the arguments are malformed and would be wrong on every provider — an empty ID, a negative limit. unsupported means 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',
});

Last updated on August 8, 2026

Was this page helpful?