Checkout sessions provide a secure, hosted checkout experience for your customers. This guide covers creating checkouts, handling responses, and managing the complete checkout lifecycle.
Overview
A checkout session:
Creates a secure, time-limited checkout URL
Handles payment collection via Stripe
Processes webhooks for completion
Redirects customers to your success URL
Basic Checkout
Create a simple checkout for a product price:
import { Polar } from '@polar-sh/sdk'
const polar = new Polar ({
accessToken: process . env . POLAR_API_KEY ,
})
const checkout = await polar . checkouts . create ({
productPriceId: 'price_...' ,
successUrl: 'https://yoursite.com/success' ,
})
// Redirect customer to checkout.url
console . log ( checkout . url )
Checkout Parameters
Required Parameters
The ID of the product price to purchase.
Optional Parameters
URL to redirect after successful checkout. Use {CHECKOUT_ID} placeholder. https://yoursite.com/success?checkout_id={CHECKOUT_ID}
Pre-fill customer email address.
Pre-fill billing address: {
line1 : '123 Main St' ,
line2 : 'Suite 100' ,
city : 'San Francisco' ,
state : 'CA' ,
postalCode : '94105' ,
country : 'US'
}
Associate checkout with existing customer ID.
Enable discount code input field.
Custom metadata (key-value pairs) attached to the checkout. {
userId : '12345' ,
source : 'mobile-app' ,
campaign : 'summer-sale'
}
Complete Example
Create checkout with all options
const checkout = await polar . checkouts . create ({
productPriceId: 'price_...' ,
successUrl: 'https://yoursite.com/success?checkout_id={CHECKOUT_ID}' ,
customerEmail: '[email protected] ' ,
customerName: 'Jane Doe' ,
customerBillingAddress: {
line1: '123 Main Street' ,
city: 'San Francisco' ,
state: 'CA' ,
postalCode: '94105' ,
country: 'US' ,
},
allowDiscountCodes: true ,
metadata: {
userId: user . id ,
source: 'web' ,
referrer: req . headers . referer ,
},
})
Redirect to checkout
// Server-side redirect
res . redirect ( checkout . url )
// Or return URL for client-side redirect
return { checkoutUrl: checkout . url }
Handle success redirect
// /success?checkout_id=checkout_abc123
app . get ( '/success' , async ( req , res ) => {
const checkoutId = req . query . checkout_id
// Verify checkout status
const checkout = await polar . checkouts . get ( checkoutId )
if ( checkout . status === 'confirmed' ) {
// Checkout completed successfully
res . render ( 'success' , { checkout })
} else {
// Still processing
res . render ( 'processing' )
}
})
Checkout States
Checkouts progress through several states:
open
Initial state. Customer can complete checkout.
processing
Payment is being processed.
confirmed
Payment successful. Order and subscription created.
failed
Payment failed. Customer can retry.
expired
Checkout session expired (default: 24 hours).
Retrieving Checkout Status
Check checkout status at any time:
const checkout = await polar . checkouts . get ( 'checkout_abc123' )
switch ( checkout . status ) {
case 'open' :
console . log ( 'Waiting for payment' )
break
case 'processing' :
console . log ( 'Processing payment...' )
break
case 'confirmed' :
console . log ( 'Payment successful!' )
console . log ( 'Order ID:' , checkout . order ?. id )
console . log ( 'Subscription ID:' , checkout . subscription ?. id )
break
case 'failed' :
console . log ( 'Payment failed' )
break
case 'expired' :
console . log ( 'Checkout expired' )
break
}
Webhook Integration
Don’t rely solely on the success redirect. Always use webhooks for order fulfillment.
Handle checkout completion via webhooks:
app . post ( '/webhooks/polar' , async ( req , res ) => {
const signature = req . headers [ 'polar-signature' ]
try {
const event = polar . webhooks . verifyEvent (
req . body ,
signature ,
process . env . POLAR_WEBHOOK_SECRET
)
if ( event . type === 'checkout.updated' ) {
const checkout = event . data
if ( checkout . status === 'confirmed' ) {
// Fulfill order
await fulfillOrder ({
customerId: checkout . customer . id ,
productId: checkout . product . id ,
metadata: checkout . metadata ,
})
}
}
res . json ({ received: true })
} catch ( error ) {
res . status ( 400 ). json ({ error: 'Invalid signature' })
}
})
Discount Codes
Customers can apply discount codes during checkout when enabled:
const checkout = await polar . checkouts . create ({
productPriceId: 'price_...' ,
allowDiscountCodes: true , // Enable discount input
})
To validate discount codes before checkout:
try {
const discount = await polar . discounts . getByCode ({
code: 'SUMMER20' ,
organizationId: 'org_...' ,
})
if ( discount . active && discount . productId === productId ) {
console . log ( `Discount: ${ discount . percentOff } % off` )
}
} catch ( error ) {
console . log ( 'Invalid discount code' )
}
Custom Prices
For products with custom pricing, let customers enter their amount:
const checkout = await polar . checkouts . create ({
productPriceId: 'price_custom_...' ,
amount: 5000 , // $50.00 in cents
currency: 'USD' ,
})
Custom prices must be enabled on the product and have a minimum amount configured.
One-Time vs Subscription
Checkout behavior differs based on product type:
One-Time Purchase:
Creates an order immediately
Customer charged once
No recurring billing
Subscription:
Creates a subscription
Recurring billing based on interval
Customer can manage in portal
Trial Subscriptions
For products with trial periods:
const checkout = await polar . checkouts . create ({
productPriceId: 'price_with_trial_...' ,
// Customer not charged until trial ends
})
The checkout will:
Create subscription in trialing status
Not charge immediately
Charge full price after trial period
Send trial ending reminders
Error Handling
Handle common checkout errors:
try {
const checkout = await polar . checkouts . create ({ ... })
} catch ( error ) {
if ( error . statusCode === 404 ) {
// Product price not found
console . error ( 'Invalid product price ID' )
} else if ( error . statusCode === 422 ) {
// Validation error
console . error ( 'Invalid checkout data:' , error . body )
} else if ( error . statusCode === 403 ) {
// Organization not ready for payments
console . error ( 'Payment processor not configured' )
} else {
// Unexpected error
console . error ( 'Checkout creation failed:' , error )
}
}
Testing
Test Mode
Use test API keys for development:
POLAR_API_KEY = polar_sk_test_...
Test Cards
Use Stripe test cards:
Success : 4242 4242 4242 4242
Declined : 4000 0000 0000 0002
3D Secure : 4000 0025 0000 3155
Webhook Testing
Test webhooks locally with ngrok:
Add the ngrok URL to your Polar webhook settings:
https://abc123.ngrok.io/webhooks/polar
Best Practices
Always create checkouts server-side
Never expose API keys in client code
Verify webhook signatures
Use HTTPS for all URLs
Pre-fill customer information when available
Show loading states during redirect
Provide clear error messages
Handle network failures gracefully
Use webhooks for order fulfillment
Implement idempotent fulfillment
Log all checkout events
Monitor checkout conversion rates
Checkout Customization
Customize the checkout experience:
Branding : Configure in Polar dashboard under Settings > Branding
Email Templates : Customize confirmation emails
Terms of Service : Add your terms URL
Privacy Policy : Add your privacy policy URL
Next Steps
Subscription Upgrades Handle plan upgrades and changes
Customer Portal Let customers manage their subscriptions