Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/polarsource/polar/llms.txt

Use this file to discover all available pages before exploring further.

This guide shows you how to implement subscription upgrades, handle prorations, and manage the billing implications of plan changes.

Overview

When customers upgrade their subscription:
  1. They switch to a higher-tier plan
  2. Prorations credit unused time on the old plan
  3. New charges apply for the remaining period
  4. Next billing continues on the new plan

Basic Upgrade Flow

1

List available upgrade options

Display higher-tier products to the customer:
const products = await polar.products.list({
  organizationId: 'org_...',
})

// Filter to show only upgrades
const upgrades = products.items.filter(product => {
  const currentAmount = currentSubscription.amount
  const upgradeAmount = product.prices[0].amount
  return upgradeAmount > currentAmount
})
2

Update subscription

Call the subscription update endpoint:
const updatedSubscription = await polar.subscriptions.update(
  subscriptionId,
  {
    productId: 'prod_premium_...',
  }
)
3

Handle immediate charge

The customer is charged the prorated difference:
console.log('Upgraded successfully!')
console.log('New amount:', updatedSubscription.amount)
console.log('Next billing:', updatedSubscription.currentPeriodEnd)

Proration Explained

When upgrading mid-cycle, Polar automatically calculates proration: Example:
  • Current plan: $29/month
  • New plan: $99/month
  • 15 days remaining in cycle
  • Days in month: 30
Calculation:
Unused credit: $29 × (15/30) = $14.50
New plan cost: $99 × (15/30) = $49.50
Charge today: $49.50 - $14.50 = $35.00
The customer pays 35now,then35 now, then 99/month going forward.

Implementation Examples

Next.js App Router

Create a server action for upgrades:
// app/actions/subscription.ts
'use server'

import { polar } from '@/lib/polar'
import { revalidatePath } from 'next/cache'

export async function upgradeSubscription(
  subscriptionId: string,
  newProductId: string
) {
  try {
    const subscription = await polar.subscriptions.update(
      subscriptionId,
      { productId: newProductId }
    )
    
    revalidatePath('/dashboard/subscriptions')
    return { success: true, subscription }
  } catch (error) {
    return { 
      success: false, 
      error: error.message 
    }
  }
}
Use in a component:
// app/dashboard/subscriptions/[id]/upgrade/page.tsx
'use client'

import { upgradeSubscription } from '@/app/actions/subscription'
import { useState } from 'react'

interface UpgradePageProps {
  params: { id: string }
  currentSubscription: Subscription
  availableProducts: Product[]
}

export default function UpgradePage({ 
  params, 
  currentSubscription,
  availableProducts 
}: UpgradePageProps) {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function handleUpgrade(productId: string) {
    if (!confirm('Upgrade your subscription?')) return
    
    setLoading(true)
    setError(null)
    
    const result = await upgradeSubscription(params.id, productId)
    
    if (result.success) {
      alert('Subscription upgraded successfully!')
    } else {
      setError(result.error)
    }
    
    setLoading(false)
  }

  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">Upgrade Subscription</h1>
      
      {error && (
        <div className="bg-red-50 text-red-600 p-4 rounded-lg">
          {error}
        </div>
      )}

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {availableProducts.map((product) => {
          const price = product.prices[0]
          const isUpgrade = price.amount > currentSubscription.amount
          
          if (!isUpgrade) return null

          return (
            <div key={product.id} className="border rounded-lg p-6">
              <h2 className="text-2xl font-bold">{product.name}</h2>
              <p className="text-gray-600 my-4">{product.description}</p>
              
              <div className="text-3xl font-bold mb-4">
                ${price.amount / 100}
                <span className="text-sm text-gray-500">/{price.recurringInterval}</span>
              </div>

              <ul className="space-y-2 mb-6">
                {product.benefits.map((benefit) => (
                  <li key={benefit.id} className="flex items-start">
                    <svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
                      <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
                    </svg>
                    {benefit.description}
                  </li>
                ))}
              </ul>

              <button
                onClick={() => handleUpgrade(product.id)}
                disabled={loading}
                className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
              >
                {loading ? 'Upgrading...' : 'Upgrade Now'}
              </button>
            </div>
          )
        })}
      </div>
    </div>
  )
}

Laravel

Create a controller for upgrades:
<?php

namespace App\Http\Controllers;

use App\Models\PolarSubscription;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Polar\Polar;

class SubscriptionUpgradeController extends Controller
{
    public function __construct(
        private Polar $polar
    ) {}

    public function index(PolarSubscription $subscription)
    {
        // Get available upgrade products
        $products = $this->polar->products->list(
            organizationId: config('services.polar.organization_id')
        );

        // Filter to show only upgrades
        $upgrades = array_filter($products->items, function($product) use ($subscription) {
            $upgradeAmount = $product->prices[0]->amount;
            return $upgradeAmount > $subscription->amount;
        });

        return view('subscriptions.upgrade', [
            'subscription' => $subscription,
            'products' => $upgrades,
        ]);
    }

    public function upgrade(
        Request $request, 
        PolarSubscription $subscription
    ): RedirectResponse {
        $request->validate([
            'product_id' => 'required|string',
        ]);

        try {
            $updated = $this->polar->subscriptions->update(
                $subscription->polar_subscription_id,
                productId: $request->product_id
            );

            // Update local database
            $subscription->update([
                'polar_product_id' => $updated->productId,
                'amount' => $updated->amount,
                'status' => $updated->status,
            ]);

            return redirect()->route('subscriptions.show', $subscription)
                ->with('success', 'Subscription upgraded successfully!');
        } catch (\Exception $e) {
            return back()->with('error', 'Failed to upgrade: ' . $e->getMessage());
        }
    }
}
Blade template:
<!-- resources/views/subscriptions/upgrade.blade.php -->
<x-app-layout>
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <h1 class="text-3xl font-bold mb-8">Upgrade Your Subscription</h1>

            @if(session('error'))
                <div class="bg-red-50 text-red-600 p-4 rounded-lg mb-4">
                    {{ session('error') }}
                </div>
            @endif

            <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
                @foreach($products as $product)
                    <div class="bg-white shadow-sm rounded-lg p-6">
                        <h2 class="text-2xl font-semibold mb-2">{{ $product->name }}</h2>
                        <p class="text-gray-600 mb-4">{{ $product->description }}</p>

                        <div class="text-3xl font-bold mb-6">
                            ${{ $product->prices[0]->amount / 100 }}
                            <span class="text-sm text-gray-500">/ {{ $product->prices[0]->recurringInterval }}</span>
                        </div>

                        <form method="POST" action="{{ route('subscriptions.upgrade', $subscription) }}">
                            @csrf
                            <input type="hidden" name="product_id" value="{{ $product->id }}">
                            <button 
                                type="submit"
                                onclick="return confirm('Upgrade to {{ $product->name }}?')"
                                class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700"
                            >
                                Upgrade Now
                            </button>
                        </form>
                    </div>
                @endforeach
            </div>
        </div>
    </div>
</x-app-layout>

Handling Upgrade Failures

Handle common upgrade errors:
try {
  const updated = await polar.subscriptions.update(subscriptionId, {
    productId: newProductId,
  })
} catch (error) {
  if (error.statusCode === 404) {
    // Subscription or product not found
    alert('Invalid subscription or product')
  } else if (error.statusCode === 403) {
    // Cannot upgrade (e.g., subscription canceled)
    alert('This subscription cannot be upgraded')
  } else if (error.statusCode === 402) {
    // Payment method failed
    alert('Payment failed. Please update your payment method.')
  } else if (error.statusCode === 409) {
    // Subscription is locked (another update in progress)
    alert('Please wait for the current update to complete')
  } else {
    alert('Upgrade failed. Please try again.')
  }
}

Webhook Handling

Listen for subscription updates:
app.post('/webhooks/polar', (req, res) => {
  const event = polar.webhooks.verifyEvent(...)
  
  if (event.type === 'subscription.updated') {
    const subscription = event.data
    
    // Update your database
    await updateSubscription({
      id: subscription.id,
      productId: subscription.productId,
      amount: subscription.amount,
      status: subscription.status,
    })
    
    // Send confirmation email
    await sendEmail(subscription.customer.email, {
      subject: 'Subscription Upgraded',
      template: 'subscription-upgraded',
      data: { subscription },
    })
  }
  
  res.json({ received: true })
})

Proration Behavior Options

Control how prorations are handled:
const subscription = await polar.subscriptions.update(
  subscriptionId,
  {
    productId: newProductId,
    prorationBehavior: 'create_prorations', // default
  }
)
Options:
  • create_prorations (default): Credits old plan, charges new plan
  • always_invoice: Immediately invoices the difference

Testing Upgrades

Test the upgrade flow:
1

Create test subscription

# Use test API key
POLAR_API_KEY=polar_sk_test_...
2

Upgrade to higher tier

Test with different pricing tiers to see proration.
3

Check Stripe dashboard

View the proration line items in test mode.
4

Verify webhooks

Confirm subscription.updated webhook is received.

Best Practices

  • Show proration preview before confirming
  • Clearly communicate billing changes
  • Send confirmation emails
  • Update UI immediately after upgrade
  • Handle payment failures gracefully
  • Provide retry options
  • Log all upgrade attempts
  • Alert on repeated failures
  • Validate product eligibility
  • Check for active subscriptions
  • Prevent downgrades via this flow
  • Handle trial to paid upgrades

Subscription Downgrades

Handle plan downgrades and cancellations

Seat-Based Pricing

Manage team subscription upgrades

Build docs developers (and LLMs) love