This guide explains how to handle subscription plan changes (upgrades/downgrades) in your application.
Customer Portal Method
The simplest way to handle plan changes is through Stripe's Customer Portal:
await iaptic.redirectToCustomerPortal({
id: 'sub_xyz',
accessKey: 'ak_123',
returnUrl: 'https://example.com/account'
});
This provides a full-featured portal where customers can:
- Change plans
- Update payment methods
- View invoices
- Manage billing
Direct Plan Change
For more control, use the direct plan change endpoint:
const response = await iaptic.changePlan({
id: 'sub_xyz',
accessKey: 'ak_123',
offerId: 'stripe:prod_xyz#price_xyz',
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel'
});
// Handle the updated subscription
console.log('New plan:', response.purchase);
Custom UI Implementation
Create your own plan selection interface:
async function showPlanSelector() {
try {
// 1. Fetch available plans
const { products } = await iaptic.getProducts();
// 2. Get current subscription
const { purchases } = await iaptic.getPurchases();
const currentPlan = purchases[0];
// 3. Filter available upgrades/downgrades
const availablePlans = products.filter(p =>
p.type === 'paid subscription' &&
p.metadata?.can_purchase === 'true' &&
p.id !== currentPlan.productId
);
// 4. Show UI
renderPlanOptions(availablePlans, currentPlan);
} catch (error) {
console.error('Failed to load plans:', error);
}
}
async function changePlan(newOfferId) {
try {
const { purchases, new_access_keys } = await iaptic.changePlan({
offerId: newOfferId
});
// Store new access key if provided
if (new_access_keys) {
for (const [subId, key] of Object.entries(new_access_keys)) {
localStorage.setItem(`stripe_access_key_${subId}`, key);
}
}
// Update UI with new plan
updateSubscriptionDisplay(purchases[0]);
} catch (error) {
console.error('Plan change failed:', error);
}
}
Handle Plan Change Events
Monitor plan changes through webhooks:
if (event.type === 'customer.subscription.updated') {
const subscription = event.data.object;
const previousAttributes = event.data.previous_attributes;
if (previousAttributes.items) {
const oldPriceId = previousAttributes.items.data[0].price.id;
const newPriceId = subscription.items.data[0].price.id;
// Handle plan change
onPlanChanged(subscription.id, oldPriceId, newPriceId);
}
}
Best Practices
-
Proration Handling
- Clearly show proration amounts
- Explain billing changes
- Handle credit application
-
UI/UX
- Show plan comparison
- Highlight differences
- Confirm changes
- Show effective date
-
Error Handling
- Payment failures
- Invalid plans
- Network issues
- Access key rotation
-
Feature Management
- Update feature access
- Handle downgrades
- Manage quotas
Common Issues
Plan Change Failed
- Check subscription status
- Verify price is active
- Validate access key
- Check payment method
Features Not Updated
- Clear feature cache
- Refresh subscription status
- Check webhook delivery
- Verify metadata
Billing Issues
- Check proration settings
- Verify price configuration
- Check customer balance
- Review invoice items
Example: Complete Plan Change Flow
class PlanManager {
constructor(iaptic) {
this.iaptic = iaptic;
}
async showPlanSelector() {
const { products } = await this.iaptic.getProducts();
const { purchases } = await this.iaptic.getPurchases();
return {
currentPlan: purchases[0],
availablePlans: products.filter(p =>
p.type === 'paid subscription' &&
p.metadata?.can_purchase === 'true'
),
pricing: products.map(p => ({
id: p.id,
price: this.iaptic.formatCurrency(
p.offers[0].pricingPhases[0].priceMicros,
p.offers[0].pricingPhases[0].currency
),
interval: this.iaptic.formatBillingPeriodEN(
p.offers[0].pricingPhases[0].billingPeriod
)
}))
};
}
async changePlan(newOfferId) {
try {
// 1. Change plan
const { purchase, new_access_keys } = await this.iaptic.changePlan({
offerId: newOfferId
});
// 2. Update access keys
if (new_access_keys) {
for (const [subId, key] of Object.entries(new_access_keys)) {
localStorage.setItem(`stripe_access_key_${subId}`, key);
}
}
// 3. Update features
await this.updateFeatures(purchase);
return purchase;
} catch (error) {
console.error('Plan change failed:', error);
throw error;
}
}
async updateFeatures(purchase) {
// Implement your feature update logic
}
}