---
title: "SaaS Subscription: Recurring Plans with Monthly Credit Grants"
description: "How to model a classic B2B SaaS with tiered subscription plans, monthly credit grants, rollover, overage policies, and plan upgrades."
order: 1
---

# SaaS Subscription

Pattern: recurring subscription with monthly credit grants.

**Pattern:** RECURRING PLANS · MONTHLY GRANTS

*Inspired by: Linear, Notion, Vercel, most B2B SaaS products*

> **Mental Model:** Think of this as the **classic B2B subscription**: tiered monthly plans that grant a fresh credit budget each cycle, with rollover for unused credits and topup packs for mid-cycle bursts. Same shape as Linear, Notion, or your average dev tool.

## Quick Take

- Tiered **plan variants** (Starter / Pro / Enterprise) with monthly recurring credit grants
- **Rollover** carries unused credits forward; **topup packs** handle mid-cycle bursts
- Burn order: **plan grants first** (priority 10), then rollover/topups (priority 5), wallet last
- **Overage policy** controls what happens when a customer hits zero — block, allow, or notify

## Diagram

Customer signs up for a plan variant; QuotaStack grants the variant's monthly credits at activation and on each renewal. Unused credits roll over per the rollover_percentage. Mid-cycle topup packs are added as separate credit blocks at priority 5. Usage events debit in burn-down order: plan grants → rollover/topups → wallet.

```mermaid
flowchart TD
    A[Sign up<br/>plan variant] --> B[POST /v1/subscriptions]
    B --> C[Plan grant<br/>P10, expires cycle end]
    D[Renewal each cycle] -->|rolls unused %| E[Rollover block<br/>P5, expires next cycle]
    D --> F[New plan grant<br/>P10]
    G[Mid-cycle topup pack] --> H[Topup block<br/>P5, expires 90d]
    C & E & F & H --> I[POST /v1/usage<br/>burn down]
```

## The problem

You run a B2B SaaS product. Customers subscribe to a plan (Starter, Pro, Enterprise). Each plan grants a monthly credit allowance. Customers consume credits through usage. When credits run out, behavior depends on your overage policy -- block further usage, or allow it and invoice the overage later.

You need:

- Tiered plans with different monthly credit amounts.
- Automatic credit grants on each billing cycle.
- Optional rollover of unused credits to the next cycle.
- Overage handling: hard block vs. soft limit.
- Plan upgrades and downgrades.
- Topup packs for customers who need more credits mid-cycle.

## Credit structure

| Block type | Priority | Expiry | Source |
|---|---|---|---|
| Monthly plan grant | 10 | End of billing cycle | Credit grant template on plan variant |
| Rollover credits | 5 | End of next billing cycle | Rolled from prior cycle |
| Topup pack | 5 | 90 days | Topup grant after payment |

**Burn-down order:** Monthly grant (priority 10) burns first. Within priority 10, soonest-expiring blocks go first. Rollover credits and topup packs (priority 5) burn next. This ensures current-cycle credits are consumed before carried-forward or purchased extras.

## Plan catalog

Set up plans, variants, and credit grant templates.

### Plans

```bash
POST /v1/plans
Idempotency-Key: plan:starter

{ "name": "Starter", "description": "For small teams" }

POST /v1/plans
Idempotency-Key: plan:pro

{ "name": "Pro", "description": "For growing teams" }

POST /v1/plans
Idempotency-Key: plan:enterprise

{ "name": "Enterprise", "description": "Custom for large organizations" }
```

### Plan variants (monthly cadence)

```bash
POST /v1/plans/{starter_id}/variants
Idempotency-Key: variant:starter-monthly

{
  "name": "Starter Monthly",
  "billing_cycle": "monthly",
  "billing_mode": "prepaid",
  "renewal_due_days": 3,
  "grace_period_days": 3,
  "allow_usage_while_overdue": true
}
```

```bash
POST /v1/plans/{pro_id}/variants
Idempotency-Key: variant:pro-monthly

{
  "name": "Pro Monthly",
  "billing_cycle": "monthly",
  "billing_mode": "prepaid",
  "renewal_due_days": 3,
  "grace_period_days": 3,
  "allow_usage_while_overdue": true
}
```

```bash
POST /v1/plans/{enterprise_id}/variants
Idempotency-Key: variant:enterprise-monthly

{
  "name": "Enterprise Monthly",
  "billing_cycle": "monthly",
  "billing_mode": "prepaid",
  "renewal_due_days": 5,
  "grace_period_days": 7,
  "allow_usage_while_overdue": true
}
```

### Credit grant templates

Attach grant rules to each variant. These fire automatically on subscription activation and renewal.

```bash
POST /v1/plans/{starter_id}/variants/{starter_monthly_id}/credit-grants
Idempotency-Key: grant:starter-monthly

{
  "credits": 10000000,
  "grant_interval": "billing_cycle",
  "grant_type": "recurring",
  "source": "plan_grant",
  "expires_after_seconds": 2592000,
  "rollover_percentage": 0,
  "accumulation_cap": null
}
```

10,000,000mc = 10,000 credits per cycle, expiring after 30 days (2,592,000 seconds). No rollover.

```bash
POST /v1/plans/{pro_id}/variants/{pro_monthly_id}/credit-grants
Idempotency-Key: grant:pro-monthly

{
  "credits": 50000000,
  "grant_interval": "billing_cycle",
  "grant_type": "recurring",
  "source": "plan_grant",
  "expires_after_seconds": 2592000,
  "rollover_percentage": 50,
  "max_rollover_cycles": 3,
  "accumulation_cap": 75000000
}
```

50,000 credits per cycle. 50% of unused credits roll over, for up to 3 consecutive cycles, capped at 75,000 total. This rewards consistent usage without unbounded accumulation.

```bash
POST /v1/plans/{enterprise_id}/variants/{enterprise_monthly_id}/credit-grants
Idempotency-Key: grant:enterprise-monthly

{
  "credits": 200000000,
  "grant_interval": "billing_cycle",
  "grant_type": "recurring",
  "source": "plan_grant",
  "expires_after_seconds": 2592000,
  "rollover_percentage": 100,
  "max_rollover_cycles": null,
  "accumulation_cap": null
}
```

200,000 credits per cycle. Full rollover, no cap, no cycle limit. Enterprise customers keep everything they paid for.

### Summary

| Plan | Credits/month | Rollover | Cap |
|---|---|---|---|
| Starter | 10,000 | None | -- |
| Pro | 50,000 | 50%, max 3 cycles | 75,000 |
| Enterprise | 200,000 | 100%, unlimited | None |

## Subscription lifecycle

### Creating a subscription

After the customer signs up and pays via your payment provider:

```bash
POST /v1/subscriptions
Idempotency-Key: sub:<your_subscription_id>

{
  "customer_id": "cus_...",
  "plan_variant_id": "pvr_starter_monthly",
  "external_subscription_id": "stripe_sub_abc",
  "metadata": {
    "stripe_price_id": "price_...",
    "monthly_amount_usd": "29"
  }
}
```

QuotaStack immediately:
1. Creates the subscription with status `active`.
2. Sets `current_period_start` to now and `current_period_end` to now + 1 month.
3. Fires the credit grant template: creates a 10,000-credit block expiring at period end.
4. Fires `subscription.created` webhook.

### Renewal flow (prepaid, tenant-driven)

QuotaStack does not charge the customer. Your app does. Here is the timeline:

**Day 27 (3 days before period end):** QuotaStack fires `subscription.renewal_due`:

```json
{
  "event": "subscription.renewal_due",
  "subscription_id": "sub_...",
  "customer_id": "cus_...",
  "current_period_end": "2026-05-13T08:00:00Z",
  "renewal_due_days": 3,
  "billing_mode": "prepaid"
}
```

Your webhook handler charges the customer via Stripe/Razorpay/etc.

**On successful payment:** Call renew:

```bash
POST /v1/subscriptions/sub_.../renew
Idempotency-Key: renew:sub_...:2

{}
```

QuotaStack:
1. Advances `current_period_start` and `current_period_end` by one month.
2. Evaluates the credit grant template. For Pro with rollover: computes unused balance from prior cycle blocks, applies 50% rollover, creates new block with `grant.credits + rollover_amount` clamped to `accumulation_cap`.
3. Fires `subscription.renewed` webhook.

**If payment fails:** Do not call renew. Let the period end.

**Day 30 (period end):** QuotaStack transitions the subscription to `overdue` and fires `subscription.renewal_overdue`:

```json
{
  "event": "subscription.renewal_overdue",
  "subscription_id": "sub_...",
  "customer_id": "cus_...",
  "period_end": "2026-05-13T08:00:00Z",
  "grace_ends_at": "2026-05-16T08:00:00Z",
  "grace_period_days": 3
}
```

During grace, the customer can still use remaining credits (because `allow_usage_while_overdue: true`). Retry the charge. If it succeeds, call renew -- the subscription returns to `active`.

**Day 33 (grace exhausted):** QuotaStack transitions to `expired` and fires `subscription.expired`:

```json
{
  "event": "subscription.expired",
  "subscription_id": "sub_...",
  "customer_id": "cus_...",
  "expired_at": "2026-05-16T08:00:00Z"
}
```

The subscription is done. Existing credit blocks continue until their own expiry. If you want the customer on a free plan, explicitly create a new subscription -- there is no automatic fallback.

## Rollover mechanics

Rollover is configured on the credit grant template, not on the subscription. When a subscription renews, QuotaStack evaluates each grant rule:

```
Prior cycle: 50,000 credits granted, 35,000 consumed, 15,000 remaining.
Rollover: 50% of 15,000 = 7,500 credits.
New cycle grant: 50,000 + 7,500 = 57,500 credits (under the 75,000 cap).
```

If the customer barely used their credits:

```
Prior cycle: 50,000 credits granted, 5,000 consumed, 45,000 remaining.
Rollover: 50% of 45,000 = 22,500 credits.
New cycle: 50,000 + 22,500 = 72,500 credits (under the 75,000 cap).
```

If rolled-over balance is already high:

```
Prior cycle: 72,500 credits granted, 10,000 consumed, 62,500 remaining.
Rollover: 50% of 62,500 = 31,250. 
New cycle: 50,000 + 31,250 = 81,250 -- exceeds cap.
Clamped to 75,000 credits.
```

The `max_rollover_cycles` limit resets the rollover counter when reached. After 3 consecutive rollovers, the cycle resets: old blocks expire, a fresh base grant is issued.

## Overage policy

What happens when a customer runs out of credits mid-cycle?

### Hard block (default)

Set `overage_policy: null` on the customer (inherits tenant default of `block`).

When balance hits 0, the entitlement check returns `allowed: false`. Your app shows "You've used your monthly credits. Upgrade your plan or buy a topup pack."

### Soft limit (allow overage)

Set `overage_policy: "allow"` on the customer:

```bash
PATCH /v1/customers/cus_...
Idempotency-Key: update:<uuid>

{
  "overage_policy": "allow"
}
```

Usage continues past 0. The balance goes negative. At cycle end, the `subscription.renewed` webhook includes usage summary data. Your billing system reads the overage, computes the fiat cost, and invoices accordingly.

### Notify + allow

Set `overage_policy: "notify"`. Same as `allow`, but QuotaStack also fires `credit.overage_threshold_exceeded` when the balance crosses a configured threshold. Use this to send the customer an alert before the invoice surprise.

## Topup packs for mid-cycle boosts

Customers who hit their limit can buy additional credits without changing plans:

```bash
POST /v1/topup/grant
Idempotency-Key: topup:<payment_id>

{
  "customer_id": "cus_...",
  "credits": 5000000,
  "expires_at": "2026-07-13T00:00:00Z",
  "metadata": {
    "source": "topup_pack",
    "priority": 5,
    "payment_id": "stripe_pi_..."
  }
}
```

5,000 credits with 90-day expiry and priority 5. These burn after the monthly grant (priority 10) but before any rollover credits at the same priority level (oldest-expiring first within the same priority).

## Upgrade and downgrade

### Upgrade (immediate)

Customer upgrades from Starter (10,000/mo) to Pro (50,000/mo). You want them to get Pro credits immediately.

1. Cancel the old subscription at period end:

```bash
POST /v1/subscriptions/{starter_sub_id}/cancel
Idempotency-Key: cancel:<uuid>

{
  "cancel_immediately": false,
  "reason": "upgrade to Pro"
}
```

2. Create the new subscription:

```bash
POST /v1/subscriptions
Idempotency-Key: sub:<new_sub_id>

{
  "customer_id": "cus_...",
  "plan_variant_id": "pvr_pro_monthly",
  "external_subscription_id": "stripe_sub_new",
  "metadata": { "upgraded_from": "starter" }
}
```

The new subscription activates immediately and grants 50,000 Pro credits. The old Starter credits remain until their block-level expiry (end of the original period). The customer temporarily has both -- credits stack.

At the old period end, the Starter subscription transitions to `canceled` and the Starter credits expire. The Pro subscription continues on its own cycle.

### Downgrade (queued)

Customer downgrades from Pro to Starter. You want them to keep Pro until the current period ends.

1. Do nothing in QuotaStack yet. In your app, record the intent: "downgrade to Starter at period end."

2. When `subscription.renewal_due` fires for the Pro subscription, do not renew it. Let it expire.

3. When `subscription.expired` fires, create the Starter subscription:

```bash
POST /v1/subscriptions
Idempotency-Key: sub:downgrade-<customer_id>-starter

{
  "customer_id": "cus_...",
  "plan_variant_id": "pvr_starter_monthly"
}
```

This approach keeps your downgrade logic in your app (where the business rules live) and uses QuotaStack purely for credit accounting and lifecycle events.

## Example: full billing cycle

Here is a complete month for a Pro customer:

```
Day 1:  Subscription created. 50,000 credits granted (priority 10, 30-day expiry).
        Customer starts using the product.

Day 15: Customer has used 30,000 credits. 20,000 remaining.
        Customer buys a 5,000-credit topup (priority 5, 90-day expiry).
        Total balance: 25,000.

Day 27: subscription.renewal_due webhook fires.
        Your app charges Stripe. Payment succeeds.
        POST /v1/subscriptions/{id}/renew.

Day 27: QuotaStack advances the period.
        Rollover: 50% of remaining Pro credits.
        Remaining at renewal: say 18,000 (used 2,000 more since day 15).
        Rollover: 9,000 credits.
        New grant: 50,000 + 9,000 = 59,000 credits (under 75,000 cap).
        Prior cycle Pro block expires at old period_end.
        New Pro block created: 59,000 credits, expires end of new period.
        Topup pack (5,000 credits) still active -- separate block, separate expiry.

Day 30: Old period ends. Old Pro block expires.
        Any remaining balance in the old block is zeroed.
        credit.expired webhook fires for the old block.

Day 31+: Customer uses new 59,000 Pro credits + any remaining topup.
```

## Tips

- **Prepaid means tenant-driven renewal.** QuotaStack fires the webhook, but you drive the charge and the renew call. If you forget to call renew, the subscription enters overdue and eventually expires. This is by design -- QuotaStack does not touch your payment provider.

- **Rollover is per-grant-rule, not per-subscription.** If a plan variant has two grant rules (e.g., "API credits" and "storage credits"), each rolls over independently with its own percentage and cap.

- **Credit blocks survive subscription cancellation.** Canceling a subscription does not void the credits already granted. The blocks remain until their own expiry. This is important for upgrades -- the old plan's remaining credits coexist with the new plan's grant.

- **`allow_usage_while_overdue` is your grace-period UX.** Set to `true` (default) for consumer-friendly behavior: during a payment retry window, the user keeps working. Set to `false` for strict enforcement: the moment the subscription is overdue, entitlement checks fail regardless of balance.

- **One customer, many subscriptions.** A customer can have a base subscription plus add-on subscriptions simultaneously. Each subscription independently grants credits and has its own renewal cycle. Credits from all sources stack in a single balance.

See also: [Subscriptions](/docs/concepts/subscriptions), [Credit Grants on Renewal](/docs/concepts/subscriptions#credit-grants-on-renewal), [Rollover](/docs/concepts/subscriptions#rollover), [Overage Policy](/docs/concepts/entitlements#configuring-overage-policy).

## Concepts Used

- [Subscriptions](/docs/concepts/subscriptions)
- [Credits](/docs/concepts/credits)
- [Topups & Wallets](/docs/concepts/topups-and-wallets)
- [Webhooks](/docs/concepts/webhooks)
