SaaS Per-Seat Subscription
The dominant pricing pattern in modern B2B SaaS: customers pay per active user per month or year, quantity changes mid-cycle as teams grow, and an annual prepay carries a meaningful discount over the equivalent monthly rate. Looks simple from the outside; the complexity hides in mid-cycle proration, trial-to-paid conversion, and accurate expansion-revenue tracking.
Real-world examples. Slack, Linear, Notion, Figma, Loom, GitHub, Cursor, ChatGPT Team. Common shape: $10-30 per seat per month, annual rates 15-20% lower, free trial 14-30 days, mid-cycle seat additions billed prorated, mid-cycle removals create credit toward the next invoice.
The shape of the problem
Per-seat pricing looks like a flat number on the page. The hard parts:
- Mid-cycle quantity changes - when a customer adds 3 seats on day 12 of a 30-day period, you owe them 18 days of charges at the new total, not a full month at the new total. Removing seats produces credit (some companies refund, others bank it).
- Trial conversion - the moment the trial ends, the first full-period invoice has to generate automatically with the seat count as of that moment.
- Annual prepay vs monthly - same product, two prices, customer can switch between them at renewal. Switching mid-term is also legitimate (and creates a credit/charge).
- Expansion revenue tracking - finance wants to know what fraction of MRR growth came from new customers vs existing customer expansion vs churn recovery. The seat-change events are the source of truth.
- Hard caps and soft caps - some pricing schemes cap seats at a tier boundary; some warn but allow overage.
Kontorion blueprint
| Concern | Kontorion primitive |
|---|---|
| Per-user quantity tracking | Subscription item quantity on a SEAT-typed product |
| Annual prepay discount | Two prices on the same plan, different billing_interval |
| Mid-cycle seat changes | POST /subscriptions/{id}/change-quantity with automatic proration |
| Trial conversion | Subscription lifecycle: TRIALING → ACTIVE after trial_duration_days |
| Switching annual / monthly | Scheduled change at next renewal |
| Expansion revenue tracking | subscription.quantity_changed webhook + /v1/analytics/mrr |
Build it
1. Define the seat product
Code
type: SEAT tells Kontorion that the subscription item's quantity field drives the line quantity at billing time.
2. Attach two prices to the plan - monthly and annual
Code
Both prices live on the same plan; the subscription chooses one via its billing_interval.
3. Start a subscription with a 14-day trial
Code
The subscription enters TRIALING. No invoice generates until the trial ends. When it does, the lifecycle transitions to ACTIVE and the first full-period invoice posts at 5 × $15 = $75.
4. Mid-cycle seat increase (proration handled)
On day 12 of a 30-day period, customer adds 3 seats:
Code
Proration is automatic - the engine emits credit and charge atoms based on the org's proration_strategy setting. The next invoice (or the current draft, depending on org settings) carries:
- A credit line: 5 seats × $15 × (12/30) for the days already paid at the old quantity
- A charge line: 8 seats × $15 × (18/30) for the remaining days at the new quantity
Net effect: customer is charged for the prorated delta, not double-charged.
5. Switch to annual at the next renewal
Customer wants to lock in the annual rate. Don't apply the change immediately - schedule it for the next billing period boundary so the current monthly cycle finishes cleanly:
Code
scheduled_change.upcoming fires at the configured lead time before the change so you can email the customer the heads-up.
Variations
- Volume discount over 50 seats. Replace the flat tier with a STAIRCASE walk:
[{up_to: 50, unit_amount: "15.00"}, {up_to: null, unit_amount: "12.00"}]. Pricing automatically applies the lower rate to seats above 50. - Free tier with hard cap. Set
unit_amount: "0"for the first 3 seats; downstream listeners onsubscription.quantity_changedcan alert or block when the customer crosses a threshold. - Custom enterprise rate. Apply a customer price override on the seat product for that one customer. The override beats the catalog price; the rest of the plan stays standard.
- Multi-currency catalog. Add EUR, GBP, and CAD prices with the same plan; each customer is billed in their
preferred_currency. FX is only used for cross-currency reporting, not customer-facing charges.
What you don't have to build
- Proration math on quantity changes (handled by the org-level
proration_strategysetting) - Trial countdown and conversion (lifecycle transition is automatic when
trial_duration_dayselapses) - Annual-vs-monthly switching at renewal (scheduled change carries the new
billing_interval) - Expansion-revenue analytics (
/v1/analytics/mrrreturns current vs. previous and a 12-month time series) - Trial-conversion notifications (
subscription.activatedwebhook) - "Manage seats" customer portal (use the same
POST /change-quantityendpoint server-side)
Next steps
- Subscriptions - lifecycle states, proration semantics
- Pricing Models - VOLUME vs STAIRCASE for seat tiering
- Scheduled Changes - annual / monthly switching
- Analytics - MRR, ARR, expansion revenue, cohort retention