Files
english/.opencode/skills/payment-integration/references/polar/subscriptions.md
2026-04-12 01:06:31 +07:00

341 lines
7.4 KiB
Markdown

# Polar Subscriptions
Subscription lifecycle, upgrades, downgrades, and trial management.
## Lifecycle States
- `created` - New subscription, payment pending
- `active` - Payment successful, benefits granted
- `canceled` - Scheduled cancellation at period end
- `revoked` - Billing stopped, benefits revoked immediately
- `past_due` - Payment failed, in dunning period
## API Operations
### List Subscriptions
```typescript
const subscriptions = await polar.subscriptions.list({
organization_id: "org_xxx",
product_id: "prod_xxx",
customer_id: "cust_xxx",
status: "active"
});
```
### Get Subscription
```typescript
const subscription = await polar.subscriptions.get(subscriptionId);
```
### Update Subscription
```typescript
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
```typescript
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:**
```typescript
// 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:**
```typescript
const product = await polar.products.create({
name: "Pro Plan",
prices: [{
trial_period_days: 14
}]
});
```
**Checkout-level:**
```typescript
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
```typescript
// Listen to webhooks
subscription.created // Trial starts
subscription.active // Trial ends, first charge
subscription.canceled // Trial canceled
```
## Cancellations
### Cancel at Period End
```typescript
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
```typescript
// 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
```typescript
await polar.subscriptions.update(subscriptionId, {
cancel_at_period_end: false
});
// Removes cancellation
// Subscription continues normally
```
## Renewals
### Listening to Renewals
```typescript
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_due` webhook fired
- Dunning process initiated
- Customer notified via email
- Multiple retry attempts
- Eventually revoked if payment fails
## Discounts
### Apply Discount
```typescript
await polar.subscriptions.update(subscriptionId, {
discount_id: "discount_xxx"
});
```
### Remove Discount
```typescript
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
```typescript
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
```typescript
// 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
```typescript
await polar.subscriptions.update(subscriptionId, {
metadata: {
internal_id: "sub_123",
tier: "pro",
source: "web"
}
});
```
### Query by Metadata
```typescript
const subscriptions = await polar.subscriptions.list({
organization_id: "org_xxx",
metadata: { tier: "pro" }
});
```
## Best Practices
1. **Lifecycle Management:**
- Listen to all subscription webhooks
- Handle each state appropriately
- Sync state to your database
- Grant/revoke access based on state
2. **Upgrades/Downgrades:**
- Use proration for fair billing
- Communicate changes clearly
- Preview invoice before change
- Allow customer self-service
3. **Trials:**
- Set appropriate trial duration
- Notify before trial ends
- Easy cancellation during trial
- Clear trial end date in UI
4. **Cancellations:**
- Make cancellation easy
- Offer alternatives (pause, downgrade)
- Collect feedback
- Keep benefits until period end
- Send confirmation email
5. **Failed Payments:**
- Handle `past_due` webhook
- Notify customer promptly
- Provide retry mechanism
- Grace period before revocation
- Clear reactivation path
6. **Customer Communication:**
- Renewal reminders
- Payment confirmations
- Failed payment notifications
- Upgrade/downgrade confirmations
- Cancellation confirmations
7. **Analytics:**
- Track churn reasons
- Monitor upgrade/downgrade patterns
- Analyze trial conversion
- Measure payment failure rates
- Lifetime value calculations
## Common Patterns
### Subscription Status Check
```typescript
async function hasActiveSubscription(userId) {
const subscriptions = await polar.subscriptions.list({
external_customer_id: userId,
status: "active"
});
return subscriptions.items.length > 0;
}
```
### Grace Period Handler
```typescript
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
```typescript
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"
});
}
}
```