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);
items?CheckoutItem[]
At least one { product, quantity? }. product is a Price.checkoutRef, never a Product.id.
CheckoutItem[]successUrl?string
Where the customer lands after paying. Requires the checkoutSuccessUrl capability.
stringcustomerId?string
Attach an existing provider customer.
stringcustomerEmail?string
Prefill the email for a new customer.
stringmetadata?Record<string, string | number | boolean>
Copied onto the resulting order/subscription where the provider supports it.
Record<string, string | number | boolean>signal?AbortSignal
Abort the request.
AbortSignalThe 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
id?string
Provider checkout identifier.
stringurl?string
Where to send the customer. Empty string when the provider did not return one.
stringstatus?'open' | 'complete' | 'expired' | null
null when the provider exposes no checkout status (Lemon Squeezy).
'open' | 'complete' | 'expired' | nullcustomerId?string
Provider customer, when known.
stringcustomerEmail?string
Customer email, when known.
stringsubscriptionId?string
The subscription the checkout created, when known.
stringmetadata?Record<string, string | number | boolean>
Metadata echoed back by the provider.
Record<string, string | number | boolean>expiresAt?Date
When the checkout link expires, when the provider says so.
Dateraw?unknown
The untouched provider payload.
unknownItems, 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
quantityother than1. - Lemon Squeezy rejects more than one item, and rejects
customerId(passcustomerEmailinstead). - Paddle rejects
successUrl— the client gates this one before the request, via thecheckoutSuccessUrlcapability.
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-sdkwritessubscription_data[metadata]as well whenever the checkout is in subscription mode, sosubscription.metadatacarries your keys. - Lemon Squeezy never attaches custom data to the subscription resource. It travels only in the
webhook envelope (
meta.custom_data), which is whySubscription.metadatais populated on Lemon Squeezy webhook events but isundefinedwhen 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.
}