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}/variantsA 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}/grantsThis 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}/entitlementsUse 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/plansAddinclude_variants=trueto see them.getPlanalone never returns variants;listPlanVariantsalone never returns grants. - You already have a plan variant’s
idand only need that one variant →GET /v1/plan-variants/{id}Reads/v1/plan-variants/{id}directly, with no planidneeded.listPlanVariantsneeds the parent plan’sidtoo.
List plans
/v1/plans | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. 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.
| Field | Type | Meaning |
|---|---|---|
statusoptional | string | Only plans in this state. If you leave it out: QuotaStack returns only
|
include_variantsoptional | boolean | Nest each plan’s variants, and each variant’s credit grants, in the response. If you leave it out: Left out, |
cursoroptional | string | Opaque pagination cursor from the previous page's |
limitoptional | integer | Page size. Default 20, max 100. |
{
"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
}
} | Field | Type | Meaning |
|---|---|---|
datarequired | array | One row per plan, newest first. |
data.namerequired | string | The tier’s name, shown in the dashboard and in every read. |
data.is_defaultrequired | boolean | Whether new customers land on this plan when nothing else applies. |
data.statusrequired | string | This plan is
|
data.variantsrequired | array | Every variant under this plan. Empty unless you sent |
data.variants.billing_cyclerequired | string | How often this variant renews. Fixed forever once the variant is created.
|
data.variants.statusrequired | string | This variant is
|
data.variants.billing_moderequired | string | Who advances the billing period. Fixed forever once the variant is created.
|
data.variants.grantsoptional | array | Every credit grant tied to this variant. |
data.variants.grants.creditsrequired | integer (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_intervalrequired | string | When this grant fires. Use one of the five keywords below, or an ISO 8601 duration like If you leave it out: QuotaStack rejects the call. This field is required. |
data.variants.grants.grant_typerequired | string | How often this grant fires overall. Pairing If you leave it out: QuotaStack rejects the call. This field is required.
|
data.variants.grants.sourcerequired | string | Why this grant exists. If you leave it out: By default, it is
|
data.variants.grants.metadatarequired | object | QuotaStack always returns |
paginationrequired | object | Cursor-based pagination envelope. Returned on every list endpoint. No |
See also
Create a plan
/v1/plans | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | One 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.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
namerequired | string | The tier’s name. If you leave it out: QuotaStack rejects the call. This field is required. |
is_defaultoptional | boolean | 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 |
metadataoptional | object | Your own key-value tags. QuotaStack stores them unchanged and returns them on every read. If you leave it out: Left out, QuotaStack stores |
{
"name": "Pro"
} {
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
namerequired | string | The tier’s name, shown in the dashboard and in every read. |
is_defaultrequired | boolean | Whether new customers land on this plan when nothing else applies. |
statusrequired | string | This plan is
|
metadatarequired | object | |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
Errors
422—nameis missing or empty. The response names the field. validation-error
Retrieve a plan
/v1/plans/{id} | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. 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.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
{
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
namerequired | string | The tier’s name, shown in the dashboard and in every read. |
is_defaultrequired | boolean | Whether new customers land on this plan when nothing else applies. |
statusrequired | string | This plan is
|
metadatarequired | object | |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
Errors
404— No plan here has thisid. not-found
See also
Update a plan
/v1/plans/{id} | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Replaces 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.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
| Field | Type | Meaning |
|---|---|---|
nameoptional | string | The tier’s new name. Unlike a customer’s If you leave it out: The old value stays. QuotaStack changes nothing. |
statusoptional | string | This plan’s new lifecycle state. If you leave it out: The old value stays. QuotaStack changes nothing.
|
is_defaultoptional | boolean | Set If you leave it out: The old value stays. QuotaStack changes nothing. |
metadataoptional | object | 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. |
{
"is_default": true
} {
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
namerequired | string | The tier’s name, shown in the dashboard and in every read. |
is_defaultrequired | boolean | Whether new customers land on this plan when nothing else applies. |
statusrequired | string | This plan is
|
metadatarequired | object | |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
See also
List plan variants
/v1/plans/{id}/variants | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. 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.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
{
"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"
}
]
} | Field | Type | Meaning |
|---|---|---|
datarequired | array | One row per variant, oldest first. Both active and retired variants come back. |
data.billing_cyclerequired | string | How often this variant renews. Fixed forever once the variant is created.
|
data.statusrequired | string | This variant is
|
data.billing_moderequired | string | Who advances the billing period. Fixed forever once the variant is created.
|
See also
Create a plan variant
/v1/plans/{id}/variants | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | One 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.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
| Field | Type | Meaning |
|---|---|---|
billing_cyclerequired | string | How often this variant renews. If you leave it out: QuotaStack rejects the call. This field is required.
|
trial_daysrequired | integer0–∞ | How many days a new subscription spends in 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_modeoptional | string | Who advances the billing period. Fixed forever once you create the variant. If you leave it out: By default, it is
|
renewal_due_daysoptional | integer | 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_daysoptional | integer | 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_overdueoptional | boolean | Whether a customer can spend credit while their bill is late. If you leave it out: By default, it is |
metadataoptional | object | Your own key-value tags. QuotaStack stores them unchanged and returns them on every read. If you leave it out: Left out, QuotaStack stores |
{
"billing_cycle": "monthly",
"trial_days": 14
} {
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
plan_idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
billing_cyclerequired | string | How often this variant renews. Fixed forever once the variant is created.
|
trial_daysrequired | integer0–∞ | How many days a new subscription on this variant spends in |
statusrequired | string | This variant is
|
billing_moderequired | string | Who advances the billing period. Fixed forever once the variant is created.
|
renewal_due_daysrequired | integer | Days before the period ends that QuotaStack warns you to renew. Prepaid only. |
grace_period_daysrequired | integer | How many days late a prepaid plan can go before it ends. |
allow_usage_while_overduerequired | boolean | Whether a customer can spend credit while their bill is late. |
metadatarequired | object | Your own key-value tags, unchanged since the last write. |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
Update a plan variant
/v1/plans/{id}/variants/{vid} | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Replaces 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.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
vidrequired | string (uuid) | The plan variant’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
| Field | Type | Meaning |
|---|---|---|
statusoptional | string | This variant’s new state: If you leave it out: The old value stays. QuotaStack changes nothing.
|
trial_daysoptional | integer | How many trial days this variant grants a new subscription. If you leave it out: The old value stays. QuotaStack changes nothing. |
metadataoptional | object | 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. |
{
"status": "retired"
} {
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
plan_idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
billing_cyclerequired | string | How often this variant renews. Fixed forever once the variant is created.
|
trial_daysrequired | integer0–∞ | How many days a new subscription on this variant spends in |
statusrequired | string | This variant is
|
billing_moderequired | string | Who advances the billing period. Fixed forever once the variant is created.
|
renewal_due_daysrequired | integer | Days before the period ends that QuotaStack warns you to renew. Prepaid only. |
grace_period_daysrequired | integer | How many days late a prepaid plan can go before it ends. |
allow_usage_while_overduerequired | boolean | Whether a customer can spend credit while their bill is late. |
metadatarequired | object | Your own key-value tags, unchanged since the last write. |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
See also
Attach a credit grant to a plan variant
/v1/plans/{id}/variants/{vid}/grants | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | One 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.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
vidrequired | string (uuid) | The plan variant’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
| Field | Type | Meaning |
|---|---|---|
creditsrequired | integer (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_intervalrequired | string | When this grant fires. Use one of the five keywords below, or an ISO 8601 duration like If you leave it out: QuotaStack rejects the call. This field is required. |
expires_after_secondsoptional | integer | 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_capoptional | integer (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_percentageoptional | integer0–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_cyclesoptional | integer | 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_typerequired | string | How often this grant fires overall. Pairing If you leave it out: QuotaStack rejects the call. This field is required.
|
sourceoptional | string | Why this grant exists. QuotaStack stores it on every ledger entry the grant writes. If you leave it out: By default, it is
|
{
"credits": 10000,
"grant_interval": "monthly",
"grant_type": "recurring",
"source": "promotional",
"rollover_percentage": 50,
"accumulation_cap": 15000,
"max_rollover_cycles": 3
} {
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
plan_variant_idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
creditsrequired | integer (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_intervalrequired | string | When this grant fires. Use one of the five keywords below, or an ISO 8601 duration like If you leave it out: QuotaStack rejects the call. This field is required. |
expires_after_secondsoptional | integer | 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_capoptional | integer (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_percentageoptional | integer0–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_cyclesoptional | integer | 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_typerequired | string | How often this grant fires overall. Pairing If you leave it out: QuotaStack rejects the call. This field is required.
|
sourcerequired | string | Why this grant exists. If you leave it out: By default, it is
|
metadatarequired | object | QuotaStack always returns |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
See also
Retrieve a plan variant
/v1/plan-variants/{id} | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. 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.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan variant’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
{
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
plan_idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
billing_cyclerequired | string | How often this variant renews. Fixed forever once the variant is created.
|
trial_daysrequired | integer0–∞ | How many days a new subscription on this variant spends in |
statusrequired | string | This variant is
|
billing_moderequired | string | Who advances the billing period. Fixed forever once the variant is created.
|
renewal_due_daysrequired | integer | Days before the period ends that QuotaStack warns you to renew. Prepaid only. |
grace_period_daysrequired | integer | How many days late a prepaid plan can go before it ends. |
allow_usage_while_overduerequired | boolean | Whether a customer can spend credit while their bill is late. |
metadatarequired | object | Your own key-value tags, unchanged since the last write. |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
Errors
404— No variant here has thisid. not-found
See also
List variant entitlements
/v1/plans/{id}/variants/{vid}/entitlements | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. 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.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
vidrequired | string (uuid) | The plan variant’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
{
"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"
}
]
} | Field | Type | Meaning |
|---|---|---|
datarequired | array | One row per attached metric. |
data.billable_metric_keyoptional | string | The billable metric’s own |
data.valuerequired | object | The stored value, shaped by the metric’s |
See also
Attach a variant entitlement
/v1/plans/{id}/variants/{vid}/entitlements | Idempotency-Key | Not required for this operation. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | One 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.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
vidrequired | string (uuid) | The plan variant’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
| Field | Type | Meaning |
|---|---|---|
billable_metric_keyrequired | string | Which billable metric this grants, by its If you leave it out: QuotaStack rejects the call. This field is required — an empty or unknown key returns |
valuerequired | object | The value this attachment grants, shaped by the metric’s If you leave it out: For a |
{
"billable_metric_key": "sso",
"value": {
"enabled": true
}
} {
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
plan_variant_idrequired | string (uuid) | |
billable_metric_idrequired | string (uuid) | |
billable_metric_keyoptional | string | The billable metric’s own |
valuerequired | object | The stored value, shaped by the metric’s |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
Errors
404— No plan variant here has thisid, or no billable metric has thisbillable_metric_key. not-found409— This metric is already attached to this variant. UseupdateVariantEntitlementto change its value. conflict422— For abooleanmetric,valuemust carryenabledas a real boolean. For agaugemetric,valuemust carry a non-negative whole-numbercap. Seevalue’s own prose below — this status does not always fire the way this row implies. validation-error
Update a variant entitlement
/v1/plans/{id}/variants/{vid}/entitlements/{billable_metric_key} | Idempotency-Key | Not required for this operation. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Replaces 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.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
vidrequired | string (uuid) | The plan variant’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
billable_metric_keyrequired | string | The billable metric’s own If you leave it out: QuotaStack rejects the call. This path segment is required. |
| Field | Type | Meaning |
|---|---|---|
valuerequired | object | The value this attachment grants, shaped by the metric’s If you leave it out: For a |
{
"value": {
"enabled": false
}
} {
"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"
} | Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
plan_variant_idrequired | string (uuid) | |
billable_metric_idrequired | string (uuid) | |
billable_metric_keyoptional | string | The billable metric’s own |
valuerequired | object | The stored value, shaped by the metric’s |
created_atrequired | string (date-time) | |
updated_atrequired | string (date-time) |
Errors
404— No plan variant here has thisid, or no metric is attached under thisbillable_metric_key. not-found422— For abooleanmetric,valuemust carryenabledas a real boolean. For agaugemetric,valuemust carry a non-negative whole-numbercap. Seevalue’s own prose below — this status does not always fire the way this row implies. validation-error
Detach a variant entitlement
/v1/plans/{id}/variants/{vid}/entitlements/{billable_metric_key} | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Soft-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.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | The plan’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
vidrequired | string (uuid) | The plan variant’s UUID. If you leave it out: QuotaStack rejects the call. This path segment is required. |
billable_metric_keyrequired | string | The billable metric’s own If you leave it out: QuotaStack rejects the call. This path segment is required. |
Errors
404— No plan variant here has thisid, or no metric is attached under thisbillable_metric_key. not-found
Loading…