quotastack Docs
Docs / API / Subscriptions

Subscriptions

A customer on a plan, over time.

A subscription puts one customer on one plan, over time. The subscription tracks the billing period, and it grants credits again at renewal.

Subscriptions are optional. A customer can buy credits with a one-time topup instead, with no subscription at all.

Which call do I want?

  • You are putting a customer on a plan for the first timePOST /v1/subscriptions
    createSubscription grants the plan variant's credits in the same call. There is no separate grant step.
  • The customer is moving to a more expensive variantPOST /v1/subscriptions/{id}/upgrade
  • The customer is moving to a cheaper variantPOST /v1/subscriptions/{id}/downgrade
    Upgrade grants prorated credit right away. Downgrade grants nothing until the next renewal. The two are not mirror images.
  • The customer is stopping, but may come backPOST /v1/subscriptions/{id}/pause
    Existing credit stays spendable. The billing period stops moving too. Sending resume_at does nothing. Nothing brings it back on its own — call resumeSubscription.
  • The customer is stopping for goodPOST /v1/subscriptions/{id}/cancel
    Pause freezes the period and keeps the account open. Cancel ends it, right away or at period end, and never comes back on its own.
  • A paused subscription is coming backPOST /v1/subscriptions/{id}/resume
    Restarts the billing period from right now. Whatever time was left when it paused is gone.
  • You are advancing a manual prepaid subscription yourselfPOST /v1/subscriptions/{id}/renew
    Use this call when old credit can roll over. Postpaid plans move on their own. A payment connector moves after a paid charge.
  • You only need to push out a contract's end datePOST /v1/subscriptions/{id}/extend
    This call touches contract_end alone. Nothing here moves the billing period or grants credit — use renewSubscription for that.
10 of 10 operations documented in full.

List subscriptions

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

This call lists every subscription for your tenant, newest first. Filter by customer_id, status, or both — both are optional. An unrecognized customer_id returns an empty list, not 404. An unrecognized status is different. QuotaStack does not check it first. The query then fails at the database with 500, since status is a fixed set of values there, not free text.

Query parameters
FieldTypeMeaning
customer_idoptionalstring

Only subscriptions for this customer. Takes your own external_id or QuotaStack's own id. An unrecognized value returns an empty list, not 404.

If you leave it out: Every subscription comes back, with no filter.

statusoptionalstring

Only subscriptions in this state. QuotaStack does not check it against the eight real values. An unknown value returns 500.

If you leave it out: Every status comes back, with no filter.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
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-4f6c8e1b3a21",
      "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
      "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
      "environment": "live",
      "status": "active",
      "origin": "api",
      "started_at": "2026-07-01T09:00:00Z",
      "trial_ends_at": null,
      "current_period_start": "2026-07-01T09:00:00Z",
      "current_period_end": "2026-08-01T09:00:00Z",
      "billing_anchor": 1,
      "cancel_at_period_end": false,
      "scheduled_variant_id": null,
      "external_subscription_id": null,
      "paused_at": null,
      "resumed_at": null,
      "expired_at": null,
      "contract_start": "2026-07-01T09:00:00Z",
      "contract_end": null,
      "contract_ending_soon_days": 0,
      "metadata": {},
      "created_at": "2026-07-01T09:00:00Z",
      "updated_at": "2026-07-01T09:00:00Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
Response fields
FieldTypeMeaning
datarequiredarray
data.statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
data.originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
data.trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

data.current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

data.current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

data.billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

data.cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

data.scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

data.external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

data.paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

data.resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

data.expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

data.contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

data.contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

data.contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

data.metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

paginationrequiredobject

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

See also

Create a subscription

POST/v1/subscriptions
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne new subscription. QuotaStack also grants the plan variant's credits in the same call, best-effort. The grant never applies an accumulation_cap or rollover_percentage, even if the variant's grant defines one — only a renewal does.
Fires subscription.created, credit.granted

This call puts one customer on one plan variant, starting now. QuotaStack grants that variant's credits in the same call. A failed grant does not fail the subscription itself. A trial variant starts the subscription as trialing. Every other variant starts it active. Send customer_id or external_customer_id, never both. QuotaStack rejects the call with neither too. external_customer_id looks up, or creates, a customer by that value alone. customer_id is the lenient form: it also takes your own external ID, and a UUID from a different tenant returns 400. The reply never says how many credits the call granted — check the customer's balance or ledger history for that. Skip this call for a customer already mid-plan somewhere else — it starts the period now and grants the full activation credits again. Use POST /v1/imports/customers for that case 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}.

Body parameters
FieldTypeMeaning
customer_idoptionalstring

QuotaStack's own id for this customer. Send this or external_customer_id, never both.

If you leave it out: Send external_customer_id instead. QuotaStack rejects the call with neither.

external_customer_idoptionalstring

Your own id for this customer. Looked up, or created, strictly by this value — no UUID fallback.

If you leave it out: Send customer_id instead. QuotaStack rejects the call with neither.

plan_variant_idrequiredstring (uuid)

Which plan variant to subscribe the customer to.

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

billing_anchoroptionalinteger

The day of the month, 1 through 31, a monthly period should renew on.

If you leave it out: QuotaStack uses today's day of the month. An out-of-range value falls back to the same default.

external_subscription_idoptionalstring

Your own identifier for this subscription. QuotaStack stores it unchanged.

If you leave it out: Left null.

contract_endoptionalstring (date-time)

When the contract ends.

If you leave it out: The subscription has no contract end, and never reaches contract_ended on its own.

contract_ending_soon_daysoptionalinteger

Days before contract_end that QuotaStack should fire subscription.contract_ending_soon.

If you leave it out: QuotaStack stores 0, not 30. The scheduler treats 0 the same as 30 when it checks the reminder, so the webhook still fires on time.

metadataoptionalobject

Your own key-value tags, stored as the subscription's starting value.

If you leave it out: An empty object is stored instead.

Request
{
  "external_customer_id": "user_42",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "contract_end": "2027-07-01T00:00:00Z"
}
Response 201
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {},
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

Errors

  • 400Sending both customer_id and external_customer_id, or neither, returns this. So does a customer_id UUID that belongs to a different tenant. bad-request
  • 401Missing or invalid authentication.
  • 404No plan variant here has this plan_variant_id. not-found
  • 422A missing plan_variant_id returns this, under that name. A plan_variant_id for a retired variant returns this too, under variant-not-active instead. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Retrieve a subscription

GET/v1/subscriptions/{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 call returns one subscription by its own id. The lookup is environment-scoped. A sandbox key resolves only a sandbox subscription. A live key resolves only a live one. Add include_plan=true to also embed the plan variant and its credit grants. openapi.yaml does not declare that parameter, though QuotaStack accepts it.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The subscription's own UUID. Not a customer id.

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

Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {},
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

Errors

  • 404No subscription here has this id. Passing a customer's own id instead also returns this — this path only takes a subscription's own id. So does a sandbox key naming a live subscription, or a live key naming a sandbox one. not-found

Upgrade a subscription

POST/v1/subscriptions/{id}/upgrade
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesChanges plan_variant_id right away and clears scheduled_variant_id. Grants prorated credit for the rest of this period. No accumulation_cap or rollover applies.
Fires subscription.upgraded, credit.granted

This call switches plan_variant_id right away. The subscription keeps its current status. This call also clears any downgrade scheduled by downgradeSubscription, canceling that schedule outright, not merging with it. QuotaStack grants prorated credit for the rest of the current period, best-effort, in the same call. The amount ignores any accumulation_cap or rollover_percentage the new variant's grant defines; only a renewal honors those. Only an active or trialing subscription can upgrade. Any other status returns 409, a status openapi.yaml does not declare here. This reply is not the full Subscription object the field table below describes. The real reply adds credits_granted, in millicredits, and drops origin, paused_at, resumed_at, expired_at, contract_start, contract_end, and contract_ending_soon_days.

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 subscription's own UUID. Not a customer id.

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

Body parameters
FieldTypeMeaning
plan_variant_idrequiredstring (uuid)

The plan variant to switch to, right away.

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

metadataoptionalobject

QuotaStack accepts this field and stores nothing from it. Upgrade never reads it.

If you leave it out: No difference. This field has no effect either way.

Request
{
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12"
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "started_at": "2026-06-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T00:00:00Z",
  "current_period_end": "2026-08-01T00:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "credits_granted": 6774,
  "metadata": {},
  "created_at": "2026-06-01T09:00:00Z",
  "updated_at": "2026-07-15T14:22:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

Errors

  • 400QuotaStack could not parse the request body as JSON. bad-request
  • 404No subscription here has this id, or no plan variant has this plan_variant_id. not-found
  • 422A missing plan_variant_id returns this, under that name. A retired target variant returns this too, under variant-not-active instead. validation-error

Downgrade a subscription

POST/v1/subscriptions/{id}/downgrade
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesSets scheduled_variant_id. plan_variant_id and the account balance stay as they were until the next renewal applies the switch.
Fires subscription.downgraded

This call schedules a variant switch for the end of the current period. plan_variant_id on the subscription does not change now — only scheduled_variant_id does. QuotaStack grants no credit here. The new variant's credits wait for the renewal that applies the switch. Only an active or trialing subscription can downgrade. Any other status returns 409, also undeclared 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 subscription's own UUID. Not a customer id.

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

Body parameters
FieldTypeMeaning
plan_variant_idrequiredstring (uuid)

The plan variant to switch to at the next renewal.

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

metadataoptionalobject

QuotaStack accepts this field and stores nothing from it. Downgrade never reads it.

If you leave it out: No difference. This field has no effect either way.

Request
{
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12"
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {},
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

Errors

  • 404No subscription here has this id, or no plan variant has this plan_variant_id. not-found
  • 422A missing plan_variant_id returns this, under that name. A retired target variant returns this too, under variant-not-active instead. validation-error

Cancel a subscription

POST/v1/subscriptions/{id}/cancel
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesSets status to canceled (immediate) or cancelling (default) and merges metadata, adding reason under cancel_reason when sent. Grants and takes no credit.
Fires subscription.canceled

With cancel_immediately: true, this call ends the subscription right away. status becomes canceled, terminal, no further credit grants. Left at the default false, this call schedules the end instead. status becomes cancelling, cancel_at_period_end becomes true, and the customer keeps access until the period ends. Then status becomes expired. QuotaStack grants and takes back no credit either way — remaining balance is not refunded. A reason you send lands in metadata.cancel_reason, merged alongside any metadata you send. The two modes guard different statuses. The default path only works from active; every other status, including trialing, paused, and overdue, returns 409, undeclared here. cancel_immediately: true is more lenient: it works from trialing, active, cancelling, paused, or overdue, and 409s only from canceled, expired, or contract_ended.

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 subscription's own UUID. Not a customer id.

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

Body parameters
FieldTypeMeaning
cancel_immediatelyoptionalboolean

true ends the subscription right away. false schedules the end for when the current period finishes.

If you leave it out: By default, it is false — cancel at period end.

reasonoptionalstring

Your own words for why. QuotaStack stores this under metadata.cancel_reason.

If you leave it out: Nothing gets written to metadata.cancel_reason.

metadataoptionalobject

Your own key-value tags. QuotaStack merges this into the existing map, one key at a time.

If you leave it out: Nothing changes. The tags stay as they are.

Request
{
  "cancel_immediately": false,
  "reason": "Customer requested downgrade"
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "cancelling",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": true,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {
    "cancel_reason": "Customer requested downgrade"
  },
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

What comes back in each case

Default: ends at period end

The customer keeps access until the period ends. Then the subscription expires on its own.

{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "cancelling",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": true,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {
    "cancel_reason": "Customer requested downgrade"
  },
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
cancel_immediately: true

The subscription ends right away. Terminal — nothing here changes again.

{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "canceled",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {},
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}

Errors

Pause a subscription

POST/v1/subscriptions/{id}/pause
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesSets status to paused and records paused_at. resume_at and metadata are accepted and discarded.
Fires subscription.paused

This call suspends a subscription without ending it. QuotaStack fires no credit grants and does not move the billing period while paused. Existing credit stays spendable. Only an active subscription can pause — not trialing, not overdue. An unknown id returns 404. Pausing from any other status returns 409. openapi.yaml declares neither status for this call. resume_at and metadata on the request have no effect. QuotaStack accepts both and stores neither. Nothing auto-resumes a paused subscription — call resumeSubscription when you want it back.

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 subscription's own UUID. Not a customer id.

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

Body parameters
FieldTypeMeaning
resume_atoptionalstring (date-time)

QuotaStack accepts this field and stores nothing from it. Nothing auto-resumes a paused subscription.

If you leave it out: No difference. This field has no effect either way — call resumeSubscription yourself, whenever that should be.

metadataoptionalobject

QuotaStack accepts this field and stores nothing from it. Pause never reads it.

If you leave it out: No difference. This field has no effect either way.

Request
{
  "resume_at": "2026-08-01T00:00:00Z"
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "paused",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": "2026-07-20T11:00:00Z",
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {},
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

See also

Resume a subscription

POST/v1/subscriptions/{id}/resume
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesSets status to active, records resumed_at, and resets current_period_start/current_period_end to a fresh period starting now. Merges metadata. Grants credit for the new period, best-effort, with no rollover or accumulation_cap applied.
Fires subscription.resumed, credit.granted

This call is for a paused subscription. QuotaStack's status guard also allows a trialing, overdue, or contract_ended subscription to resume, though none of those is the case this call is built for. Resuming a contract_ended subscription reactivates it too — the same outcome extendSubscription produces. Resuming restarts the billing period from right now. Whatever time was left when it paused is gone. QuotaStack also grants credit for the new period, best-effort, the same amount createSubscription grants on activation: no rollover, no accumulation_cap. A grant with grant_interval: on_activation or grant_type: one_time does not fire again on resume. An unknown id returns 404. Resuming from a status that cannot reach active returns 409. Neither status is declared for this 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}.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The subscription's own UUID. Not a customer id.

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

Body parameters
FieldTypeMeaning
metadataoptionalobject

Your own key-value tags. QuotaStack merges this into the existing map, one key at a time.

If you leave it out: Nothing changes. The tags stay as they are.

Request
{
  "metadata": {
    "resumed_via": "support-ticket-482"
  }
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-22T08:00:00Z",
  "current_period_end": "2026-08-22T08:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": "2026-07-22T08:00:00Z",
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": null,
  "contract_ending_soon_days": 0,
  "metadata": {
    "resumed_via": "support-ticket-482"
  },
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

Renew a subscription

POST/v1/subscriptions/{id}/renew
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesAdvances current_period_start/current_period_end and grants that cycle's credit, applying rollover_percentage, max_rollover_cycles, and accumulation_cap. Merges metadata. external_payment_id overwrites external_subscription_id.
Fires subscription.renewed, credit.granted

This call is the credit-grant path for a manual prepaid subscription. Renewing moves the billing period forward and grants that cycle's credit. A connector-managed subscription advances only after a verified provider payment. This public call returns 409 for one. This call is the only one of the ten that honors rollover_percentage, max_rollover_cycles, and accumulation_cap. A postpaid variant moves its own period forward; you never call this for one. external_payment_id, when sent, overwrites external_subscription_id directly — it is not a field of its own. QuotaStack merges metadata. A failed grant aborts the whole call: nothing moves, and QuotaStack saves nothing. A retry is safe. If two renewal attempts race for the same subscription, the loser gets 200 back with credits_granted: 0. Treat that as done, not as a failure. A race with upgradeSubscription, downgradeSubscription, cancelSubscription, pauseSubscription, or resumeSubscription on the same subscription is a different case. This kind of race can fail the call with 500 instead, a status the spec never lists here. Any credit already granted stays granted, and a later retry will not grant it twice. An unknown id returns 404. Renewing from a status that cannot renew, one already set to cancel, or one managed by a payment connector returns 409. Neither is declared. This reply is not the full Subscription object either: the real reply adds credits_granted, in millicredits, and drops origin, started_at, trial_ends_at, paused_at, resumed_at, expired_at, contract_start, contract_end, and contract_ending_soon_days. The subscription.renewed webhook this call fires carries a different payload than this reply. That payload carries usage and balance totals for the period that ended, not the Subscription fields 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 subscription's own UUID. Not a customer id.

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

Body parameters
FieldTypeMeaning
external_payment_idoptionalstring

Your own id for the payment behind this renewal. QuotaStack writes it into external_subscription_id — the same field createSubscription sets, not a field of its own.

If you leave it out: external_subscription_id does not change.

metadataoptionalobject

Your own key-value tags. QuotaStack merges this into the existing map, one key at a time.

If you leave it out: Nothing changes. The tags stay as they are.

Request
{
  "external_payment_id": "pay_xyz789"
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "current_period_start": "2026-08-01T00:00:00Z",
  "current_period_end": "2026-09-01T00:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": "pay_xyz789",
  "credits_granted": 10000,
  "metadata": {},
  "created_at": "2026-06-01T09:00:00Z",
  "updated_at": "2026-08-01T00:03:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

What comes back in each case

The normal case

The period moved forward. This cycle's credit was granted too.

{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "current_period_start": "2026-08-01T00:00:00Z",
  "current_period_end": "2026-09-01T00:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": "pay_xyz789",
  "credits_granted": 10000,
  "metadata": {},
  "created_at": "2026-06-01T09:00:00Z",
  "updated_at": "2026-08-01T00:03:00Z"
}
Someone else renewed this cycle first

The scheduler, or another call, already advanced this period. QuotaStack grants nothing and reports the subscription as it now stands. Treat this as success, not as an error.

{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "current_period_start": "2026-08-01T00:00:00Z",
  "current_period_end": "2026-09-01T00:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": "pay_xyz789",
  "credits_granted": 0,
  "metadata": {},
  "created_at": "2026-06-01T09:00:00Z",
  "updated_at": "2026-08-01T00:02:47Z"
}

Extend a contract

POST/v1/subscriptions/{id}/extend
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesReplaces contract_end and merges metadata. Reactivates a contract_ended subscription to active; every other status is unaffected. Grants no credit and fires no event.
Fires No webhooks.

This call changes contract_end alone. The billing period stays put, and no credit changes hands — use renewSubscription for that. This call works on a subscription in any status. Only a contract_ended subscription gets reactivated, back to active. Every other status stays as it is. Omitting contract_end does not return an error. QuotaStack decodes the missing field as 0001-01-01T00:00:00Z and stores that as the new contract end. Always send a real date. reason has no effect — unlike cancelSubscription's reason, QuotaStack accepts this one and stores it nowhere. An unknown id returns 404, undeclared 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 subscription's own UUID. Not a customer id.

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

Body parameters
FieldTypeMeaning
contract_endrequiredstring (date-time)

The contract's new end date.

If you leave it out: QuotaStack does not reject a missing value. QuotaStack stores the zero date 0001-01-01T00:00:00Z. Always send a real one.

reasonoptionalstring

QuotaStack accepts this field and stores nothing from it, unlike cancelSubscription's reason.

If you leave it out: No difference. This field has no effect either way.

metadataoptionalobject

Your own key-value tags. QuotaStack merges this into the existing map, one key at a time.

If you leave it out: Nothing changes. The tags stay as they are.

Request
{
  "contract_end": "2027-07-01T00:00:00Z",
  "reason": "Annual renewal"
}
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
  "environment": "live",
  "status": "active",
  "origin": "api",
  "started_at": "2026-07-01T09:00:00Z",
  "trial_ends_at": null,
  "current_period_start": "2026-07-01T09:00:00Z",
  "current_period_end": "2026-08-01T09:00:00Z",
  "billing_anchor": 1,
  "cancel_at_period_end": false,
  "scheduled_variant_id": null,
  "external_subscription_id": null,
  "paused_at": null,
  "resumed_at": null,
  "expired_at": null,
  "contract_start": "2026-07-01T09:00:00Z",
  "contract_end": "2027-07-01T00:00:00Z",
  "contract_ending_soon_days": 0,
  "metadata": {},
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring

This subscription's current state in its lifecycle.

  • trialingA trial. QuotaStack moves this to active on its own once the trial ends.
  • activeThe normal state. Credit grants fire on schedule, and usage debits the balance.
  • cancellingEnding at period end, from cancelSubscription. Access continues until then.
  • canceledEnded right away, from cancelSubscription with cancel_immediately: true. Terminal.
  • expiredA grace period ran out, or a cancelling period ended. Terminal.
  • pausedSuspended, from pauseSubscription. No credit grants fire, and the billing period does not move.
  • overdueThe period ended and nobody called renewSubscription yet. Prepaid only.
  • contract_endedThe contract's end date passed. extendSubscription can bring this back to active.
originoptionalstring

How this subscription came to exist.

  • apiCreated through these ten endpoints — the ordinary case.
  • importBrought in through a legacy cutover, not createSubscription.
started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)

When the trial ends. null for a subscription that never had one.

current_period_startoptionalstring (date-time)

The start of the billing period this subscription is in right now.

current_period_endoptionalstring (date-time)

The end of the billing period this subscription is in right now.

billing_anchoroptionalinteger

The day of the month a monthly period renews on, 1 through 31.

cancel_at_period_endrequiredboolean

Whether this subscription stops at the end of its current period instead of right away.

scheduled_variant_idoptionalstring (uuid)

The plan variant a pending downgradeSubscription will switch to at the next renewal. null with no downgrade pending. An upgradeSubscription call clears this, canceling the pending downgrade outright.

external_subscription_idoptionalstring

Your own identifier for this subscription. renewSubscription's external_payment_id writes here too — the two calls share this one column, not two separate fields.

paused_atoptionalstring (date-time)

When pauseSubscription last ran. null if this subscription has never paused.

resumed_atoptionalstring (date-time)

When resumeSubscription last ran. null if this subscription has never resumed.

expired_atoptionalstring (date-time)

When this subscription reached expired. null until it does.

contract_startoptionalstring (date-time)

When the contract began. QuotaStack sets this once, at creation.

contract_endoptionalstring (date-time)

When the contract ends. null for an open-ended subscription.

contract_ending_soon_daysrequiredinteger

Days before contract_end that QuotaStack fires subscription.contract_ending_soon. A subscription created with no value here reads 0, not 30 — the scheduler still treats 0 as 30 when it checks the reminder.

metadatarequiredobject

Your own key-value tags, as they stand after the last write that touched them.

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

See also