Advanced Pricing

Multi-line item checkout, tiered pricing, and optional add-ons for complex billing scenarios

Advanced pricing in Makerkit lets you create subscriptions with multiple price components: a base fee, per-seat charges, and metered usage like AI tokens. Use lineItems instead of planId when you need this flexibility. Stripe supports multi-line item checkout; Polar requires separate products per tier.

This guide is part of the Billing & Subscriptions documentation. Introduced in version 1.1.0.

Advanced Billing Pricing Table

When to Use Advanced Pricing

The billing configuration supports two plan structures:

StructureUse CaseKey Field
SimplePlanSingle price per planplanId
AdvancedPlanMultiple prices, complex scenarioslineItems

Decision Tree

Use SimplePlan (planId) when:

  • You have a single recurring price per plan
  • Pricing is straightforward (e.g., $19/month for Pro)
  • You don't need usage-based components

Use AdvancedPlan (lineItems) when:

  • You need base fee + per-seat pricing
  • You want to include metered usage (AI tokens, API calls)
  • You need tiered pricing for any component
  • You want customers to select optional add-ons at checkout

Multi-Line Item Checkout (Stripe Only)

Multi-line item checkout creates a subscription with multiple prices. Each line item becomes a separate line on the invoice.

Line Item Structure

interface LineItem {
id: string; // Unique identifier
name: string; // Display name
type: 'flat' | 'usage'; // Pricing type
priceId?: string; // Stripe price ID
productId?: string; // Polar product ID (not for multi-item)
unit?: string; // Unit label (e.g., "seat", "token")
cost?: number; // Base cost for display
tiers?: PricingTier[]; // Tiered pricing
quantity?: number; // Initial quantity (default: 1)
optional?: boolean; // Customer can toggle
adjustableQuantity?: { // Customer can change quantity
enabled: boolean;
minimum?: number;
maximum?: number;
};
billingUsageType?: 'licensed' | 'metered';
}

Line Item Types

TypeDescriptionUse Case
flatFixed recurring costBase subscription fee
usageQuantity-based costPer-seat, per-unit billing

Billing Usage Types

The billingUsageType field determines how quantities are handled:

TypeQuantityBillingUse Case
licensedSet at checkoutPre-determinedPer-seat billing
meteredTracked by StripePost-usageAI tokens, API calls

Important: Metered prices cannot have adjustableQuantity or be optional - Stripe tracks usage automatically.

Primary Price ID

When using lineItems, set primaryPriceId to identify the plan in webhooks:

{
name: 'pro-monthly',
displayName: 'Pro Monthly',
interval: 'month',
primaryPriceId: process.env.STRIPE_PRICE_PRO_BASE!,
lineItems: [
{
id: 'base',
name: 'Pro Subscription',
type: 'flat',
priceId: process.env.STRIPE_PRICE_PRO_BASE!,
cost: 29,
},
// ... additional line items
],
}

If primaryPriceId is not set, the system uses the first line item with a priceId as the primary. Explicitly setting it avoids order-dependent matching issues.

Tiered Pricing

Tiered pricing charges different rates based on quantity ranges. Common for per-seat or usage-based billing.

Tier Structure

interface PricingTier {
cost: number; // Unit price in this tier
upTo: number | 'infinite' | undefined; // Upper bound
}

upTo Values

ValueMeaning
numberUp to this quantity (inclusive)
'infinite'No upper limit
undefinedSame as 'infinite'

Example: Per-Seat Tiered Pricing

{
id: 'seats',
name: 'Team Members',
type: 'usage',
priceId: process.env.STRIPE_PRICE_PRO_SEATS!,
unit: 'seat',
billingUsageType: 'licensed',
tiers: [
{ cost: 0, upTo: 1 }, // First seat included
{ cost: 5, upTo: 10 }, // $5/seat for seats 2-10
{ cost: 4, upTo: 50 }, // $4/seat for seats 11-50
{ cost: 3, upTo: 'infinite' }, // $3/seat for 51+
],
}

Example: AI Token Tiers

{
id: 'ai-tokens',
name: 'AI Tokens',
type: 'usage',
priceId: process.env.STRIPE_PRICE_AI_TOKENS!,
unit: 'token',
billingUsageType: 'metered',
tiers: [
{ cost: 0, upTo: 5000 }, // 5K free tokens
{ cost: 0.02, upTo: 50000 }, // $0.02/token up to 50K
{ cost: 0.01, upTo: 'infinite' }, // $0.01/token after 50K
],
}

Optional Line Items (Stripe Only)

Optional line items let customers choose add-ons during checkout. They appear as toggleable options.

Configuration

{
id: 'premium-support',
name: 'Premium Support',
type: 'flat',
priceId: process.env.STRIPE_PRICE_PREMIUM_SUPPORT!,
cost: 49,
optional: true, // Customer can toggle this
}

With Adjustable Quantity

{
id: 'extra-seats',
name: 'Additional Seats',
type: 'usage',
priceId: process.env.STRIPE_PRICE_EXTRA_SEATS!,
unit: 'seat',
cost: 10,
optional: true,
billingUsageType: 'licensed',
adjustableQuantity: {
enabled: true,
minimum: 1,
maximum: 100,
},
}

Restrictions

  • Requires priceId: Optional items must have a price ID for checkout
  • Cannot be metered: Metered prices are tracked by Stripe, not selected by customer
  • Requires base plan: Optional items need a planId or base lineItems - they can't be standalone

Display-Only Line Items

Use display-only line items to show pricing details in the UI without including them in checkout. This is useful when:

  • Checkout uses a single planId but you want to show component breakdown
  • You're displaying future or estimated costs

Configuration

Omit priceId and productId to make a line item display-only:

{
id: 'included-seats',
name: 'Included Seats',
type: 'usage',
unit: 'seat',
cost: 0,
// No priceId - display only
}

Provider Support Matrix

FeatureStripePolar
Multi-line item checkoutYesNo
Tiered pricingYesNo
Optional add-onsYesNo
Adjustable quantityYesNo
Subscription quantity updatesYesYes
Metered billingYesYes
Display-only itemsYesYes

Polar users: Build complex pricing in the Polar dashboard. lineItems in MakerKit are for UI display only (showing price breakdowns) - actual checkout uses the productId.

Complete Examples

Base + Seats + Metered Usage

A Pro plan with base fee, per-seat pricing, and metered AI tokens:

packages/billing/config/src/config.ts

{
id: 'pro',
name: 'Pro',
description: 'For growing teams',
currency: 'USD',
features: [
'Unlimited projects',
'Advanced analytics',
'Priority support',
],
plans: [
{
name: 'pro-monthly',
displayName: 'Pro',
interval: 'month',
cost: 29,
primaryPriceId: process.env.STRIPE_PRICE_PRO_BASE!,
limits: {
projects: null, // Unlimited
aiTokens: 100_000,
},
lineItems: [
{
id: 'base',
name: 'Pro Subscription',
type: 'flat',
priceId: process.env.STRIPE_PRICE_PRO_BASE!,
cost: 29,
},
{
id: 'seats',
name: 'Team Members',
type: 'usage',
priceId: process.env.STRIPE_PRICE_PRO_SEATS!,
unit: 'seat',
billingUsageType: 'licensed',
tiers: [
{ cost: 0, upTo: 1 }, // First seat included
{ cost: 5, upTo: 'infinite' },
],
},
{
id: 'ai-tokens',
name: 'AI Tokens',
type: 'usage',
priceId: process.env.STRIPE_PRICE_AI_TOKENS!,
unit: 'token',
billingUsageType: 'metered',
tiers: [
{ cost: 0, upTo: 5000 },
{ cost: 0.02, upTo: 50000 },
{ cost: 0.01, upTo: 'infinite' },
],
},
],
},
],
}

Plan with Optional Add-Ons

A plan using planId for the base subscription with optional premium support

packages/billing/config/src/config.ts

{
id: 'starter',
name: 'Starter',
description: 'Perfect for small teams',
currency: 'USD',
features: [
'Up to 5 team members',
'Core features',
'Email support',
],
plans: [
{
name: 'starter-monthly',
displayName: 'Starter',
interval: 'month',
limits: { seats: 5 },
lineItems: [
{
id: 'base',
name: 'Starter Subscription',
type: 'flat',
priceId: process.env.STRIPE_PRICE_STARTER!,
cost: 19,
},
{
id: 'premium-support',
name: 'Premium Support',
type: 'flat',
priceId: process.env.STRIPE_PRICE_PREMIUM_SUPPORT!,
cost: 49,
optional: true,
},
{
id: 'extra-storage',
name: 'Extra Storage (10GB)',
type: 'flat',
priceId: process.env.STRIPE_PRICE_EXTRA_STORAGE!,
cost: 9,
optional: true,
},
],
},
],
}

Volume Discount Tiered Pricing

Per-seat pricing with volume discounts:

{
id: 'team-seats',
name: 'Team Members',
type: 'usage',
priceId: process.env.STRIPE_PRICE_TEAM_SEATS!,
unit: 'seat',
billingUsageType: 'licensed',
adjustableQuantity: {
enabled: true,
minimum: 1,
maximum: 500,
},
tiers: [
{ cost: 15, upTo: 5 }, // $15/seat for 1-5 seats
{ cost: 12, upTo: 25 }, // $12/seat for 6-25 seats
{ cost: 10, upTo: 100 }, // $10/seat for 26-100 seats
{ cost: 8, upTo: 'infinite' }, // $8/seat for 101+ seats
],
}

Setting Up Stripe Prices

For multi-line item checkout, you need to create the corresponding prices in Stripe:

1. Flat Prices

Create as a standard recurring price:

  • Pricing model: Flat rate
  • Billing period: Monthly or yearly

2. Tiered Prices

Create with tiered pricing:

  • Pricing model: Tiered pricing
  • Tiers: Match your config tiers exactly

3. Metered Prices

Create with usage-based pricing:

  • Pricing model: Usage-based
  • Usage type: Metered
  • Aggregation: Sum (or as needed)

Limitations

Stripe Customer Portal and Multi-Line Subscriptions

When a subscription has multiple line items, the Stripe Customer Portal becomes limited. Customers with multi-product subscriptions can only cancel through the portal. They lose the ability to:

  • Change plans
  • Adjust quantities
  • Add or remove products
  • Make any subscription modifications

This is a documented Stripe limitation. Usage-based billing subscriptions face the same constraint.

What MakerKit does: The billing UI detects multi-line subscriptions and hides plan switching options automatically. Only cancellation remains available through the portal.

Upgrade strategies for multi-line subscriptions:

  1. Immediate invoicing (recommended): Configure Stripe to bill immediately on subscription changes. Programmatic upgrades via API feel instant to customers rather than waiting for the billing cycle.
  2. Custom upgrade flow: Build upgrade functionality into your app using billing.updateSubscriptionQuantity() and Stripe's API directly. Your UI handles what the portal cannot.
  3. SimplePlan for self-service: When customer-driven plan changes are a priority, use planId over lineItems.

Other Stripe Constraints

No mixed billing intervals: Every line item in a multi-price checkout must use the same interval. Mixing monthly base fees with yearly add-ons in one checkout session is not possible.

Trial behavior: Modifying a subscription that's still in trial ends the trial immediately and bills the customer.

Plan switching limits: The portal supports a maximum of 10 products in plan switching configurations.

Polar: A Different Model

Polar takes a different approach to complex pricing. Rather than assembling line items at checkout, you build complete products with all pricing components directly in the Polar dashboard.

lineItems with Polar is for display only. You can add lineItems to your billing config to show price breakdowns in your pricing table UI (base cost, per-seat pricing, what's included), but checkout uses productId to reference the Polar product. The line items are purely presentational - Polar bills based on how you configured the product in their dashboard.

Common Pitfalls

  • Mixing planId with lineItems for checkout: When using lineItems for multi-price checkout, don't include planId. Choose one approach. You can use planId with optional lineItems, but not planId with base lineItems.
  • Missing primaryPriceId: Without primaryPriceId, webhook handlers use the first line item's price ID for plan matching. This can cause issues if you reorder line items.
  • Metered prices as optional items: Metered prices cannot be optional because Stripe tracks usage automatically - there's no quantity to select at checkout.
  • Tiers not matching Stripe: Your config tiers must match Stripe's tier configuration exactly. Mismatches cause checkout or display issues.
  • Forgetting billingUsageType: For usage line items, always specify licensed (quantity at checkout) or metered (Stripe tracks usage).
  • Thinking lineItems affects Polar billing: With Polar, lineItems only control what's displayed in your pricing UI. Actual checkout uses productId - set up pricing in the Polar dashboard.

Frequently Asked Questions

Can I combine planId with lineItems?
Yes, but only for optional add-ons. Use planId for the base subscription and lineItems with optional: true for add-ons. Don't use both for the base checkout.
How do I show tiered pricing in the UI?
The pricing table component reads the tiers array and displays them. You can customize the display in packages/billing/ui/src/components/pricing-table.tsx.
What happens if a customer changes quantity mid-subscription?
Use the Stripe customer portal for quantity changes. Stripe prorates the difference automatically based on your portal settings.
Can I have different tiers for monthly vs yearly plans?
Yes. Each plan has its own lineItems with its own tiers. Configure different tier structures per interval.
How do I migrate from SimplePlan to AdvancedPlan?
Replace planId with lineItems containing your prices. Existing subscriptions continue unchanged; new checkouts use the new structure.

Next: Providers →