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 seat-based pricing for team subscriptions, allowing customers to add and remove team members with automatic billing adjustments.
Overview
Seat-based pricing:
Charges per user/seat (e.g., $10/seat/month)
Customers can add/remove seats dynamically
Billing adjusts automatically with prorations
Supports minimum and maximum seat limits
Creating Seat-Based Products
First, create a product with seat-based pricing in your Polar dashboard:
Go to Products > Create Product
Set billing type to “Seat-based”
Configure:
Price per seat
Minimum seats (e.g., 1)
Maximum seats (e.g., 100)
Billing interval (month/year)
Checkout with Seats
Create a checkout allowing customers to select seat count:
const checkout = await polar . checkouts . create ({
productPriceId: 'price_seat_based_...' ,
seats: 5 , // Customer purchases 5 seats
successUrl: 'https://yoursite.com/success' ,
metadata: {
organizationName: 'Acme Corp' ,
},
})
// Initial cost: 5 seats × $10/seat = $50/month
Dynamic Seat Selection
Let customers choose seat count before checkout:
'use client'
import { createCheckout } from '@/app/actions/checkout'
import { useState } from 'react'
interface SeatSelectorProps {
productPriceId : string
pricePerSeat : number
minSeats : number
maxSeats : number
}
export function SeatSelector ({
productPriceId ,
pricePerSeat ,
minSeats ,
maxSeats
} : SeatSelectorProps ) {
const [ seats , setSeats ] = useState ( minSeats )
const [ loading , setLoading ] = useState ( false )
const totalPrice = seats * pricePerSeat
async function handleCheckout () {
setLoading ( true )
await createCheckout ( productPriceId , seats )
}
return (
< div className = "border rounded-lg p-6" >
< h3 className = "text-xl font-bold mb-4" > Team Plan </ h3 >
< div className = "mb-6" >
< label className = "block text-sm font-medium mb-2" >
Number of seats
</ label >
< div className = "flex items-center gap-4" >
< button
onClick = {() => setSeats (Math.max( minSeats , seats - 1))}
className = "w-10 h-10 border rounded-lg"
disabled = {seats <= minSeats }
>
-
</ button >
< input
type = "number"
value = { seats }
onChange = {(e) => {
const value = parseInt ( e . target . value )
if ( value >= minSeats && value <= maxSeats ) {
setSeats ( value )
}
}}
className = "w-20 text-center border rounded-lg py-2"
min = { minSeats }
max = { maxSeats }
/>
< button
onClick = {() => setSeats (Math.min( maxSeats , seats + 1))}
className = "w-10 h-10 border rounded-lg"
disabled = {seats >= maxSeats }
>
+
</ button >
</ div >
< p className = "text-sm text-gray-500 mt-2" >
{ minSeats } - { maxSeats } seats available
</ p >
</ div >
< div className = "bg-gray-50 p-4 rounded-lg mb-4" >
< div className = "flex justify-between mb-2" >
< span >{ seats } × $ {pricePerSeat/ 100 } / seat </ span >
< span className = "font-bold" > $ {totalPrice/ 100 } / month </ span >
</ div >
</ div >
< button
onClick = { handleCheckout }
disabled = { loading }
className = "w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700"
>
{ loading ? 'Loading...' : 'Subscribe' }
</ button >
</ div >
)
}
Server action:
// app/actions/checkout.ts
'use server'
import { polar } from '@/lib/polar'
import { redirect } from 'next/navigation'
export async function createCheckout (
productPriceId : string ,
seats : number
) {
const checkout = await polar . checkouts . create ({
productPriceId ,
seats ,
successUrl: ` ${ process . env . NEXT_PUBLIC_URL } /success?checkout_id={CHECKOUT_ID}` ,
})
redirect ( checkout . url )
}
Managing Seats
Update Seat Count
Customers can increase or decrease seats:
const subscription = await polar . subscriptions . update (
subscriptionId ,
{
seats: 10 , // Update from 5 to 10 seats
prorationBehavior: 'create_prorations' ,
}
)
// Prorated charge: 5 additional seats for remainder of period
Customer Portal Integration
Let customers manage seats themselves:
'use client'
import { useState } from 'react'
interface SeatManagerProps {
subscription : Subscription
customerToken : string
}
export function SeatManager ({ subscription , customerToken } : SeatManagerProps ) {
const [ seats , setSeats ] = useState ( subscription . seats )
const [ loading , setLoading ] = useState ( false )
async function updateSeats ( newSeats : number ) {
setLoading ( true )
try {
const response = await fetch (
`https://api.polar.sh/v1/customer-portal/subscriptions/ ${ subscription . id } ` ,
{
method: 'PATCH' ,
headers: {
'Authorization' : `Bearer ${ customerToken } ` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({ seats: newSeats }),
}
)
if ( response . ok ) {
setSeats ( newSeats )
alert ( 'Seats updated successfully!' )
} else {
const error = await response . json ()
alert ( error . detail || 'Failed to update seats' )
}
} catch ( error ) {
alert ( 'Failed to update seats' )
} finally {
setLoading ( false )
}
}
return (
< div className = "border rounded-lg p-6" >
< h3 className = "text-lg font-semibold mb-4" > Manage Seats </ h3 >
< div className = "mb-4" >
< p className = "text-sm text-gray-600 mb-2" >
Current : { seats } seats
</ p >
< p className = "text-sm text-gray-600" >
$ {subscription.amount / 100 } / month
</ p >
</ div >
< div className = "flex items-center gap-4 mb-4" >
< button
onClick = {() => updateSeats ( seats - 1)}
disabled = {loading || seats <= 1 }
className = "px-4 py-2 border rounded-lg disabled:opacity-50"
>
Remove Seat
</ button >
< button
onClick = {() => updateSeats ( seats + 1)}
disabled = { loading }
className = "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Add Seat
</ button >
</ div >
< p className = "text-xs text-gray-500" >
Billing adjusts automatically with prorations
</ p >
</ div >
)
}
Seat Assignment
Assign seats to specific team members:
List Seats
const seats = await fetch (
`https://api.polar.sh/v1/customer-portal/seats?subscription_id= ${ subscriptionId } ` ,
{
headers: {
'Authorization' : `Bearer ${ customerToken } ` ,
},
}
). then ( r => r . json ())
console . log ( ` ${ seats . seats . length } of ${ seats . total_seats } seats assigned` )
console . log ( ` ${ seats . available_seats } seats available` )
Assign Seat
const seat = await fetch (
'https://api.polar.sh/v1/customer-portal/seats' ,
{
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ customerToken } ` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
subscription_id: subscriptionId ,
email: 'member@example.com' ,
metadata: {
role: 'developer' ,
department: 'engineering' ,
},
}),
}
). then ( r => r . json ())
// Invitation email sent to member@example.com
Revoke Seat
await fetch (
`https://api.polar.sh/v1/customer-portal/seats/ ${ seatId } ` ,
{
method: 'DELETE' ,
headers: {
'Authorization' : `Bearer ${ customerToken } ` ,
},
}
)
Seat Management UI
Complete seat management interface:
'use client'
import { useEffect , useState } from 'react'
interface SeatManagementProps {
subscriptionId : string
customerToken : string
}
export function SeatManagement ({
subscriptionId ,
customerToken
} : SeatManagementProps ) {
const [ seats , setSeats ] = useState < any >( null )
const [ loading , setLoading ] = useState ( true )
const [ email , setEmail ] = useState ( '' )
useEffect (() => {
loadSeats ()
}, [])
async function loadSeats () {
const response = await fetch (
`https://api.polar.sh/v1/customer-portal/seats?subscription_id= ${ subscriptionId } ` ,
{
headers: { 'Authorization' : `Bearer ${ customerToken } ` },
}
)
const data = await response . json ()
setSeats ( data )
setLoading ( false )
}
async function assignSeat () {
try {
await fetch (
'https://api.polar.sh/v1/customer-portal/seats' ,
{
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ customerToken } ` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
subscription_id: subscriptionId ,
email ,
}),
}
)
setEmail ( '' )
await loadSeats ()
alert ( 'Seat assigned successfully!' )
} catch ( error ) {
alert ( 'Failed to assign seat' )
}
}
async function revokeSeat ( seatId : string ) {
if ( ! confirm ( 'Remove this seat?' )) return
try {
await fetch (
`https://api.polar.sh/v1/customer-portal/seats/ ${ seatId } ` ,
{
method: 'DELETE' ,
headers: { 'Authorization' : `Bearer ${ customerToken } ` },
}
)
await loadSeats ()
alert ( 'Seat revoked' )
} catch ( error ) {
alert ( 'Failed to revoke seat' )
}
}
if ( loading ) return < div > Loading ...</ div >
return (
< div className = "space-y-6" >
< div className = "bg-gray-50 p-4 rounded-lg" >
< p className = "text-sm text-gray-600" >
{ seats . available_seats } of { seats . total_seats } seats available
</ p >
</ div >
{ seats . available_seats > 0 && (
< div className = "border rounded-lg p-4" >
< h3 className = "font-semibold mb-4" > Assign Seat </ h3 >
< div className = "flex gap-2" >
< input
type = "email"
value = { email }
onChange = {(e) => setEmail (e.target.value)}
placeholder = "member@example.com"
className = "flex-1 border rounded-lg px-4 py-2"
/>
< button
onClick = { assignSeat }
disabled = {! email }
className = "bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
Assign
</ button >
</ div >
</ div >
)}
< div >
< h3 className = "font-semibold mb-4" > Team Members </ h3 >
< div className = "space-y-2" >
{ seats . seats . map (( seat : any ) => (
< div key = {seat. id } className = "flex items-center justify-between border rounded-lg p-4" >
< div >
< p className = "font-medium" > {seat. email } </ p >
< p className = "text-sm text-gray-500" >
Status : { seat . status }
</ p >
</ div >
< button
onClick = {() => revokeSeat (seat.id)}
className = "text-red-600 hover:text-red-700"
>
Remove
</ button >
</ div >
))}
{ seats . seats . length === 0 && (
< p className = "text-gray-500 text-center py-8" >
No seats assigned yet
</ p >
)}
</ div >
</ div >
</ div >
)
}
Proration on Seat Changes
When seats are added/removed mid-cycle:
Adding Seats:
// Current: 5 seats at $10/seat = $50/month
// Add 3 seats mid-cycle (15 days remaining)
// Charge: 3 seats × $10 × (15/30) = $15
// Next month: 8 seats × $10 = $80
Removing Seats:
// Current: 5 seats at $10/seat = $50/month
// Remove 2 seats mid-cycle (15 days remaining)
// Credit: 2 seats × $10 × (15/30) = $10
// Applied to next invoice
Seat Limits
Enforce minimum and maximum seats:
try {
await polar . subscriptions . update ( subscriptionId , {
seats: 2 , // Below minimum of 3
})
} catch ( error ) {
// Error: Minimum 3 seats required
}
try {
await polar . subscriptions . update ( subscriptionId , {
seats: 150 , // Above maximum of 100
})
} catch ( error ) {
// Error: Maximum 100 seats allowed
}
Preventing Downgrade
Prevent reducing seats below assigned count:
try {
// 5 seats assigned, trying to reduce to 3
await polar . subscriptions . update ( subscriptionId , {
seats: 3 ,
})
} catch ( error ) {
// Error: Cannot decrease seats to 3. Currently 5 seats are assigned.
// Revoke seats first.
}
Webhooks
Handle seat-related events:
app . post ( '/webhooks/polar' , ( req , res ) => {
const event = polar . webhooks . verifyEvent ( ... )
switch ( event . type ) {
case 'subscription.updated' :
if ( event . data . seats !== event . data . previousSeats ) {
// Seats changed
console . log ( `Seats: ${ event . data . previousSeats } → ${ event . data . seats } ` )
console . log ( `Amount: ${ event . data . amount } ` )
}
break
case 'customer_seat.assigned' :
// New seat assigned
await provisionAccess ( event . data . email , event . data . subscriptionId )
break
case 'customer_seat.revoked' :
// Seat removed
await revokeAccess ( event . data . email , event . data . subscriptionId )
break
}
res . json ({ received: true })
})
Best Practices
Show clear pricing breakdown
Allow easy seat adjustments
Send member invitation emails
Display current seat usage
Warn before removing assigned seats
Set appropriate min/max limits
Enforce seat assignments
Track seat utilization
Monitor unused seats
Offer volume discounts
Handle proration correctly
Use webhooks for access control
Validate seat limits
Log all seat changes
Test edge cases
Testing
Create seat-based product
Set up product with seat pricing in test mode
Test checkout
Create checkout with various seat counts
Test seat management
Add/remove seats and verify prorations
Test seat assignment
Assign and revoke seats
Verify webhooks
Check all seat-related webhook events
Next Steps
Subscription Upgrades Handle seat plan upgrades
Customer Portal Build complete customer portal