7.4 KiB
7.4 KiB
Polar Subscriptions
Subscription lifecycle, upgrades, downgrades, and trial management.
Lifecycle States
created- New subscription, payment pendingactive- Payment successful, benefits grantedcanceled- Scheduled cancellation at period endrevoked- Billing stopped, benefits revoked immediatelypast_due- Payment failed, in dunning period
API Operations
List Subscriptions
const subscriptions = await polar.subscriptions.list({
organization_id: "org_xxx",
product_id: "prod_xxx",
customer_id: "cust_xxx",
status: "active"
});
Get Subscription
const subscription = await polar.subscriptions.get(subscriptionId);
Update Subscription
const updated = await polar.subscriptions.update(subscriptionId, {
product_price_id: "newPriceId",
discount_id: "discount_xxx",
metadata: { plan: "pro" }
});
Upgrades & Downgrades
Proration Options
Next Invoice (default):
- Credit/charge applied to upcoming invoice
- Subscription updates immediately
- Customer billed at next cycle
Invoice Immediately:
- Credit/charge processed right away
- Subscription updates immediately
- New invoice generated
await polar.subscriptions.update(subscriptionId, {
product_price_id: "higher_tier_price",
proration: "invoice_immediately" // or "next_invoice"
});
Customer-Initiated Changes
Enable in Product Settings:
- Toggle "Allow price change"
- Customer can upgrade/downgrade via portal
- Admin-only changes if disabled
Implementation:
// Check if changes allowed
const product = await polar.products.get(productId);
if (product.allow_price_change) {
// Customer can change via portal
}
Trials
Configuration
Product-level:
const product = await polar.products.create({
name: "Pro Plan",
prices: [{
trial_period_days: 14
}]
});
Checkout-level:
const session = await polar.checkouts.create({
product_price_id: "price_xxx",
trial_period_days: 7 // Overrides product setting
});
Trial Behavior
- Customer not charged during trial
- Benefits granted immediately
- Can cancel anytime during trial
- Charged at trial end if not canceled
Trial Events
// Listen to webhooks
subscription.created // Trial starts
subscription.active // Trial ends, first charge
subscription.canceled // Trial canceled
Cancellations
Cancel at Period End
await polar.subscriptions.update(subscriptionId, {
cancel_at_period_end: true
});
// Subscription remains active
// Benefits continue until period end
// Webhooks: subscription.updated, subscription.canceled
Immediate Revocation
// Happens automatically at period end
// Or manually via API (future feature)
// Status changes to "revoked"
// Billing stops, benefits revoked
// Webhooks: subscription.updated, subscription.revoked
Reactivate Canceled
await polar.subscriptions.update(subscriptionId, {
cancel_at_period_end: false
});
// Removes cancellation
// Subscription continues normally
Renewals
Listening to Renewals
app.post('/webhook/polar', async (req, res) => {
const event = validateEvent(req.body, req.headers, secret);
if (event.type === 'order.created') {
const order = event.data;
if (order.billing_reason === 'subscription_cycle') {
// This is a renewal
await handleRenewal(order.subscription_id);
}
}
res.json({ received: true });
});
Failed Renewals
subscription.past_duewebhook fired- Dunning process initiated
- Customer notified via email
- Multiple retry attempts
- Eventually revoked if payment fails
Discounts
Apply Discount
await polar.subscriptions.update(subscriptionId, {
discount_id: "discount_xxx"
});
Remove Discount
await polar.subscriptions.update(subscriptionId, {
discount_id: null
});
Discount Types
- Percentage off: 20% off
- Fixed amount: $5 off
- Duration: once, forever, repeating
Customer Portal
Generate Portal Access
const session = await polar.customerSessions.create({
customer_id: "cust_xxx"
});
// Redirect to: session.url
Portal Features
- View subscriptions
- Upgrade/downgrade plans
- Cancel subscriptions
- Update billing info
- View invoices
- Access benefits
Pre-authenticated Links
// From your app, create session and redirect
app.get('/portal', async (req, res) => {
const session = await polar.customerSessions.create({
external_customer_id: req.user.id
});
res.redirect(session.url);
});
Metadata
Update Subscription Metadata
await polar.subscriptions.update(subscriptionId, {
metadata: {
internal_id: "sub_123",
tier: "pro",
source: "web"
}
});
Query by Metadata
const subscriptions = await polar.subscriptions.list({
organization_id: "org_xxx",
metadata: { tier: "pro" }
});
Best Practices
-
Lifecycle Management:
- Listen to all subscription webhooks
- Handle each state appropriately
- Sync state to your database
- Grant/revoke access based on state
-
Upgrades/Downgrades:
- Use proration for fair billing
- Communicate changes clearly
- Preview invoice before change
- Allow customer self-service
-
Trials:
- Set appropriate trial duration
- Notify before trial ends
- Easy cancellation during trial
- Clear trial end date in UI
-
Cancellations:
- Make cancellation easy
- Offer alternatives (pause, downgrade)
- Collect feedback
- Keep benefits until period end
- Send confirmation email
-
Failed Payments:
- Handle
past_duewebhook - Notify customer promptly
- Provide retry mechanism
- Grace period before revocation
- Clear reactivation path
- Handle
-
Customer Communication:
- Renewal reminders
- Payment confirmations
- Failed payment notifications
- Upgrade/downgrade confirmations
- Cancellation confirmations
-
Analytics:
- Track churn reasons
- Monitor upgrade/downgrade patterns
- Analyze trial conversion
- Measure payment failure rates
- Lifetime value calculations
Common Patterns
Subscription Status Check
async function hasActiveSubscription(userId) {
const subscriptions = await polar.subscriptions.list({
external_customer_id: userId,
status: "active"
});
return subscriptions.items.length > 0;
}
Grace Period Handler
app.post('/webhook/polar', async (req, res) => {
const event = validateEvent(req.body, req.headers, secret);
if (event.type === 'subscription.past_due') {
const subscription = event.data;
// Grant 3-day grace period
await grantGracePeriod(subscription.customer_id, 3);
// Notify customer
await sendPaymentFailedEmail(subscription.customer_id);
}
res.json({ received: true });
});
Upgrade Path
async function upgradeSubscription(subscriptionId, newPriceId) {
// Preview invoice
const preview = await polar.subscriptions.previewUpdate(subscriptionId, {
product_price_id: newPriceId,
proration: "invoice_immediately"
});
// Show customer preview
if (await confirmUpgrade(preview)) {
await polar.subscriptions.update(subscriptionId, {
product_price_id: newPriceId,
proration: "invoice_immediately"
});
}
}