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

Checkouts

Create and read checkouts, the per-provider item and success-URL support matrix, metadata propagation, and status semantics.

A checkout is the hand-off from your app to the provider’s payment page. checkouts.create returns a normalized Checkout whose url you redirect the customer to; checkouts.get reads one back.

Creating a checkout

const checkout = await client.checkouts.create({
  items: [{ product: price.checkoutRef, quantity: 1 }],
  customerEmail: '[email protected]',
  successUrl: 'https://example.com/thanks',
  metadata: { userId: 'user_123' },
});

redirect(checkout.url);
PropType
items?CheckoutItem[]

At least one { product, quantity? }. product is a Price.checkoutRef, never a Product.id.

TypeCheckoutItem[]
successUrl?string

Where the customer lands after paying. Requires the checkoutSuccessUrl capability.

Typestring
customerId?string

Attach an existing provider customer.

Typestring
customerEmail?string

Prefill the email for a new customer.

Typestring
metadata?Record<string, string | number | boolean>

Copied onto the resulting order/subscription where the provider supports it.

TypeRecord<string, string | number | boolean>
signal?AbortSignal

Abort the request.

TypeAbortSignal

The client validates items before any request is made: an empty array, an empty product string, or a quantity that isn’t a positive integer throws RevenueError with code validation.

The Checkout model

PropType
id?string

Provider checkout identifier.

Typestring
url?string

Where to send the customer. Empty string when the provider did not return one.

Typestring
status?'open' | 'complete' | 'expired' | null

null when the provider exposes no checkout status (Lemon Squeezy).

Type'open' | 'complete' | 'expired' | null
customerId?string

Provider customer, when known.

Typestring
customerEmail?string

Customer email, when known.

Typestring
subscriptionId?string

The subscription the checkout created, when known.

Typestring
metadata?Record<string, string | number | boolean>

Metadata echoed back by the provider.

TypeRecord<string, string | number | boolean>
expiresAt?Date

When the checkout link expires, when the provider says so.

TypeDate
raw?unknown

The untouched provider payload.

Typeunknown

Items, quantity, and success URL support

Provider Multiple items quantity > 1 successUrl customerId on create
Polar yes no yes yes
Lemon Squeezy no (one) yes yes (redirect_url) no (use customerEmail)
Stripe yes yes yes yes
Paddle yes yes no — see below yes (email is resolved to a customer)
Dodo Payments yes yes yes (return_url) yes

Unsupported combinations throw RevenueError with code unsupported rather than silently dropping your input:

  • Polar rejects any item quantity other than 1.
  • Lemon Squeezy rejects more than one item, and rejects customerId (pass customerEmail instead).
  • Paddle rejects successUrl — the client gates this one before the request, via the checkoutSuccessUrl capability.

Check the capability before offering a success URL in provider-agnostic code:

await client.checkouts.create({
  items: [{ product: price.checkoutRef }],
  successUrl: client.capabilities.checkoutSuccessUrl
    ? 'https://example.com/thanks'
    : undefined,
});

Metadata propagation

metadata is a flat Record<string, string | number | boolean>. Where it ends up differs per provider:

Provider Written as Reaches the subscription?
Polar checkout metadata yes — Polar copies it onto the order/subscription
Lemon Squeezy checkout_data.custom only via webhooks, as meta.custom_data
Stripe session metadata and subscription_data[metadata] yes — the SDK writes both
Paddle transaction custom_data yes — carried onto the subscription
Dodo Payments checkout metadata yes

Two of these deserve attention:

  • Stripe does not copy session metadata onto the created subscription. revenue-sdk writes subscription_data[metadata] as well whenever the checkout is in subscription mode, so subscription.metadata carries your keys.
  • Lemon Squeezy never attaches custom data to the subscription resource. It travels only in the webhook envelope (meta.custom_data), which is why Subscription.metadata is populated on Lemon Squeezy webhook events but is undefined when you fetch the subscription over the API.

Checkout status semantics

Checkout.status is a three-value union plus null:

status Meaning
open Not paid (yet). The link may still be used.
complete Paid. Safe to fulfill.
expired The checkout failed, was canceled, or timed out.
null The provider exposes no checkout status (Lemon Squeezy).

Per-provider mapping:

Provider complete expired open
Polar succeeded expired, failed everything else
Lemon Squeezy — (status is always null)
Stripe complete and payment_status !== 'unpaid' expired open, and complete while payment_status === 'unpaid'
Paddle paid, completed canceled everything else
Dodo Payments payment_status: succeeded cancelled, failed everything else

Reading a checkout back

const checkout = await client.checkouts.get({ id: checkoutId });

if (checkout.status === 'complete') {
  // Paid.
}

Last updated on August 6, 2026

Was this page helpful?