Docs / Concepts / Subscriptions

Subscriptions

Optional recurring billing state management with prepaid and postpaid modes, credit grants on renewal, rollover, and contract lifecycle.

Mental Model

Subscriptions are state machines for recurring billing. QuotaStack tracks the cycle and credits; you handle the payment. Prepaid: you drive renewal. Postpaid: QuotaStack auto-advances and sends you a usage summary.

Quick Take
Subscriptions are optional — many apps use pure wallet + topup instead
Two modes: prepaid (you charge and renew) vs postpaid (auto-renews with usage summary)
Credit grants on renewal with configurable rollover and accumulation caps
Contract support for fixed-term commitments with auto-renewal or expiry
STEP 1 Create STEP 2 Grant credits STEP 3 Renew STEP 4 Grant + rollover STEP 5 Cancel / expire

Subscriptions

Subscriptions in QuotaStack are optional. Many tenants use a pure wallet + topup model: customers buy credit packs, credits land in their wallet, usage debits from the wallet. No subscription needed.

Use subscriptions when you need:

  • Recurring billing cycles (monthly, yearly, etc.)
  • Automatic credit replenishment each period
  • Contract periods with defined start and end dates
  • Trial periods before activation
  • Rollover of unused credits between cycles

QuotaStack manages state, not payment

This is the core principle. QuotaStack tracks subscription status, billing periods, credit grants, and contract timelines. Your application handles payment collection. The flow is always:

  1. QuotaStack tells you something needs to happen (via webhook).
  2. You charge the customer in your payment system.
  3. You tell QuotaStack the outcome (via API call).

QuotaStack never talks to a payment processor directly.

Billing modes

Each plan variant has a billing_mode: either prepaid (default) or postpaid.

Prepaid

The tenant drives renewal. QuotaStack fires a subscription.renewal_due webhook before the period ends, giving you time to collect payment.

Period nearing end
QuotaStack fires subscription.renewal_due webhook
Tenant charges customer via payment system
POST /v1/subscriptions/{id}/renew
QuotaStack advances period + grants credits

Timing is controlled by two fields on the plan variant:

FieldDefaultDescription
renewal_due_days3Days before period end to fire subscription.renewal_due
grace_period_days3Days after period end before the subscription expires
allow_usage_while_overduetrueWhether the customer can still use credits during the grace period

If the tenant does not call the renew endpoint before the grace period expires, the subscription transitions: active -> overdue -> expired.

Postpaid

QuotaStack auto-advances the billing cycle. At period end, the cycle rolls forward automatically and a subscription.renewed webhook fires with a usage_summary payload containing credits consumed during the prior period. The tenant uses this to generate an invoice.

Period ends
QuotaStack auto-advances to next period
QuotaStack fires subscription.renewed webhook
with usage_summary: total consumed, balances, per-metric breakdown
Tenant generates invoice from usage_summary

Postpaid subscriptions still grant credits each cycle (the plan’s credit grants apply). The difference is that the cycle advances without waiting for tenant confirmation.

Status lifecycle

trialing active cancelling expired canceled overdue expired canceled paused resume contract_ended re-up
StatusMeaning
trialingTrial period active. Transitions to active automatically when trial_ends_at passes. To convert early, call POST /v1/subscriptions/{id}/renew — this activates the subscription and issues the first billing-cycle credit grant. Transitions to expired if the trial ends without conversion.
activeNormal operating state. Credits are granted on renewal.
cancellingCancel requested with cancel_at_period_end: true. Remains active until the current period ends, then transitions to expired.
overduePrepaid only. Period ended but tenant has not called renew. Grace period is ticking.
pausedTemporarily suspended. Can resume to active.
canceledTerminated immediately via cancel_immediately: true. Terminal state.
expiredGrace period elapsed without renewal, or cancelling subscription reached period end. Terminal state.
contract_endedContract end date reached. Can transition back to active if the contract is renewed.

Credit grants on renewal

Each plan variant has one or more credit_grants (defined in the plan_credit_grants table). These specify what credits are issued each billing cycle.

A credit grant has:

FieldDescription
creditsMillicredits to grant (1 credit = 1,000 mc)
grant_intervalWhen to grant. Keyword (on_activation, daily, weekly, monthly, billing_cycle) or ISO 8601 duration (e.g. PT5H). See below.
grant_typeone_time, recurring, or trial
expires_after_secondsOptional. Seconds until this credit block expires. Null = never.
sourceCredit source label (e.g. plan_grant, trial)

Grants with grant_interval: on_activation or grant_type: one_time fire only when the subscription is first created. Recurring grants fire on their configured interval.

Interval form: keyword or ISO 8601

grant_interval accepts two forms:

Keywords — use these for standard cadences. on_activation | daily | weekly | monthly | billing_cycle. The billing_cycle keyword ties the grant to the subscription’s billing cycle end.

ISO 8601 durations — use these for custom cadences. The grant cadence runs independently of the subscription’s billing cycle, so you can have monthly billing with a 5-hour quota reset (Claude Pro–style).

FormExampleUse when
"PT5H"Every 5 hoursClaude Pro–style resetting quotas
"PT30M"Every 30 minutesHigh-frequency burst windows
"P3D"Every 3 daysSub-weekly cadences
"P1DT12H"Every 1.5 daysNon-standard cadences

Supported letters: D (days), H (hours), M (minutes), S (seconds). Year and month durations aren’t supported as ISO 8601 — their length is variable, so use the monthly or yearly keywords instead.

Minimum interval: 5 minutes (PT5M). Shorter values return 422.

Timing anchor. The first grant fires when the subscription is created. Subsequent grants fire on anniversary-based timing — at subscription.created_at + N × interval, not relative to the previous fire. Delayed or missed grants don’t shift future schedules forward.

Recovery after downtime. If QuotaStack is unavailable for several cadence intervals, only the current window’s credits are issued on recovery. Earlier windows are not backfilled.

For a worked Claude Pro–style example (subscription + PT5H resetting quota), see the Consumer AI Subscription use case.

Rollover

By default, unused credits from the previous cycle can be expired and replaced with a fresh grant. Rollover changes this: a percentage of unused credits carries forward into the new cycle.

FieldDescription
rollover_percentage0-100. Percentage of unused credits from the prior cycle to carry forward. 0 or null = no rollover.
max_rollover_cyclesMaximum number of consecutive cycles credits can roll over. Null = unlimited.
accumulation_capMaximum balance (in millicredits) the grant can push the account to. Null = unlimited.

Example: A plan grants 10,000 mc per month with rollover_percentage: 50 and accumulation_cap: 15000.

  • Month 1: Customer uses 6,000 mc. 4,000 mc unused.
  • Month 2: Rollover = 50% of 4,000 = 2,000 mc. New grant = 10,000 + 2,000 = 12,000 mc, capped so balance_after_grant <= 15,000 mc.
  • If the customer had 14,000 mc remaining at renewal, the grant is clamped to 1,000 mc so the post-grant balance sits at the cap (15,000 mc).
  • If the customer had 15,000 mc already, the grant is 0 — the cap clamps the grant amount, never reduces existing balance.

When max_rollover_cycles is reached, the rollover counter resets and only the base grant amount is issued.

Contracts

Subscriptions can have a contract period defined by contract_start and contract_end. These are independent of billing cycles — a yearly contract might have monthly billing.

FieldDescription
contract_startSet automatically to the subscription start date.
contract_endOptional. When the contract expires.
contract_ending_soon_daysDays before contract_end to fire the subscription.contract_ending_soon webhook. Default: 30.

When contract_end is reached, the subscription transitions to contract_ended. To renew the contract, use:

POST /v1/subscriptions/{id}/extend
{
  "contract_end": "2027-01-01T00:00:00Z",
  "reason": "Annual renewal"
}

Pause and resume

Pause suspends a subscription without canceling it. Paused subscriptions do not receive credit grants on scheduled renewals and do not advance billing periods, but the customer’s existing credits remain spendable.

curl -X POST https://api.quotastack.io/v1/subscriptions/{id}/pause \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: pause:{subscriptionId}" \
  -H "Content-Type: application/json" \
  -d '{}'

To reinstate the subscription:

curl -X POST https://api.quotastack.io/v1/subscriptions/{id}/resume \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: resume:{subscriptionId}" \
  -H "Content-Type: application/json" \
  -d '{}'

Pause and resume fire subscription.paused and subscription.resumed webhooks respectively.

After contract_end

When contract_end is reached, the subscription transitions to contract_ended. This is a historical marker — it records that the billing relationship ended but does not freeze the credit account:

  • Remaining credits stay usable until they expire naturally.
  • POST /v1/usage is still accepted — consumption debits remaining credit blocks.
  • Entitlement checks continue to return live results based on balance + overage policy.

If you need to fully cut off access at contract_ended, cancel the subscription (cancel_immediately: true) and void remaining credit blocks via POST /v1/customers/{id}/credits/adjust with a negative delta.

Cancellation

Two cancel modes, controlled by the cancel_immediately field on the cancel request:

End-of-period (cancel_immediately: false): The subscription enters cancelling status. The customer retains access until the current period ends. At period end, the subscription transitions to expired.

curl -X POST https://api.quotastack.io/v1/subscriptions/{id}/cancel \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: cancel:{subscriptionId}" \
  -H "Content-Type: application/json" \
  -d '{"cancel_immediately": false, "reason": "Customer requested downgrade"}'

Immediate (cancel_immediately: true): The subscription transitions directly to canceled. Terminal. No further credit grants.

Webhooks

Subscriptions emit these webhook events:

EventWhen
subscription.createdNew subscription created
subscription.renewal_duePrepaid: period end approaching, tenant should charge and call renew
subscription.renewedPeriod advanced and credits granted (prepaid after renew call, postpaid auto)
subscription.renewal_overduePrepaid: period ended without renewal, grace period started
subscription.expiredSubscription expired (grace period elapsed or cancelling period ended)
subscription.canceledSubscription canceled immediately
subscription.upgradedMoved to a higher-tier plan variant
subscription.downgradedMoved to a lower-tier plan variant
subscription.pausedSubscription paused
subscription.resumedSubscription resumed
subscription.contract_ending_soonContract end date approaching
subscription.contract_endedContract end date reached

For postpaid subscriptions, the subscription.renewed webhook includes a usage_summary object with total credits consumed, balance snapshots, and per-metric breakdowns for the prior period.

Example: prepaid monthly subscription

POST /v1/subscriptions accepts either external_customer_id (your tenant ID) or customer_id (the QuotaStack UUID) in the body — exactly one required, same convention as the other body endpoints (see Customer identification).

# 1. Create a subscription using your tenant's identifier.
curl -X POST https://api.quotastack.io/v1/subscriptions \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: sub-create:{externalCustomerId}:{planVariantId}" \
  -H "Content-Type: application/json" \
  -d '{
    "external_customer_id": "user_abc123",
    "plan_variant_id": "pv_monthly_pro",
    "contract_end": "2027-04-01T00:00:00Z"
  }'

# 2. When you receive "subscription.renewal_due" webhook,
#    charge the customer, then:
curl -X POST https://api.quotastack.io/v1/subscriptions/{id}/renew \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: renew:{paymentId}" \
  -H "Content-Type: application/json" \
  -d '{
    "external_payment_id": "pay_xyz789"
  }'

If you already have the QuotaStack UUID in hand (e.g., from a prior response), use customer_id instead:

{
  "customer_id": "019d8a20-4ff5-7be0-81da-e1454b3d6f64",
  "plan_variant_id": "pv_monthly_pro"
}

Constraints and notes

  • billing_mode is immutable after plan variant creation. To switch a customer from prepaid to postpaid (or vice versa), create a new plan variant with the desired mode and move the subscription over.
  • Postpaid with zero credit grants is valid. A plan variant with billing_mode: "postpaid" and no credit_grants is a pure usage-based setup — the cycle advances automatically, usage accumulates, and the subscription.renewed webhook delivers a usage_summary for you to invoice against. No credits are pre-granted.

Common Mistakes

The mistakes developers typically make with this concept — and what to do instead.

×
Don't use subscriptions if you just need topups
Why
Subscriptions add state (periods, renewals, grace, contracts). If customers simply buy credits when they need them, skip subscriptions entirely.
×
Don't ignore subscription.renewal_overdue in prepaid mode
Why
This webhook means the grace period started. Not handling it leaves customers in limbo and eventually silently expires their subscription.
×
Don't assume postpaid auto-collects payment
Why
Postpaid sends you a usage summary after each period. You still have to invoice the customer through your payment processor — QuotaStack never touches money.
🤖
Building with an AI agent?
Get this page as markdown: /docs/concepts/subscriptions.md · Full index: /llms.txt