6.2 KiB
6.2 KiB
Polar Checkouts
Checkout flows, embedded checkout, and session management.
Checkout Approaches
1. Checkout Links
- Pre-configured shareable links
- Created via dashboard or API
- For marketing campaigns
- Can pre-apply discounts
Create via API:
const link = await polar.checkoutLinks.create({
product_price_id: "price_xxx",
success_url: "https://example.com/success"
});
// Returns: link.url
2. Checkout Sessions (API)
- Programmatically created
- Server-side API call
- Dynamic workflows
- Custom logic
Create Session:
const session = await polar.checkouts.create({
product_price_id: "price_xxx",
success_url: "https://example.com/success?checkout_id={CHECKOUT_ID}",
customer_email: "user@example.com",
external_customer_id: "user_123",
metadata: {
user_id: "123",
source: "web"
}
});
// Redirect to: session.url
Response:
{
"id": "checkout_xxx",
"url": "https://polar.sh/checkout/...",
"client_secret": "cs_xxx",
"status": "open",
"expires_at": "2025-01-15T10:00:00Z"
}
3. Embedded Checkout
- Inline checkout within your site
- Seamless purchase experience
- Theme customization
Implementation:
<script src="https://polar.sh/embed.js"></script>
<div id="polar-checkout"></div>
<script>
const checkout = await fetch('/api/create-checkout', {
method: 'POST',
body: JSON.stringify({ productPriceId: 'price_xxx' })
}).then(r => r.json());
Polar('checkout', {
checkoutId: checkout.id,
clientSecret: checkout.client_secret,
onSuccess: () => {
window.location.href = '/success';
},
theme: 'dark' // or 'light'
});
</script>
Server-side (create session):
app.post('/api/create-checkout', async (req, res) => {
const session = await polar.checkouts.create({
product_price_id: req.body.productPriceId,
embed_origin: "https://example.com",
external_customer_id: req.user.id
});
res.json({
id: session.id,
client_secret: session.client_secret
});
});
Configuration Parameters
Required
product_price_id- Product to checkout (orproductsarray for multiple)success_url- Post-payment redirect (absolute URL)
Optional
external_customer_id- Your user ID mappingembed_origin- For embedded checkoutscustomer_email- Pre-fill emailcustomer_name- Pre-fill namediscount_id- Pre-apply discount codeallow_discount_codes- Allow customer to enter codes (default: true)metadata- Custom data (key-value)custom_field_data- Pre-fill custom fieldscustomer_billing_address- Pre-fill billing address
Success URL Placeholder
{
success_url: "https://example.com/success?checkout_id={CHECKOUT_ID}"
}
// Polar replaces {CHECKOUT_ID} with actual checkout ID
Multi-Product Checkout
const session = await polar.checkouts.create({
products: [
{ product_price_id: "price_1", quantity: 1 },
{ product_price_id: "price_2", quantity: 2 }
],
success_url: "https://example.com/success"
});
Discount Application
Pre-apply Discount
const session = await polar.checkouts.create({
product_price_id: "price_xxx",
discount_id: "discount_xxx",
success_url: "https://example.com/success"
});
Allow Customer Codes
{
allow_discount_codes: true // default
// Set to false to disable code entry
}
Checkout States
open- Ready for paymentconfirmed- Payment successfulexpired- Session expired (typically 24 hours)
Events
Webhook Events:
checkout.created- Session createdcheckout.updated- Session updatedorder.created- Order created after successful paymentorder.paid- Payment confirmed
Handle Success:
// Listen to order.paid webhook
app.post('/webhook/polar', async (req, res) => {
const event = validateEvent(req.body, req.headers, secret);
if (event.type === 'order.paid') {
const order = event.data;
await fulfillOrder(order);
}
res.json({ received: true });
});
Best Practices
-
Success URL:
- Must be absolute URL:
https://example.com/success - Use
{CHECKOUT_ID}placeholder to retrieve checkout details - Verify payment via webhook, not just success redirect
- Must be absolute URL:
-
External Customer ID:
- Set on first checkout
- Never change once set
- Use for all customer operations
- Enables customer lookup without storing Polar IDs
-
Pre-filling Data:
- Pre-fill customer info when available
- Reduces friction in checkout
- Improves conversion rates
-
Embedded Checkout:
- Provide seamless experience
- Match your site's theme
- Handle errors gracefully
- Show loading states
-
Metadata:
- Store tracking info (source, campaign, etc.)
- Link to your internal systems
- Use for analytics and reporting
-
Error Handling:
- Handle expired sessions
- Provide clear error messages
- Offer to create new session
- Log failures for debugging
-
Mobile Optimization:
- Test on mobile devices
- Ensure responsive design
- Consider mobile payment methods
- Test embedded checkout on mobile
Framework Examples
Next.js
// app/actions/checkout.ts
'use server'
export async function createCheckout(productPriceId: string) {
const session = await polar.checkouts.create({
product_price_id: productPriceId,
success_url: `${process.env.NEXT_PUBLIC_URL}/success?checkout_id={CHECKOUT_ID}`,
external_customer_id: await getCurrentUserId()
});
return session.url;
}
// app/product/page.tsx
export default function ProductPage() {
async function handleCheckout() {
const url = await createCheckout(productPriceId);
window.location.href = url;
}
return <button onClick={handleCheckout}>Buy Now</button>;
}
Laravel
Route::post('/checkout', function (Request $request) {
$polar = new Polar(config('polar.access_token'));
$session = $polar->checkouts->create([
'product_price_id' => $request->input('product_price_id'),
'success_url' => route('checkout.success'),
'external_customer_id' => auth()->id(),
]);
return redirect($session['url']);
});