quotastack Docs
Docs / API / Plans

Plans

What a customer can buy, and what it grants.

A plan names one product tier, like “Pro” or “Team.” A plan holds no price and no billing details of its own.

A plan variant is what a customer actually subscribes to. “Pro, billed monthly” and “Pro, billed yearly” are two variants of the same “Pro” plan. Each variant sets its own billing cycle, trial length, and billing mode. A variant can carry recurring credit grants and its own entitlements.

Which call do I want?

  • You are naming a product tier, like "Pro" or "Team"POST /v1/plans
  • You are setting a billing cycle, trial length, or billing mode for that tierPOST /v1/plans/{id}/variants
    A plan holds no price or billing details. A variant does. "Pro" is a plan; "Pro, billed monthly" is one of its variants.
  • You want every subscriber on a variant to receive credits on a schedulePOST /v1/plans/{id}/variants/{vid}/grants
    This call only defines the schedule. The scheduler grants the credits, at each renewal — not this call.
  • You want every subscriber on a variant to get a feature or cap that credits do not pricePOST /v1/plans/{id}/variants/{vid}/entitlements
    Use a metering rule instead when the feature should cost credits per use, not a flat switch.
  • You want a plan, its variants, and their credit grants in one responseGET /v1/plans
    Add include_variants=true to see them. getPlan alone never returns variants; listPlanVariants alone never returns grants.
  • You already have a plan variant’s id and only need that one variantGET /v1/plan-variants/{id}
    Reads /v1/plan-variants/{id} directly, with no plan id needed. listPlanVariants needs the parent plan’s id too.
13 of 13 operations documented in full.

List plans

GET/v1/plans
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This endpoint lists every plan for your tenant, newest first. Left out, status defaults to active only. Add status=retired to see retired plans too. Add include_variants=true to nest each plan’s variants and their credit grants in one response. That one call is the only way to see all three levels at once. A bad cursor returns 400. openapi.yaml does not declare that status here.

Query parameters
FieldTypeMeaning
statusoptionalstring

Only plans in this state.

If you leave it out: QuotaStack returns only active plans by default. Add status=retired to also see retired ones.

  • activeThe normal state. Nothing here blocks a new variant or a new subscription.
  • retiredA lifecycle flag only. Creating a variant under this plan still works — retiring does not lock it.
include_variantsoptionalboolean

Nest each plan’s variants, and each variant’s credit grants, in the response.

If you leave it out: Left out, data[].variants is an empty array.

cursoroptionalstring

Opaque pagination cursor from the previous page's pagination.next_cursor. Omit to start at the first page.

limitoptionalinteger

Page size. Default 20, max 100.

Response 200
{
  "data": [
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
      "name": "Pro",
      "is_default": true,
      "status": "active",
      "metadata": {},
      "created_at": "2026-07-20T09:00:00Z",
      "updated_at": "2026-07-20T09:00:00Z",
      "variants": [
        {
          "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
          "plan_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
          "billing_cycle": "monthly",
          "trial_days": 0,
          "status": "active",
          "billing_mode": "prepaid",
          "renewal_due_days": 3,
          "grace_period_days": 3,
          "allow_usage_while_overdue": true,
          "metadata": {},
          "created_at": "2026-07-20T09:05:00Z",
          "updated_at": "2026-07-20T09:05:00Z",
          "grants": [
            {
              "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a61",
              "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
              "credits": 10000,
              "grant_interval": "monthly",
              "expires_after_seconds": null,
              "accumulation_cap": 15000,
              "rollover_percentage": 50,
              "max_rollover_cycles": 3,
              "grant_type": "recurring",
              "source": "promotional",
              "metadata": null,
              "created_at": "2026-07-20T09:10:00Z",
              "updated_at": "2026-07-20T09:10:00Z"
            }
          ]
        }
      ]
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
Response fields
FieldTypeMeaning
datarequiredarray

One row per plan, newest first.

data.namerequiredstring

The tier’s name, shown in the dashboard and in every read.

data.is_defaultrequiredboolean

Whether new customers land on this plan when nothing else applies.

data.statusrequiredstring

This plan is active, or it is retired.

  • activeThe normal state. Nothing here blocks a new variant or a new subscription.
  • retiredA lifecycle flag only. Creating a variant under this plan still works — retiring does not lock it.
data.variantsrequiredarray

Every variant under this plan. Empty unless you sent include_variants=true.

data.variants.billing_cyclerequiredstring

How often this variant renews. Fixed forever once the variant is created.

  • dailyRenews once a day.
  • weeklyRenews once a week.
  • monthlyRenews on the same day each month.
  • yearlyRenews on the same day each year.
  • one_timeNever renews. One purchase, and nothing repeats after it.
data.variants.statusrequiredstring

This variant is active, or it is retired.

  • activeThe normal state.
  • retiredA lifecycle flag only. This variant keeps its subscriptions, grants, and entitlements exactly as they were.
data.variants.billing_moderequiredstring

Who advances the billing period. Fixed forever once the variant is created.

  • prepaidYou call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaidQuotaStack moves the period forward on its own. QuotaStack then reports usage back to you, to invoice.
data.variants.grantsoptionalarray

Every credit grant tied to this variant.

data.variants.grants.creditsrequiredinteger (int64)

How many credits this grant issues each time it fires.

Unit: millicredits, where 1 credit is 1000 millicredits.

If you leave it out: QuotaStack rejects the call. This field is required, and must be more than zero.

data.variants.grants.grant_intervalrequiredstring

When this grant fires. Use one of the five keywords below, or an ISO 8601 duration like PT5H for a custom cadence that runs on its own clock, apart from billing. QuotaStack accepts nothing shorter than 5 minutes. See subscriptions for the full duration rules.

If you leave it out: QuotaStack rejects the call. This field is required.

data.variants.grants.grant_typerequiredstring

How often this grant fires overall. Pairing recurring with grant_interval: on_activation is invalid. Use one_time for a grant that fires only at activation.

If you leave it out: QuotaStack rejects the call. This field is required.

  • recurringFires every time grant_interval comes around, for as long as the subscription stays active.
  • one_timeFires exactly once, on grant_interval’s next turn, then never again.
  • trialFires once, meant for a subscription’s trial period.
data.variants.grants.sourcerequiredstring

Why this grant exists.

If you leave it out: By default, it is plan_grant.

  • plan_grantThe default. Use this for a grant tied to the variant’s normal schedule.
  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • migrationYou moved these from an old system.
data.variants.grants.metadatarequiredobject

QuotaStack always returns null here. There is no way to attach metadata to a credit grant through this API.

paginationrequiredobject

Cursor-based pagination envelope. Returned on every list endpoint. No total — cursor-only.

Create a plan

POST/v1/plans
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne new plan, status: active. If is_default is true, QuotaStack also clears the previous default plan’s flag.
Fires No webhooks.

A plan names one product tier, like "Pro" or "Team." A plan holds no price and no billing cadence — add a plan variant for that. Sending is_default: true also clears the tenant’s old default plan, in this same call.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

A unique string you choose, up to 256 characters. It stops a retry from doing the work twice.

This header is required on every POST and PATCH. Leave it out and the request fails with 422, before any change is made.

Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries X-Idempotent-Replayed: true. Replays are available for 24 hours.

Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example topup:{payment_intent_id} or renewal:{subscription_id}:{period_start}.

Body parameters
FieldTypeMeaning
namerequiredstring

The tier’s name.

If you leave it out: QuotaStack rejects the call. This field is required.

is_defaultoptionalboolean

Whether new customers land on this plan when nothing else applies. Making this plan the default clears any previous one.

If you leave it out: By default, it is false.

metadataoptionalobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: Left out, QuotaStack stores {}.

Request
{
  "name": "Pro"
}
Response 201
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
  "name": "Pro",
  "is_default": false,
  "status": "active",
  "metadata": {},
  "created_at": "2026-07-20T09:00:00Z",
  "updated_at": "2026-07-20T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
namerequiredstring

The tier’s name, shown in the dashboard and in every read.

is_defaultrequiredboolean

Whether new customers land on this plan when nothing else applies.

statusrequiredstring

This plan is active, or it is retired.

  • activeThe normal state. Nothing here blocks a new variant or a new subscription.
  • retiredA lifecycle flag only. Creating a variant under this plan still works — retiring does not lock it.
metadatarequiredobject
created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Errors

  • 422name is missing or empty. The response names the field. validation-error

Retrieve a plan

GET/v1/plans/{id}
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This endpoint returns one plan by its id. The reply never includes variants — list listPlanVariants on its own, or call listPlans with include_variants=true.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
  "name": "Pro",
  "is_default": false,
  "status": "active",
  "metadata": {},
  "created_at": "2026-07-20T09:00:00Z",
  "updated_at": "2026-07-20T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
namerequiredstring

The tier’s name, shown in the dashboard and in every read.

is_defaultrequiredboolean

Whether new customers land on this plan when nothing else applies.

statusrequiredstring

This plan is active, or it is retired.

  • activeThe normal state. Nothing here blocks a new variant or a new subscription.
  • retiredA lifecycle flag only. Creating a variant under this plan still works — retiring does not lock it.
metadatarequiredobject
created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Errors

  • 404No plan here has this id. not-found

Update a plan

PATCH/v1/plans/{id}
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesReplaces whichever of name, status, is_default, and metadata you send; metadata is replaced outright, not merged. Setting is_default: true also clears the previous default plan’s flag, in the same call.
Fires No webhooks.

This endpoint changes one or more fields on an existing plan. A field you skip keeps its old value. Setting is_default to true clears the old default plan’s flag; nothing else about that plan changes. A missing plan returns 404. Retiring the default plan returns 409. A bad status, or a blank name, returns 422. openapi.yaml declares only 200 here, so none of the three shows below.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

A unique string you choose, up to 256 characters. It stops a retry from doing the work twice.

This header is required on every POST and PATCH. Leave it out and the request fails with 422, before any change is made.

Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries X-Idempotent-Replayed: true. Replays are available for 24 hours.

Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example topup:{payment_intent_id} or renewal:{subscription_id}:{period_start}.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Body parameters
FieldTypeMeaning
nameoptionalstring

The tier’s new name. Unlike a customer’s display_name, an empty string is rejected — a plan always needs a real name.

If you leave it out: The old value stays. QuotaStack changes nothing.

statusoptionalstring

This plan’s new lifecycle state.

If you leave it out: The old value stays. QuotaStack changes nothing.

  • activeThe normal state. Nothing here blocks a new variant or a new subscription.
  • retiredA lifecycle flag only. Creating a variant under this plan still works — retiring does not lock it.
is_defaultoptionalboolean

Set true to make this plan the default, clearing whichever plan held that flag before.

If you leave it out: The old value stays. QuotaStack changes nothing.

metadataoptionalobject

Your own key-value tags. Sending any value here replaces the whole stored object — QuotaStack does not merge keys in.

If you leave it out: The old value stays. QuotaStack changes nothing.

Request
{
  "is_default": true
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
  "name": "Pro",
  "is_default": true,
  "status": "active",
  "metadata": {},
  "created_at": "2026-07-20T09:00:00Z",
  "updated_at": "2026-07-20T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
namerequiredstring

The tier’s name, shown in the dashboard and in every read.

is_defaultrequiredboolean

Whether new customers land on this plan when nothing else applies.

statusrequiredstring

This plan is active, or it is retired.

  • activeThe normal state. Nothing here blocks a new variant or a new subscription.
  • retiredA lifecycle flag only. Creating a variant under this plan still works — retiring does not lock it.
metadatarequiredobject
created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

List plan variants

GET/v1/plans/{id}/variants
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This endpoint lists every variant under one plan, oldest first. Both active and retired variants come back, with no paging — every variant comes back in one call. QuotaStack never checks that the plan itself is real. An unknown plan id returns 200 with an empty list, not 404. The reply never includes a variant’s credit grants — call listPlans with include_variants=true for those.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Response 200
{
  "data": [
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
      "plan_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
      "billing_cycle": "monthly",
      "trial_days": 0,
      "status": "active",
      "billing_mode": "prepaid",
      "renewal_due_days": 3,
      "grace_period_days": 3,
      "allow_usage_while_overdue": true,
      "metadata": {},
      "created_at": "2026-07-20T09:05:00Z",
      "updated_at": "2026-07-20T09:05:00Z"
    }
  ]
}
Response fields
FieldTypeMeaning
datarequiredarray

One row per variant, oldest first. Both active and retired variants come back.

data.billing_cyclerequiredstring

How often this variant renews. Fixed forever once the variant is created.

  • dailyRenews once a day.
  • weeklyRenews once a week.
  • monthlyRenews on the same day each month.
  • yearlyRenews on the same day each year.
  • one_timeNever renews. One purchase, and nothing repeats after it.
data.statusrequiredstring

This variant is active, or it is retired.

  • activeThe normal state.
  • retiredA lifecycle flag only. This variant keeps its subscriptions, grants, and entitlements exactly as they were.
data.billing_moderequiredstring

Who advances the billing period. Fixed forever once the variant is created.

  • prepaidYou call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaidQuotaStack moves the period forward on its own. QuotaStack then reports usage back to you, to invoice.

Create a plan variant

POST/v1/plans/{id}/variants
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne new plan variant, status: active. The new variant carries no credit grants or entitlements yet — add those with separate calls.
Fires No webhooks.

A plan variant is a priced, billed instance of a plan. "Pro, billed monthly" and "Pro, billed yearly" are two variants of the same "Pro" plan. billing_cycle, billing_mode, and the renewal timing fields are all fixed forever once this call returns. A missing plan returns 404 and a bad billing_cycle returns 422, though openapi.yaml declares only 201 here.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

A unique string you choose, up to 256 characters. It stops a retry from doing the work twice.

This header is required on every POST and PATCH. Leave it out and the request fails with 422, before any change is made.

Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries X-Idempotent-Replayed: true. Replays are available for 24 hours.

Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example topup:{payment_intent_id} or renewal:{subscription_id}:{period_start}.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Body parameters
FieldTypeMeaning
billing_cyclerequiredstring

How often this variant renews.

If you leave it out: QuotaStack rejects the call. This field is required.

  • dailyRenews once a day.
  • weeklyRenews once a week.
  • monthlyRenews on the same day each month.
  • yearlyRenews on the same day each year.
  • one_timeNever renews. One purchase, and nothing repeats after it.
trial_daysrequiredinteger0–∞

How many days a new subscription spends in trialing before its first charge.

If you leave it out: openapi.yaml marks this field required, but QuotaStack does not enforce that. Left out, it defaults to 0 — no trial.

billing_modeoptionalstring

Who advances the billing period. Fixed forever once you create the variant.

If you leave it out: By default, it is prepaid.

  • prepaidYou call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaidQuotaStack moves the period forward on its own. QuotaStack then reports usage back to you, to invoice.
renewal_due_daysoptionalinteger

Days before the period ends that QuotaStack warns you to renew. Prepaid only.

If you leave it out: By default, it is 3.

grace_period_daysoptionalinteger

How many days late a prepaid plan can go before it ends.

If you leave it out: By default, it is 3.

allow_usage_while_overdueoptionalboolean

Whether a customer can spend credit while their bill is late.

If you leave it out: By default, it is true.

metadataoptionalobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: Left out, QuotaStack stores {}.

Request
{
  "billing_cycle": "monthly",
  "trial_days": 14
}
Response 201
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "plan_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
  "billing_cycle": "monthly",
  "trial_days": 0,
  "status": "active",
  "billing_mode": "prepaid",
  "renewal_due_days": 3,
  "grace_period_days": 3,
  "allow_usage_while_overdue": true,
  "metadata": {},
  "created_at": "2026-07-20T09:05:00Z",
  "updated_at": "2026-07-20T09:05:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
plan_idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
billing_cyclerequiredstring

How often this variant renews. Fixed forever once the variant is created.

  • dailyRenews once a day.
  • weeklyRenews once a week.
  • monthlyRenews on the same day each month.
  • yearlyRenews on the same day each year.
  • one_timeNever renews. One purchase, and nothing repeats after it.
trial_daysrequiredinteger0–∞

How many days a new subscription on this variant spends in trialing before its first charge.

statusrequiredstring

This variant is active, or it is retired.

  • activeThe normal state.
  • retiredA lifecycle flag only. This variant keeps its subscriptions, grants, and entitlements exactly as they were.
billing_moderequiredstring

Who advances the billing period. Fixed forever once the variant is created.

  • prepaidYou call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaidQuotaStack moves the period forward on its own. QuotaStack then reports usage back to you, to invoice.
renewal_due_daysrequiredinteger

Days before the period ends that QuotaStack warns you to renew. Prepaid only.

grace_period_daysrequiredinteger

How many days late a prepaid plan can go before it ends.

allow_usage_while_overduerequiredboolean

Whether a customer can spend credit while their bill is late.

metadatarequiredobject

Your own key-value tags, unchanged since the last write.

created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Update a plan variant

PATCH/v1/plans/{id}/variants/{vid}
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesReplaces whichever of status, trial_days, and metadata you send; a field left out keeps its old value. metadata is replaced outright, not merged.
Fires No webhooks.

This endpoint changes only status, trial_days, and metadata. Every other field is fixed at creation — make a new variant to change one of those. A missing variant returns 404. A bad status returns 422. openapi.yaml declares only 200 here.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

A unique string you choose, up to 256 characters. It stops a retry from doing the work twice.

This header is required on every POST and PATCH. Leave it out and the request fails with 422, before any change is made.

Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries X-Idempotent-Replayed: true. Replays are available for 24 hours.

Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example topup:{payment_intent_id} or renewal:{subscription_id}:{period_start}.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

vidrequiredstring (uuid)

The plan variant’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Body parameters
FieldTypeMeaning
statusoptionalstring

This variant’s new state: active or retired.

If you leave it out: The old value stays. QuotaStack changes nothing.

  • activeThe normal state.
  • retiredA lifecycle flag only. This variant keeps its subscriptions, grants, and entitlements exactly as they were.
trial_daysoptionalinteger

How many trial days this variant grants a new subscription.

If you leave it out: The old value stays. QuotaStack changes nothing.

metadataoptionalobject

Your own key-value tags. Sending any value here replaces the whole stored object — QuotaStack does not merge keys in.

If you leave it out: The old value stays. QuotaStack changes nothing.

Request
{
  "status": "retired"
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "plan_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
  "billing_cycle": "monthly",
  "trial_days": 0,
  "status": "retired",
  "billing_mode": "prepaid",
  "renewal_due_days": 3,
  "grace_period_days": 3,
  "allow_usage_while_overdue": true,
  "metadata": {},
  "created_at": "2026-07-20T09:05:00Z",
  "updated_at": "2026-07-20T09:05:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
plan_idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
billing_cyclerequiredstring

How often this variant renews. Fixed forever once the variant is created.

  • dailyRenews once a day.
  • weeklyRenews once a week.
  • monthlyRenews on the same day each month.
  • yearlyRenews on the same day each year.
  • one_timeNever renews. One purchase, and nothing repeats after it.
trial_daysrequiredinteger0–∞

How many days a new subscription on this variant spends in trialing before its first charge.

statusrequiredstring

This variant is active, or it is retired.

  • activeThe normal state.
  • retiredA lifecycle flag only. This variant keeps its subscriptions, grants, and entitlements exactly as they were.
billing_moderequiredstring

Who advances the billing period. Fixed forever once the variant is created.

  • prepaidYou call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaidQuotaStack moves the period forward on its own. QuotaStack then reports usage back to you, to invoice.
renewal_due_daysrequiredinteger

Days before the period ends that QuotaStack warns you to renew. Prepaid only.

grace_period_daysrequiredinteger

How many days late a prepaid plan can go before it ends.

allow_usage_while_overduerequiredboolean

Whether a customer can spend credit while their bill is late.

metadatarequiredobject

Your own key-value tags, unchanged since the last write.

created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Attach a credit grant to a plan variant

POST/v1/plans/{id}/variants/{vid}/grants
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne new grant schedule on this variant. QuotaStack grants no credit yet; the scheduler does that later, per subscription. The reply’s metadata is always null — this request has no metadata field to set it with.
Fires No webhooks.

This endpoint is where a recurring grant schedule lives. Creating one grants no credit right away. The scheduler grants credit at each grant_interval, when a subscription first starts or renews. That same renewal fires subscription.renewed. There is no way to update or delete a grant afterward. Make a new plan variant to change one. A missing variant returns 404. A bad credits, grant_interval, or grant_type returns 422. openapi.yaml declares only 201 here.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

A unique string you choose, up to 256 characters. It stops a retry from doing the work twice.

This header is required on every POST and PATCH. Leave it out and the request fails with 422, before any change is made.

Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries X-Idempotent-Replayed: true. Replays are available for 24 hours.

Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example topup:{payment_intent_id} or renewal:{subscription_id}:{period_start}.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

vidrequiredstring (uuid)

The plan variant’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Body parameters
FieldTypeMeaning
creditsrequiredinteger (int64)1–∞

How many credits this grant issues each time it fires.

Unit: millicredits, where 1 credit is 1000 millicredits.

If you leave it out: QuotaStack rejects the call. This field is required, and must be more than zero.

grant_intervalrequiredstring

When this grant fires. Use one of the five keywords below, or an ISO 8601 duration like PT5H for a custom cadence that runs on its own clock, apart from billing. QuotaStack accepts nothing shorter than 5 minutes. See subscriptions for the full duration rules.

If you leave it out: QuotaStack rejects the call. This field is required.

expires_after_secondsoptionalinteger

How long each credit block from this grant lasts, counted from when it fires.

Unit: seconds.

If you leave it out: Left out, the block never expires.

accumulation_capoptionalinteger (int64)

The highest balance this grant is allowed to push the account to.

Unit: millicredits, where 1 credit is 1000 millicredits.

If you leave it out: Left out, there is no cap. The grant always adds its full amount.

rollover_percentageoptionalinteger0–100

How much unused credit from last cycle carries into this grant, from 0 to 100 percent. A prepaid renewal is what applies this.

Unit: percent.

If you leave it out: Left out, or 0, nothing carries forward.

max_rollover_cyclesoptionalinteger

How many cycles in a row credit can roll over. After that, QuotaStack resets the count to zero.

If you leave it out: Left out, rollover has no cycle limit.

grant_typerequiredstring

How often this grant fires overall. Pairing recurring with grant_interval: on_activation is invalid. Use one_time for a grant that fires only at activation.

If you leave it out: QuotaStack rejects the call. This field is required.

  • recurringFires every time grant_interval comes around, for as long as the subscription stays active.
  • one_timeFires exactly once, on grant_interval’s next turn, then never again.
  • trialFires once, meant for a subscription’s trial period.
sourceoptionalstring

Why this grant exists. QuotaStack stores it on every ledger entry the grant writes.

If you leave it out: By default, it is plan_grant.

  • plan_grantThe default. Use this for a grant tied to the variant’s normal schedule.
  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • migrationYou moved these from an old system.
Request
{
  "credits": 10000,
  "grant_interval": "monthly",
  "grant_type": "recurring",
  "source": "promotional",
  "rollover_percentage": 50,
  "accumulation_cap": 15000,
  "max_rollover_cycles": 3
}
Response 201
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a61",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "credits": 10000,
  "grant_interval": "monthly",
  "expires_after_seconds": null,
  "accumulation_cap": 15000,
  "rollover_percentage": 50,
  "max_rollover_cycles": 3,
  "grant_type": "recurring",
  "source": "promotional",
  "metadata": null,
  "created_at": "2026-07-20T09:10:00Z",
  "updated_at": "2026-07-20T09:10:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
creditsrequiredinteger (int64)

How many credits this grant issues each time it fires.

Unit: millicredits, where 1 credit is 1000 millicredits.

If you leave it out: QuotaStack rejects the call. This field is required, and must be more than zero.

grant_intervalrequiredstring

When this grant fires. Use one of the five keywords below, or an ISO 8601 duration like PT5H for a custom cadence that runs on its own clock, apart from billing. QuotaStack accepts nothing shorter than 5 minutes. See subscriptions for the full duration rules.

If you leave it out: QuotaStack rejects the call. This field is required.

expires_after_secondsoptionalinteger

How long each credit block from this grant lasts, counted from when it fires.

Unit: seconds.

If you leave it out: Left out, the block never expires.

accumulation_capoptionalinteger (int64)

The highest balance this grant is allowed to push the account to.

Unit: millicredits, where 1 credit is 1000 millicredits.

If you leave it out: Left out, there is no cap. The grant always adds its full amount.

rollover_percentageoptionalinteger0–100

How much unused credit from last cycle carries into this grant, from 0 to 100 percent. A prepaid renewal is what applies this.

Unit: percent.

If you leave it out: Left out, or 0, nothing carries forward.

max_rollover_cyclesoptionalinteger

How many cycles in a row credit can roll over. After that, QuotaStack resets the count to zero.

If you leave it out: Left out, rollover has no cycle limit.

grant_typerequiredstring

How often this grant fires overall. Pairing recurring with grant_interval: on_activation is invalid. Use one_time for a grant that fires only at activation.

If you leave it out: QuotaStack rejects the call. This field is required.

  • recurringFires every time grant_interval comes around, for as long as the subscription stays active.
  • one_timeFires exactly once, on grant_interval’s next turn, then never again.
  • trialFires once, meant for a subscription’s trial period.
sourcerequiredstring

Why this grant exists.

If you leave it out: By default, it is plan_grant.

  • plan_grantThe default. Use this for a grant tied to the variant’s normal schedule.
  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • migrationYou moved these from an old system.
metadatarequiredobject

QuotaStack always returns null here. There is no way to attach metadata to a credit grant through this API.

created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Retrieve a plan variant

GET/v1/plan-variants/{id}
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This endpoint reads /v1/plan-variants/{id} directly, a different path root from every other Plans call. This one needs no plan id at all. Use it when you already have a variant’s id from somewhere else, a subscription say, and only need that one variant. The reply never includes its credit grants — call listPlans with include_variants=true for those.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan variant’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "plan_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a11",
  "billing_cycle": "monthly",
  "trial_days": 0,
  "status": "active",
  "billing_mode": "prepaid",
  "renewal_due_days": 3,
  "grace_period_days": 3,
  "allow_usage_while_overdue": true,
  "metadata": {},
  "created_at": "2026-07-20T09:05:00Z",
  "updated_at": "2026-07-20T09:05:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
plan_idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
billing_cyclerequiredstring

How often this variant renews. Fixed forever once the variant is created.

  • dailyRenews once a day.
  • weeklyRenews once a week.
  • monthlyRenews on the same day each month.
  • yearlyRenews on the same day each year.
  • one_timeNever renews. One purchase, and nothing repeats after it.
trial_daysrequiredinteger0–∞

How many days a new subscription on this variant spends in trialing before its first charge.

statusrequiredstring

This variant is active, or it is retired.

  • activeThe normal state.
  • retiredA lifecycle flag only. This variant keeps its subscriptions, grants, and entitlements exactly as they were.
billing_moderequiredstring

Who advances the billing period. Fixed forever once the variant is created.

  • prepaidYou call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaidQuotaStack moves the period forward on its own. QuotaStack then reports usage back to you, to invoice.
renewal_due_daysrequiredinteger

Days before the period ends that QuotaStack warns you to renew. Prepaid only.

grace_period_daysrequiredinteger

How many days late a prepaid plan can go before it ends.

allow_usage_while_overduerequiredboolean

Whether a customer can spend credit while their bill is late.

metadatarequiredobject

Your own key-value tags, unchanged since the last write.

created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Errors

  • 404No variant here has this id. not-found

List variant entitlements

GET/v1/plans/{id}/variants/{vid}/entitlements
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This endpoint lists every entitlement tied to one variant. There is no paging; every row comes back in one call. A missing variant returns 404. openapi.yaml declares only 200 here.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

vidrequiredstring (uuid)

The plan variant’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Response 200
{
  "data": [
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a62",
      "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
      "billable_metric_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a63",
      "billable_metric_key": "sso",
      "value": {
        "enabled": true
      },
      "created_at": "2026-07-20T09:15:00Z",
      "updated_at": "2026-07-20T09:15:00Z"
    }
  ]
}
Response fields
FieldTypeMeaning
datarequiredarray

One row per attached metric.

data.billable_metric_keyoptionalstring

The billable metric’s own key, the same one you used to attach it. QuotaStack fills this in on its own; you cannot set it.

data.valuerequiredobject

The stored value, shaped by the metric’s type. boolean reads {"enabled": ...}. gauge reads {"cap": ...}. static reads any object. metered usually reads {}.

Attach a variant entitlement

POST/v1/plans/{id}/variants/{vid}/entitlements
Idempotency-KeyNot required for this operation.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne new entitlement attachment on this variant.
Fires No webhooks.

An entitlement attachment grants a feature or a cap that credits do not price: a flag, a cap, or a config blob. Use a metering rule instead for a feature that should cost credits per use. This call needs an Idempotency-Key header, like every write. openapi.yaml skips it here, but QuotaStack still checks.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

vidrequiredstring (uuid)

The plan variant’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Body parameters
FieldTypeMeaning
billable_metric_keyrequiredstring

Which billable metric this grants, by its key.

If you leave it out: QuotaStack rejects the call. This field is required — an empty or unknown key returns 404, not 422.

valuerequiredobject

The value this attachment grants, shaped by the metric’s type. boolean takes {"enabled": <true|false>}. gauge takes {"cap": <non-negative integer>}. static takes any object, often {"config": ...}. metered ignores this field.

If you leave it out: For a metered or static metric, you can leave value out. QuotaStack then stores {} as the value. For boolean or gauge, a bad or missing value returns 500, not the documented 422.

Request
{
  "billable_metric_key": "sso",
  "value": {
    "enabled": true
  }
}
Response 201
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a62",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "billable_metric_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a63",
  "billable_metric_key": "sso",
  "value": {
    "enabled": true
  },
  "created_at": "2026-07-20T09:15:00Z",
  "updated_at": "2026-07-20T09:15:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
billable_metric_idrequiredstring (uuid)
billable_metric_keyoptionalstring

The billable metric’s own key, the same one you used to attach it. QuotaStack fills this in on its own; you cannot set it.

valuerequiredobject

The stored value, shaped by the metric’s type. boolean reads {"enabled": ...}. gauge reads {"cap": ...}. static reads any object. metered usually reads {}.

created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Errors

  • 404No plan variant here has this id, or no billable metric has this billable_metric_key. not-found
  • 409This metric is already attached to this variant. Use updateVariantEntitlement to change its value. conflict
  • 422For a boolean metric, value must carry enabled as a real boolean. For a gauge metric, value must carry a non-negative whole-number cap. See value’s own prose below — this status does not always fire the way this row implies. validation-error

Update a variant entitlement

PATCH/v1/plans/{id}/variants/{vid}/entitlements/{billable_metric_key}
Idempotency-KeyNot required for this operation.
EnvironmentSandbox and Live
AuthTenant API key
WritesReplaces the attachment’s value. See value’s own prose below for what happens when you leave it out.
Fires No webhooks.

This endpoint swaps an attached metric’s value for a new one — no merge. This call needs an Idempotency-Key header, like every write. openapi.yaml skips it here, but QuotaStack still checks. You cannot change the metric itself; detach and reattach instead.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

vidrequiredstring (uuid)

The plan variant’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

billable_metric_keyrequiredstring

The billable metric’s own key, the same one you used to attach it.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Body parameters
FieldTypeMeaning
valuerequiredobject

The value this attachment grants, shaped by the metric’s type. boolean takes {"enabled": <true|false>}. gauge takes {"cap": <non-negative integer>}. static takes any object, often {"config": ...}. metered ignores this field.

If you leave it out: For a metered or static metric, you can leave value out. QuotaStack then quietly replaces the stored value with {}. For boolean or gauge, a bad or missing value returns 500, not the documented 422.

Request
{
  "value": {
    "enabled": false
  }
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a62",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "billable_metric_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a63",
  "billable_metric_key": "sso",
  "value": {
    "enabled": false
  },
  "created_at": "2026-07-20T09:15:00Z",
  "updated_at": "2026-07-20T09:15:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
billable_metric_idrequiredstring (uuid)
billable_metric_keyoptionalstring

The billable metric’s own key, the same one you used to attach it. QuotaStack fills this in on its own; you cannot set it.

valuerequiredobject

The stored value, shaped by the metric’s type. boolean reads {"enabled": ...}. gauge reads {"cap": ...}. static reads any object. metered usually reads {}.

created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

Errors

  • 404No plan variant here has this id, or no metric is attached under this billable_metric_key. not-found
  • 422For a boolean metric, value must carry enabled as a real boolean. For a gauge metric, value must carry a non-negative whole-number cap. See value’s own prose below — this status does not always fire the way this row implies. validation-error

Detach a variant entitlement

DELETE/v1/plans/{id}/variants/{vid}/entitlements/{billable_metric_key}
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesSoft-deletes the attachment. The metric stays. Every other variant that has it stays too.
Fires No webhooks.

This endpoint is a soft delete. QuotaStack marks the attachment deleted and nothing else. Subscribers on this variant fall back to the metric’s own default_value right after.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The plan’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

vidrequiredstring (uuid)

The plan variant’s UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

billable_metric_keyrequiredstring

The billable metric’s own key, the same one you used to attach it.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Errors

  • 404No plan variant here has this id, or no metric is attached under this billable_metric_key. not-found