Docs / Use Cases / SaaS Subscription: Recurring Plans with Monthly Credit Grants
RECURRING PLANS · MONTHLY GRANTS

SaaS Subscription: Recurring Plans with Monthly Credit Grants

How to model a classic B2B SaaS with tiered subscription plans, monthly credit grants, rollover, overage policies, and plan upgrades.

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
EACH CYCLE Subscription grants rolls over customer buys P10 · EXPIRES CYCLE END Plan grant P5 · EXPIRES NEXT CYCLE Rollover block P5 · EXPIRES 90 DAYS Topup pack BURNS IN PRIORITY ORDER Usage Event

SaaS Subscription

Pattern: recurring subscription with monthly credit grants.

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 typePriorityExpirySource
Monthly plan grant10End of billing cycleCredit grant template on plan variant
Rollover credits5End of next billing cycleRolled from prior cycle
Topup pack590 daysTopup 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

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)

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
}
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
}
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.

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.

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.

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

PlanCredits/monthRolloverCap
Starter10,000None
Pro50,00050%, max 3 cycles75,000
Enterprise200,000100%, unlimitedNone

Subscription lifecycle

Creating a subscription

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

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:

{
  "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:

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:

{
  "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:

{
  "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:

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:

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:
POST /v1/subscriptions/{starter_sub_id}/cancel
Idempotency-Key: cancel:<uuid>

{
  "cancel_immediately": false,
  "reason": "upgrade to Pro"
}
  1. Create the new subscription:
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:

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, Credit Grants on Renewal, Rollover, Overage Policy.

Concepts used in this pattern

🤖
Building with an AI agent?
Get this page as markdown: /docs/use-cases/saas-subscription.md · Full index: /llms.txt