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 tier → POST /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 schedule → POST /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 price → POST /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 response → GET /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 variant → GET /v1/plan-variants/{id}
    Reads /v1/plan-variants/{id} directly, with no plan id needed. listPlanVariants needs the parent plan’s id too.
14 of 14 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.

  • active — The normal state. Nothing here blocks a new variant or a new subscription.
  • retired — A 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.

  • active — The normal state. Nothing here blocks a new variant or a new subscription.
  • retired — A 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.

  • daily — Renews once a day.
  • weekly — Renews once a week.
  • monthly — Renews on the same day each month.
  • yearly — Renews on the same day each year.
  • one_time — Never renews. One purchase, and nothing repeats after it.
data.variants.statusrequiredstring

This variant is active, or it is retired.

  • active — The normal state.
  • retired — A 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.

  • prepaid — You call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaid — QuotaStack 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 five keywords, or an ISO 8601 duration like PT5H. daily fires every 24 hours. weekly fires every 7 days. monthly fires once a calendar month, on the day the subscription started. A start on the 30th fires on 28 February. March goes back to the 30th. on_activation fires once, at the start. billing_cycle is the only value that waits for billing. The rest run on their own clock. So a yearly variant can grant credit every month. The shortest value allowed is 5 minutes. A duration may use days, hours, minutes and seconds. Write P7D, not P1W.

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.

  • recurring — Fires every time grant_interval comes around, for as long as the subscription stays active.
  • one_time — Fires exactly once, on grant_interval’s next turn, then never again.
  • trial — Fires 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_grant — The default. Use this for a grant tied to the variant’s normal schedule.
  • promotional — A free gift. Maybe a welcome bonus, or an ad you ran.
  • compensation — You are making something right. An outage, or a support case.
  • referral — A customer sent a friend your way.
  • manual — A person added these by hand.
  • migration — You 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.

  • active — The normal state. Nothing here blocks a new variant or a new subscription.
  • retired — A 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

  • 409 — State conflict (e.g., duplicate resource, stale version). On any route that requires Idempotency-Key, a 409 here can also mean a concurrent duplicate request under the same key is still in flight: the duplicate waited up to 10 seconds for the original to finish and gave up. That case carries a Retry-After header — retry after that many seconds; the original has by then either completed (so the retry replays it) or failed, in which case the retry runs itself.
  • 422 — name 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.

  • active — The normal state. Nothing here blocks a new variant or a new subscription.
  • retired — A 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

  • 404 — No 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.

  • active — The normal state. Nothing here blocks a new variant or a new subscription.
  • retired — A 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.

  • active — The normal state. Nothing here blocks a new variant or a new subscription.
  • retired — A 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

  • 409 — State conflict (e.g., duplicate resource, stale version). On any route that requires Idempotency-Key, a 409 here can also mean a concurrent duplicate request under the same key is still in flight: the duplicate waited up to 10 seconds for the original to finish and gave up. That case carries a Retry-After header — retry after that many seconds; the original has by then either completed (so the retry replays it) or failed, in which case the retry runs itself.

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.

  • daily — Renews once a day.
  • weekly — Renews once a week.
  • monthly — Renews on the same day each month.
  • yearly — Renews on the same day each year.
  • one_time — Never renews. One purchase, and nothing repeats after it.
data.statusrequiredstring

This variant is active, or it is retired.

  • active — The normal state.
  • retired — A 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.

  • prepaid — You call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaid — QuotaStack 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.

  • daily — Renews once a day.
  • weekly — Renews once a week.
  • monthly — Renews on the same day each month.
  • yearly — Renews on the same day each year.
  • one_time — Never 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.

  • prepaid — You call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaid — QuotaStack 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.

  • daily — Renews once a day.
  • weekly — Renews once a week.
  • monthly — Renews on the same day each month.
  • yearly — Renews on the same day each year.
  • one_time — Never 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.

  • active — The normal state.
  • retired — A 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.

  • prepaid — You call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaid — QuotaStack 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

  • 409 — State conflict (e.g., duplicate resource, stale version). On any route that requires Idempotency-Key, a 409 here can also mean a concurrent duplicate request under the same key is still in flight: the duplicate waited up to 10 seconds for the original to finish and gave up. That case carries a Retry-After header — retry after that many seconds; the original has by then either completed (so the retry replays it) or failed, in which case the retry runs itself.

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.

  • active — The normal state.
  • retired — A 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.

  • daily — Renews once a day.
  • weekly — Renews once a week.
  • monthly — Renews on the same day each month.
  • yearly — Renews on the same day each year.
  • one_time — Never 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.

  • active — The normal state.
  • retired — A 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.

  • prepaid — You call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaid — QuotaStack 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

  • 409 — State conflict (e.g., duplicate resource, stale version). On any route that requires Idempotency-Key, a 409 here can also mean a concurrent duplicate request under the same key is still in flight: the duplicate waited up to 10 seconds for the original to finish and gave up. That case carries a Retry-After header — retry after that many seconds; the original has by then either completed (so the retry replays it) or failed, in which case the retry runs itself.

List a plan variant’s credit grants

GET/v1/plans/{id}/variants/{vid}/grants
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
Fires No webhooks.

Every grant schedule on this variant, oldest first. Read it to see what a subscriber gets, and how often. A variant with no grants returns an empty data array. Then the plan grants no credit at all. A variant from a different plan returns 404. Both ids in the path must point at the same plan.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)
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-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
datarequiredarray

Errors

  • 404 — No variant here has this vid, or it belongs to another plan. not-found

See also

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, starting when a subscription begins. Only grant_interval: billing_cycle waits for a renewal; every other value runs on its own clock. A 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 five keywords, or an ISO 8601 duration like PT5H. daily fires every 24 hours. weekly fires every 7 days. monthly fires once a calendar month, on the day the subscription started. A start on the 30th fires on 28 February. March goes back to the 30th. on_activation fires once, at the start. billing_cycle is the only value that waits for billing. The rest run on their own clock. So a yearly variant can grant credit every month. The shortest value allowed is 5 minutes. A duration may use days, hours, minutes and seconds. Write P7D, not P1W.

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. Set it and each new grant expires the last block from this rule first. So one grant never leaves two live blocks.

Unit: seconds.

If you leave it out: Left out, the block never expires and credits from this grant build up.

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.

  • recurring — Fires every time grant_interval comes around, for as long as the subscription stays active.
  • one_time — Fires exactly once, on grant_interval’s next turn, then never again.
  • trial — Fires 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_grant — The default. Use this for a grant tied to the variant’s normal schedule.
  • promotional — A free gift. Maybe a welcome bonus, or an ad you ran.
  • compensation — You are making something right. An outage, or a support case.
  • referral — A customer sent a friend your way.
  • manual — A person added these by hand.
  • migration — You 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 five keywords, or an ISO 8601 duration like PT5H. daily fires every 24 hours. weekly fires every 7 days. monthly fires once a calendar month, on the day the subscription started. A start on the 30th fires on 28 February. March goes back to the 30th. on_activation fires once, at the start. billing_cycle is the only value that waits for billing. The rest run on their own clock. So a yearly variant can grant credit every month. The shortest value allowed is 5 minutes. A duration may use days, hours, minutes and seconds. Write P7D, not P1W.

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. Set it and each new grant expires the last block from this rule first. So one grant never leaves two live blocks.

Unit: seconds.

If you leave it out: Left out, the block never expires and credits from this grant build up.

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.

  • recurring — Fires every time grant_interval comes around, for as long as the subscription stays active.
  • one_time — Fires exactly once, on grant_interval’s next turn, then never again.
  • trial — Fires 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_grant — The default. Use this for a grant tied to the variant’s normal schedule.
  • promotional — A free gift. Maybe a welcome bonus, or an ad you ran.
  • compensation — You are making something right. An outage, or a support case.
  • referral — A customer sent a friend your way.
  • manual — A person added these by hand.
  • migration — You 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)

Errors

  • 409 — State conflict (e.g., duplicate resource, stale version). On any route that requires Idempotency-Key, a 409 here can also mean a concurrent duplicate request under the same key is still in flight: the duplicate waited up to 10 seconds for the original to finish and gave up. That case carries a Retry-After header — retry after that many seconds; the original has by then either completed (so the retry replays it) or failed, in which case the retry runs itself.

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.

  • daily — Renews once a day.
  • weekly — Renews once a week.
  • monthly — Renews on the same day each month.
  • yearly — Renews on the same day each year.
  • one_time — Never 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.

  • active — The normal state.
  • retired — A 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.

  • prepaid — You call renew after you charge the customer. QuotaStack waits for that call before it moves the period forward.
  • postpaid — QuotaStack 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

  • 404 — No 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

  • 404 — No plan variant here has this id, or no billable metric has this billable_metric_key. not-found
  • 409 — This metric is already attached to this variant. Use updateVariantEntitlement to change its value. conflict
  • 422 — For 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-KeyRequired. Returns 422 without it.
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.

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.

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

  • 404 — No plan variant here has this id, or no metric is attached under this billable_metric_key. not-found
  • 409 — State conflict (e.g., duplicate resource, stale version). On any route that requires Idempotency-Key, a 409 here can also mean a concurrent duplicate request under the same key is still in flight: the duplicate waited up to 10 seconds for the original to finish and gave up. That case carries a Retry-After header — retry after that many seconds; the original has by then either completed (so the retry replays it) or failed, in which case the retry runs itself.
  • 422 — For 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

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