quotastack Docs
Docs / API / Entitlements

Entitlements

Can this customer do this, right now?

An entitlement check answers one question: can this customer do this, right now? You call it before the work starts, not after.

The answer depends on the customer’s plan and their current credit balance. A check runs fast enough to call on every request.

Which call do I want?

  • You want to know whether a customer can do something, and change nothingGET /v1/entitlements/check
    A check never holds credits. The balance can still change before you call again.
  • You want to charge a customer for something they already didPOST /v1/entitlements/consume
    A check changes nothing. A consume takes credits. Calling check before consume is not required, and it does not reserve anything. The balance can change between the two calls.
  • You already let the action happen, and only want to bill for it afterwardrecordUsage
    recordUsage queues the event and bills it later, in the background. consumeEntitlement checks and charges right away, and tells you the result at once. Use consumeEntitlement to decide whether to allow the action. Use recordUsage once you already have.
  • You want every entitlement for a customer, not one metric aloneGET /v1/customers/{id}/entitlements
  • You are tracking a running count that has a cap, like seats or storagePOST /v1/customers/{id}/entitlements/{billable_metric_key}/increment
    A gauge has no price of its own. QuotaStack blocks or allows based on the cap, never the credit balance. Use decrementGauge when a seat frees up or storage is deleted.
10 of 10 operations documented in full.

Check an entitlement

GET/v1/entitlements/check
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing to the entitlement itself. A check never touches a balance or a subscription. An unrecognized customer id is different. QuotaStack makes a new customer for it, the same as a credit grant. The new one starts with no balance and no subscription.
Fires No webhooks.

A check answers one question: can the customer do this, right now? QuotaStack changes nothing when you call it. Send units for a metered metric. QuotaStack ignores units for every other type. The four entitlement types answer with different fields — see the type table below.

Query parameters
FieldTypeMeaning
customer_idrequiredstring

The customer’s own id, or your own external_id. Either one works here.

billable_metric_keyrequiredstring

Which billable metric to check.

unitsoptionalinteger

How many units to check for. Only a metered metric uses this — every other type ignores it.

If you leave it out: Left out, QuotaStack checks for 1 unit.

Response 200
{
  "type": "metered",
  "allowed": true,
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 1,
  "balance": 250000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 250000,
  "estimated_cost": 500,
  "balance_after": 249500,
  "subscription_status": "active",
  "overage_policy": "block"
}
Response fields
FieldTypeMeaning
typerequiredstring

Which entitlement type this metric is.

  • meteredPriced by a metering rule, paid out of the customer’s balance.
  • booleanA plain on/off flag. Only allowed matters — nothing costs credits.
  • gaugeA running count with a cap, like a seat limit. Nothing costs credits either.
  • staticA JSON value your app reads directly. QuotaStack never charges for reading it.
allowedrequiredboolean

Whether QuotaStack says yes to this call, right now.

customer_idrequiredstring (uuid)

The customer’s own id inside QuotaStack. The same id comes back, no matter which id you sent.

billable_metric_keyrequiredstring

The billable metric key you asked about.

unitsoptionalinteger (int64)

How many units you asked about. Only present for a metered check.

Unit: a count of billable metric units, not credits.

balanceoptionalinteger (int64)

What the customer has, before holds. Only present for a metered check.

Unit: millicredits, where 1 credit is 1000 millicredits.

reserved_balanceoptionalinteger (int64)

Credits held by an open reservation. Only present for a metered check.

Unit: millicredits.

pending_balanceoptionalinteger (int64)

Credits in a block that starts later. Only present for a metered check.

Unit: millicredits.

effective_balanceoptionalinteger (int64)

What a new charge can spend, right now. Only present for a metered check.

Unit: millicredits.

estimated_costoptionalinteger (int64)

What units would cost, at today’s price. Only present for a metered check.

Unit: millicredits.

balance_afteroptionalinteger (int64)

How much would be left after this charge. effective_balance minus estimated_cost, worked out ahead of time. Below zero means the charge costs more than the customer can pay. Under allow or notify, QuotaStack would log the extra as overage instead of blocking. Only present for a metered check.

Unit: millicredits.

subscription_statusoptionalstring

The customer’s subscription status. null with no subscription. Only present for a metered check.

overage_policyoptionalstring

The customer's own value wins when set. The tenant default overage rule applies when it is not. Only present for a metered check.

  • blockRefuse the call once the balance runs out.
  • allowLet the call continue. QuotaStack logs the rest as overage.
  • notifyLike allow. No webhook fires for the overage yet.
enabledoptionalboolean

The flag’s value for this customer. Only present for a boolean check.

configoptionalobject

The free-form value your app should read. Comes from the customer’s plan variant, or the metric’s own default. Only present for a static check.

currentoptionalinteger (int64)

How many units this customer has already used, for this gauge. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

capoptionalinteger (int64)

The most this customer may use, for this gauge. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

headroomoptionalinteger (int64)

The cap minus how much the customer has used. Below zero once they are already over it. A plan downgrade can cause that, by lowering the cap. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

Errors

  • 400A required query parameter is missing: customer_id or billable_metric_key. A units value of zero or less returns this too. So does a customer_id that belongs to a different tenant. Use your own customer_id, or send your own external id instead. bad-request
  • 401Missing or invalid authentication.
  • 404No active metering rule covers this billable_metric_key. A boolean, gauge, or static metric never returns this — only a metered check can. not-found
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Check an entitlement by customer ID and metric

GET/v1/customers/{id}/entitlements/{metric}
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing to the entitlement itself. A check never touches a balance or a subscription. An unrecognized customer id is different. QuotaStack makes a new customer for it, the same as a credit grant. The new one starts with no balance and no subscription.
Fires No webhooks.

checkCustomerEntitlement checks the same thing as checkEntitlement. The customer and the metric sit in the path here, not the query string. Both return the same result. openapi.yaml lists only 401 and 404 for this call. A bad units value still returns 400 in practice. So does a customer_id from another tenant.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The customer’s own id, or your own external_id. Either one works here.

metricrequiredstring

The billable metric’s own key. Not a QuotaStack id.

Query parameters
FieldTypeMeaning
unitsoptionalinteger

How many units to check for. Only a metered metric uses this — every other type ignores it.

If you leave it out: Left out, QuotaStack checks for 1 unit.

Response 200
{
  "type": "metered",
  "allowed": true,
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 1,
  "balance": 250000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 250000,
  "estimated_cost": 500,
  "balance_after": 249500,
  "subscription_status": "active",
  "overage_policy": "block"
}
Response fields
FieldTypeMeaning
typerequiredstring

Which entitlement type this metric is.

  • meteredPriced by a metering rule, paid out of the customer’s balance.
  • booleanA plain on/off flag. Only allowed matters — nothing costs credits.
  • gaugeA running count with a cap, like a seat limit. Nothing costs credits either.
  • staticA JSON value your app reads directly. QuotaStack never charges for reading it.
allowedrequiredboolean

Whether QuotaStack says yes to this call, right now.

customer_idrequiredstring (uuid)

The customer’s own id inside QuotaStack. The same id comes back, no matter which id you sent.

billable_metric_keyrequiredstring

The billable metric key you asked about.

unitsoptionalinteger (int64)

How many units you asked about. Only present for a metered check.

Unit: a count of billable metric units, not credits.

balanceoptionalinteger (int64)

What the customer has, before holds. Only present for a metered check.

Unit: millicredits, where 1 credit is 1000 millicredits.

reserved_balanceoptionalinteger (int64)

Credits held by an open reservation. Only present for a metered check.

Unit: millicredits.

pending_balanceoptionalinteger (int64)

Credits in a block that starts later. Only present for a metered check.

Unit: millicredits.

effective_balanceoptionalinteger (int64)

What a new charge can spend, right now. Only present for a metered check.

Unit: millicredits.

estimated_costoptionalinteger (int64)

What units would cost, at today’s price. Only present for a metered check.

Unit: millicredits.

balance_afteroptionalinteger (int64)

How much would be left after this charge. effective_balance minus estimated_cost, worked out ahead of time. Below zero means the charge costs more than the customer can pay. Under allow or notify, QuotaStack would log the extra as overage instead of blocking. Only present for a metered check.

Unit: millicredits.

subscription_statusoptionalstring

The customer’s subscription status. null with no subscription. Only present for a metered check.

overage_policyoptionalstring

The customer's own value wins when set. The tenant default overage rule applies when it is not. Only present for a metered check.

  • blockRefuse the call once the balance runs out.
  • allowLet the call continue. QuotaStack logs the rest as overage.
  • notifyLike allow. No webhook fires for the overage yet.
enabledoptionalboolean

The flag’s value for this customer. Only present for a boolean check.

configoptionalobject

The free-form value your app should read. Comes from the customer’s plan variant, or the metric’s own default. Only present for a static check.

currentoptionalinteger (int64)

How many units this customer has already used, for this gauge. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

capoptionalinteger (int64)

The most this customer may use, for this gauge. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

headroomoptionalinteger (int64)

The cap minus how much the customer has used. Below zero once they are already over it. A plan downgrade can cause that, by lowering the cap. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

Errors

  • 401Missing or invalid authentication.
  • 404No active metering rule covers this metric. A boolean, gauge, or static metric never returns this. not-found

Get every entitlement for a customer

GET/v1/customers/{id}/entitlements
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing to the entitlement itself. A check never touches a balance or a subscription. An unrecognized customer id is different. QuotaStack makes a new customer for it, the same as a credit grant. The new one starts with no balance and no subscription.
Fires No webhooks.

This call returns one snapshot for one customer: the balance, the subscription, and every entitlement the tenant has defined. QuotaStack lists every billable metric the tenant has. A customer’s plan does not filter this list. allowed says whether this customer can use each one right now. QuotaStack caches this snapshot for up to 30 seconds. Send Cache-Control: no-cache to read a fresh one instead. The response also carries a pending_balance field, in millicredits. openapi.yaml does not yet declare it.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The customer’s own id, or your own external_id. Either one works here.

Response 200
{
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "environment": "live",
  "balance": 250000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 250000,
  "subscription_status": "active",
  "plan_name": "Pro",
  "entitlements": {
    "chat_message": {
      "type": "metered",
      "billable_metric_key": "chat_message",
      "allowed": true,
      "estimated_cost_per_unit": 500,
      "affordable_units": 500,
      "cost_type": "per_unit"
    },
    "sso": {
      "type": "boolean",
      "billable_metric_key": "sso",
      "allowed": true,
      "enabled": true
    },
    "seats": {
      "type": "gauge",
      "billable_metric_key": "seats",
      "allowed": true,
      "current": 3,
      "cap": 5,
      "headroom": 2
    }
  },
  "cached_at": "2026-07-27T10:00:00Z"
}
Response fields
FieldTypeMeaning
customer_idrequiredstring (uuid)

This customer’s id inside QuotaStack. The same id comes back, no matter which id you sent.

environmentrequiredstring

Sandbox or live. Matches the API key you called with.

  • live
  • sandbox
balancerequiredinteger (int64)

What the customer has, before holds.

Unit: millicredits, where 1 credit is 1000 millicredits.

reserved_balancerequiredinteger (int64)

Credits held by an open reservation.

Unit: millicredits.

effective_balancerequiredinteger (int64)

What a new charge can spend, right now.

Unit: millicredits.

subscription_statusoptionalstring

The customer’s subscription status. null with no subscription.

plan_nameoptionalstring

The customer’s plan name. null with no subscription.

entitlementsrequiredobject

One entry per billable metric your tenant has, keyed by its billable_metric_key. Every metric your tenant defines shows up here, whether or not the customer’s plan grants it. allowed says whether they can use it right now. Each entry carries its own type: metered, boolean, gauge, or static. A metered entry adds estimated_cost_per_unit, affordable_units, and cost_type. A boolean entry adds enabled. A gauge entry adds current, cap, and headroom. A static entry adds config.

cached_atrequiredstring (date-time)

When QuotaStack computed this snapshot. The snapshot can be up to 30 seconds old.

Errors

  • 401Missing or invalid authentication.
  • 404Declared here, but never reached. An unrecognized customer_id makes a new customer, instead of a 404. A customer_id from another tenant gets 400 instead. not-found

Check an entitlement by external ID and metric

GET/v1/customer-by-external-id/{external_id}/entitlements/{metric}
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing to the entitlement itself. A check never touches a balance or a subscription.
Fires No webhooks.

This one works the same as checkCustomerEntitlement does. You use your own id here, not the QuotaStack one. A bad id here never makes a new customer — you get a 404 back instead. A bad units number still gives you a 400, in real use.

Path parameters
FieldTypeMeaning
external_idrequiredstring

Your own ID for this customer, set when you created it. QuotaStack looks up only this value, never the QuotaStack UUID.

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

metricrequiredstring

The billable metric’s own key. Not a QuotaStack id.

Query parameters
FieldTypeMeaning
unitsoptionalinteger

How many units to check for. Only a metered metric uses this — every other type ignores it.

If you leave it out: Left out, QuotaStack checks for 1 unit.

Response 200
{
  "type": "metered",
  "allowed": true,
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 1,
  "balance": 250000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 250000,
  "estimated_cost": 500,
  "balance_after": 249500,
  "subscription_status": "active",
  "overage_policy": "block"
}
Response fields
FieldTypeMeaning
typerequiredstring

Which entitlement type this metric is.

  • meteredPriced by a metering rule, paid out of the customer’s balance.
  • booleanA plain on/off flag. Only allowed matters — nothing costs credits.
  • gaugeA running count with a cap, like a seat limit. Nothing costs credits either.
  • staticA JSON value your app reads directly. QuotaStack never charges for reading it.
allowedrequiredboolean

Whether QuotaStack says yes to this call, right now.

customer_idrequiredstring (uuid)

The customer’s own id inside QuotaStack. The same id comes back, no matter which id you sent.

billable_metric_keyrequiredstring

The billable metric key you asked about.

unitsoptionalinteger (int64)

How many units you asked about. Only present for a metered check.

Unit: a count of billable metric units, not credits.

balanceoptionalinteger (int64)

What the customer has, before holds. Only present for a metered check.

Unit: millicredits, where 1 credit is 1000 millicredits.

reserved_balanceoptionalinteger (int64)

Credits held by an open reservation. Only present for a metered check.

Unit: millicredits.

pending_balanceoptionalinteger (int64)

Credits in a block that starts later. Only present for a metered check.

Unit: millicredits.

effective_balanceoptionalinteger (int64)

What a new charge can spend, right now. Only present for a metered check.

Unit: millicredits.

estimated_costoptionalinteger (int64)

What units would cost, at today’s price. Only present for a metered check.

Unit: millicredits.

balance_afteroptionalinteger (int64)

How much would be left after this charge. effective_balance minus estimated_cost, worked out ahead of time. Below zero means the charge costs more than the customer can pay. Under allow or notify, QuotaStack would log the extra as overage instead of blocking. Only present for a metered check.

Unit: millicredits.

subscription_statusoptionalstring

The customer’s subscription status. null with no subscription. Only present for a metered check.

overage_policyoptionalstring

The customer's own value wins when set. The tenant default overage rule applies when it is not. Only present for a metered check.

  • blockRefuse the call once the balance runs out.
  • allowLet the call continue. QuotaStack logs the rest as overage.
  • notifyLike allow. No webhook fires for the overage yet.
enabledoptionalboolean

The flag’s value for this customer. Only present for a boolean check.

configoptionalobject

The free-form value your app should read. Comes from the customer’s plan variant, or the metric’s own default. Only present for a static check.

currentoptionalinteger (int64)

How many units this customer has already used, for this gauge. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

capoptionalinteger (int64)

The most this customer may use, for this gauge. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

headroomoptionalinteger (int64)

The cap minus how much the customer has used. Below zero once they are already over it. A plan downgrade can cause that, by lowering the cap. Only present for a gauge check.

Unit: a count of this gauge’s own units, not credits.

Errors

  • 401Missing or invalid authentication.
  • 404No customer here has this external_id, or no active metering rule covers this metric. not-found

Get every entitlement by external ID

GET/v1/customer-by-external-id/{external_id}/entitlements
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing to the entitlement itself. A check never touches a balance or a subscription.
Fires No webhooks.

getCustomerEntitlementsByExternalId is the external_id twin of getCustomerEntitlements. An unrecognized external_id never creates a customer here. QuotaStack returns 404 instead. Here, unlike the id form, 404 is real and reachable.

Path parameters
FieldTypeMeaning
external_idrequiredstring

Your own ID for this customer, set when you created it. QuotaStack looks up only this value, never the QuotaStack UUID.

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

Response 200
{
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "environment": "live",
  "balance": 250000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 250000,
  "subscription_status": "active",
  "plan_name": "Pro",
  "entitlements": {
    "chat_message": {
      "type": "metered",
      "billable_metric_key": "chat_message",
      "allowed": true,
      "estimated_cost_per_unit": 500,
      "affordable_units": 500,
      "cost_type": "per_unit"
    },
    "sso": {
      "type": "boolean",
      "billable_metric_key": "sso",
      "allowed": true,
      "enabled": true
    },
    "seats": {
      "type": "gauge",
      "billable_metric_key": "seats",
      "allowed": true,
      "current": 3,
      "cap": 5,
      "headroom": 2
    }
  },
  "cached_at": "2026-07-27T10:00:00Z"
}
Response fields
FieldTypeMeaning
customer_idrequiredstring (uuid)

This customer’s id inside QuotaStack. The same id comes back, no matter which id you sent.

environmentrequiredstring

Sandbox or live. Matches the API key you called with.

  • live
  • sandbox
balancerequiredinteger (int64)

What the customer has, before holds.

Unit: millicredits, where 1 credit is 1000 millicredits.

reserved_balancerequiredinteger (int64)

Credits held by an open reservation.

Unit: millicredits.

effective_balancerequiredinteger (int64)

What a new charge can spend, right now.

Unit: millicredits.

subscription_statusoptionalstring

The customer’s subscription status. null with no subscription.

plan_nameoptionalstring

The customer’s plan name. null with no subscription.

entitlementsrequiredobject

One entry per billable metric your tenant has, keyed by its billable_metric_key. Every metric your tenant defines shows up here, whether or not the customer’s plan grants it. allowed says whether they can use it right now. Each entry carries its own type: metered, boolean, gauge, or static. A metered entry adds estimated_cost_per_unit, affordable_units, and cost_type. A boolean entry adds enabled. A gauge entry adds current, cap, and headroom. A static entry adds config.

cached_atrequiredstring (date-time)

When QuotaStack computed this snapshot. The snapshot can be up to 30 seconds old.

Errors

  • 401Missing or invalid authentication.
  • 404No customer here has this external_id. not-found

Increment a gauge

POST/v1/customers/{id}/entitlements/{billable_metric_key}/increment
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesAdds amount to this customer’s gauge count for this metric. First use makes the counter at zero, then applies the increment. An unrecognized customer id is different. QuotaStack makes a new customer for it, the same as a credit grant. The new one starts with no balance and no subscription.
Fires No webhooks.

A gauge tracks a running count with a cap, like seats or storage. Increment adds amount to the count. The cap comes from the customer’s plan variant, or the metric’s own default. With policy: block, the default, an increment past the cap is refused. With policy: allow, QuotaStack lets it through instead — headroom in the response goes negative.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

Required idempotency key for this mutating admin/public operation. Reusing the same key with a different body returns 409 Conflict; successful 2xx and client 4xx responses are cached for 24 hours.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The customer’s own id, or your own external_id. Either one works here.

billable_metric_keyrequiredstring

The billable metric’s own key. Not a QuotaStack id.

Body parameters
FieldTypeMeaning
amountrequiredinteger (int64)1–∞

How much to change the gauge by — added on increment, subtracted on decrement.

Unit: a count of this gauge’s own units, not credits.

If you leave it out: QuotaStack rejects the call. Must be a whole number above zero.

policyoptionalstring

What to do when the change would cross the cap. Only incrementGauge and its external_id twin act on this. decrementGauge takes the field too, but ignores it — a decrement can never cross the cap upward.

If you leave it out: Left out, QuotaStack uses block.

  • blockRefuse the increment. The count stays where it was.
  • allowLet the increment through, even past the cap. headroom in the response goes negative.
metadataoptionalobject

Your own key-value tags. QuotaStack takes this field but throws it away — nothing is stored.

If you leave it out: Nothing changes either way. The field has no effect.

Request
{
  "amount": 1
}
Response 200
{
  "billable_metric_key": "seats",
  "current": 4,
  "cap": 5,
  "allowed": true,
  "headroom": 1
}
Response fields
FieldTypeMeaning
billable_metric_keyrequiredstring

The gauge metric this change touched.

currentrequiredinteger (int64)

The gauge’s count after this change.

Unit: a count of this gauge’s own units, not credits.

caprequiredinteger (int64)

The cap in force right now. Comes from the customer’s plan variant, or the metric’s own default value.

Unit: a count of this gauge’s own units, not credits.

allowedrequiredboolean

Whether current still sits below cap, after this change.

headroomrequiredinteger (int64)

cap minus current. Negative once policy: allow lets an increment cross the cap.

Unit: a count of this gauge’s own units, not credits.

Errors

  • 400amount is zero or negative. policy can also be wrong — anything other than block or allow. Or the metric is not a gauge type. So does a customer_id that belongs to a different tenant. Use your own customer_id, or send your own external id instead. bad-request
  • 401Missing or invalid authentication.
  • 404No billable metric here has this key at all. not-found
  • 409Two different bodies come back here. Under policy: block, crossing the cap sends back current, cap, and shortfall. A version clash sends back a plain conflict instead — retry it. conflict

Decrement a gauge

POST/v1/customers/{id}/entitlements/{billable_metric_key}/decrement
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesSubtracts amount from this customer’s gauge count for this metric. An unrecognized customer id is different. QuotaStack makes a new customer for it, the same as a credit grant. The new one starts with no balance and no subscription.
Fires No webhooks.

Decrement subtracts amount from the customer’s gauge count. The count never goes below zero. QuotaStack refuses a decrement that would take it there. The request takes a policy field, the same one incrementGauge uses, but decrement ignores it.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

Required idempotency key for this mutating admin/public operation. Reusing the same key with a different body returns 409 Conflict; successful 2xx and client 4xx responses are cached for 24 hours.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

The customer’s own id, or your own external_id. Either one works here.

billable_metric_keyrequiredstring

The billable metric’s own key. Not a QuotaStack id.

Body parameters
FieldTypeMeaning
amountrequiredinteger (int64)1–∞

How much to change the gauge by — added on increment, subtracted on decrement.

Unit: a count of this gauge’s own units, not credits.

If you leave it out: QuotaStack rejects the call. Must be a whole number above zero.

policyoptionalstring

What to do when the change would cross the cap. Only incrementGauge and its external_id twin act on this. decrementGauge takes the field too, but ignores it — a decrement can never cross the cap upward.

If you leave it out: Left out, QuotaStack uses block.

  • blockRefuse the increment. The count stays where it was.
  • allowLet the increment through, even past the cap. headroom in the response goes negative.
metadataoptionalobject

Your own key-value tags. QuotaStack takes this field but throws it away — nothing is stored.

If you leave it out: Nothing changes either way. The field has no effect.

Request
{
  "amount": 1
}
Response 200
{
  "billable_metric_key": "seats",
  "current": 2,
  "cap": 5,
  "allowed": true,
  "headroom": 3
}
Response fields
FieldTypeMeaning
billable_metric_keyrequiredstring

The gauge metric this change touched.

currentrequiredinteger (int64)

The gauge’s count after this change.

Unit: a count of this gauge’s own units, not credits.

caprequiredinteger (int64)

The cap in force right now. Comes from the customer’s plan variant, or the metric’s own default value.

Unit: a count of this gauge’s own units, not credits.

allowedrequiredboolean

Whether current still sits below cap, after this change.

headroomrequiredinteger (int64)

cap minus current. Negative once policy: allow lets an increment cross the cap.

Unit: a count of this gauge’s own units, not credits.

Errors

  • 400amount is zero or negative. The metric can also be the wrong type — not a gauge. A customer id from a different tenant returns this too. bad-request
  • 401Missing or invalid authentication.
  • 404No billable metric here has this key at all. not-found
  • 409The decrement would take the count below zero. Or another mutation on the same gauge ran at the same time. Retry either one. conflict

Check and charge in one call

POST/v1/entitlements/consume
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesWrites one debit to the ledger, unless a reason value says nothing was charged. An unrecognized customer id is different. QuotaStack makes a new customer for it, the same as a credit grant. The new one starts with no balance and no subscription.
Fires credit.consumed, credit.low_balance, credit.exhausted

A consume checks whether the customer can afford units of a metered metric. QuotaStack charges them too, in the same step. Only a metered metric works here. Sending a boolean, gauge, or static metric’s key returns 400. An exact repeat of the same Idempotency-Key replays from cache for 24 hours, like every write. The Idempotency-Key cache can expire before you retry. reason: duplicate_replay catches that case too. QuotaStack replays the ORIGINAL numbers, not new ones.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

Required idempotency key for this mutating admin/public operation. Reusing the same key with a different body returns 409 Conflict; successful 2xx and client 4xx responses are cached for 24 hours.

Body parameters
FieldTypeMeaning
customer_idrequiredstring

The customer’s own id, or your own external_id. Either works here.

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

billable_metric_keyrequiredstring

Which metered metric to charge for. This metric needs an active metering rule.

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

unitsrequiredinteger (int64)1–∞

How many units this call used.

Unit: a count of billable metric units, not credits.

If you leave it out: QuotaStack rejects the call. Must be at least 1.

metadataoptionalobject

Your own key-value tags. QuotaStack writes them onto the ledger entry this call makes.

If you leave it out: QuotaStack writes no tags onto the ledger entry.

Request
{
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 10
}
Response 200
{
  "type": "metered",
  "allowed": true,
  "debited": true,
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 10,
  "estimated_cost": 5000,
  "balance": 245000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 245000,
  "subscription_status": "active",
  "overage_policy": "block"
}
Response fields
FieldTypeMeaning
typerequiredstring

Always metered. Only a metered metric ever reaches this endpoint.

  • metered
allowedrequiredboolean

Whether QuotaStack says yes to this call, right now.

debitedrequiredboolean

Whether QuotaStack took any credits at all. true even for a partial overage debit.

reasonoptionalstring

Why the call ended this way. QuotaStack only sends this field when something notable happened. A normal call has no reason.

  • insufficient_creditsThe customer did not have enough credits. Nothing was charged.
  • overage_recordedThe customer ran out of credits partway through. QuotaStack charged what they had. QuotaStack wrote down the rest as overage. The overage field holds that amount.
  • overdue_blockedThe customer's subscription is overdue. Nothing was charged.
  • zero_costThis metric costs nothing. Nothing was charged.
  • duplicate_replayYou already sent this request. QuotaStack already charged for it then. QuotaStack does not charge again now. units and estimated_cost show the first request's own numbers, not this one's.
original_reasonoptionalstring

The first call’s own reason, replayed here. Only present when reason is duplicate_replay and that first call was overage_recorded.

  • overage_recorded
customer_idrequiredstring (uuid)

The customer’s own id inside QuotaStack. The same id comes back, no matter which id you sent.

billable_metric_keyrequiredstring

The metric key this call charged against.

unitsrequiredinteger (int64)

The units this call charged for. On reason: duplicate_replay, this is the FIRST call’s units, not this one’s.

Unit: a count of billable metric units, not credits.

estimated_costrequiredinteger (int64)

What units cost, at the price in effect. On reason: duplicate_replay, this is the FIRST call’s cost, not this one’s.

Unit: millicredits, where 1 credit is 1000 millicredits.

balancerequiredinteger (int64)

What the customer has, before holds, read fresh after this call.

Unit: millicredits.

reserved_balancerequiredinteger (int64)

Credits held by an open reservation. A consume never changes this.

Unit: millicredits.

pending_balancerequiredinteger (int64)

Credits in a block that starts later.

Unit: millicredits.

effective_balancerequiredinteger (int64)

What a new charge can spend, right after this call.

Unit: millicredits.

shortfalloptionalinteger (int64)

estimated_cost minus effective_balance. Only present when reason is insufficient_credits.

Unit: millicredits.

overageoptionalinteger (int64)

Credit spent past the balance, logged for arrears billing later. Only present when reason is overage_recorded, or a duplicate_replay of one.

Unit: millicredits.

subscription_statusrequiredstring

The customer’s subscription status. null with no subscription.

overage_policyrequiredstring

The customer's own value wins when set. The tenant default overage rule applies when it is not.

  • blockRefuse the call once the balance runs out.
  • allowLet the call continue. QuotaStack logs the rest as overage.
  • notifyLike allow. No webhook fires for the overage yet.

What comes back in each case

The normal case

The customer had enough credit. QuotaStack went ahead and charged them.

{
  "type": "metered",
  "allowed": true,
  "debited": true,
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 10,
  "estimated_cost": 5000,
  "balance": 245000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 245000,
  "subscription_status": "active",
  "overage_policy": "block"
}
Not enough credits

QuotaStack charged nothing at all. Your app should stop right here.

{
  "type": "metered",
  "allowed": false,
  "debited": false,
  "reason": "insufficient_credits",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 10,
  "estimated_cost": 5000,
  "balance": 2000,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 2000,
  "shortfall": 3000,
  "subscription_status": "active",
  "overage_policy": "block"
}
Ran out partway through

QuotaStack charged what the customer had, and logged the rest as overage. You bill for that part later.

{
  "type": "metered",
  "allowed": true,
  "debited": true,
  "reason": "overage_recorded",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 10,
  "estimated_cost": 5000,
  "balance": 0,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 0,
  "overage": 2000,
  "subscription_status": "active",
  "overage_policy": "allow"
}
You sent the same key twice

QuotaStack charged nothing the second time. The numbers you get back are from the first call.

{
  "type": "metered",
  "allowed": true,
  "debited": true,
  "reason": "duplicate_replay",
  "original_reason": "overage_recorded",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "units": 10,
  "estimated_cost": 5000,
  "balance": 0,
  "reserved_balance": 0,
  "pending_balance": 0,
  "effective_balance": 0,
  "overage": 2000,
  "subscription_status": "active",
  "overage_policy": "allow"
}

Errors

  • 400customer_id, billable_metric_key, or a positive units value is missing. A boolean, gauge, or static metric also returns this. So does a customer id from a different tenant. bad-request
  • 401Missing or invalid authentication.
  • 404No active metering rule covers this billable_metric_key. not-found
  • 409You reused this Idempotency-Key with a different customer_id, billable_metric_key, or units. The hash QuotaStack compares ignores metadata — changing only that still replays. idempotency-key-reuse
  • 422The Idempotency-Key header is missing here. QuotaStack’s idempotency middleware catches this before the handler runs. validation-error

Increment a gauge by external ID

POST/v1/customer-by-external-id/{external_id}/entitlements/{billable_metric_key}/increment
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesAdds amount to this customer’s gauge count for this metric. First use makes the counter at zero, then applies the increment.
Fires No webhooks.

incrementGaugeByExternalId works the same as incrementGauge. An unrecognized external_id never makes a customer here. 404 comes back instead.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

Required idempotency key for this mutating admin/public operation. Reusing the same key with a different body returns 409 Conflict; successful 2xx and client 4xx responses are cached for 24 hours.

Path parameters
FieldTypeMeaning
external_idrequiredstring

Your own ID for this customer, set when you created it. QuotaStack looks up only this value, never the QuotaStack UUID.

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

billable_metric_keyrequiredstring

The billable metric’s own key. Not a QuotaStack id.

Body parameters
FieldTypeMeaning
amountrequiredinteger (int64)1–∞

How much to change the gauge by — added on increment, subtracted on decrement.

Unit: a count of this gauge’s own units, not credits.

If you leave it out: QuotaStack rejects the call. Must be a whole number above zero.

policyoptionalstring

What to do when the change would cross the cap. Only incrementGauge and its external_id twin act on this. decrementGauge takes the field too, but ignores it — a decrement can never cross the cap upward.

If you leave it out: Left out, QuotaStack uses block.

  • blockRefuse the increment. The count stays where it was.
  • allowLet the increment through, even past the cap. headroom in the response goes negative.
metadataoptionalobject

Your own key-value tags. QuotaStack takes this field but throws it away — nothing is stored.

If you leave it out: Nothing changes either way. The field has no effect.

Request
{
  "amount": 1
}
Response 200
{
  "billable_metric_key": "seats",
  "current": 4,
  "cap": 5,
  "allowed": true,
  "headroom": 1
}
Response fields
FieldTypeMeaning
billable_metric_keyrequiredstring

The gauge metric this change touched.

currentrequiredinteger (int64)

The gauge’s count after this change.

Unit: a count of this gauge’s own units, not credits.

caprequiredinteger (int64)

The cap in force right now. Comes from the customer’s plan variant, or the metric’s own default value.

Unit: a count of this gauge’s own units, not credits.

allowedrequiredboolean

Whether current still sits below cap, after this change.

headroomrequiredinteger (int64)

cap minus current. Negative once policy: allow lets an increment cross the cap.

Unit: a count of this gauge’s own units, not credits.

Errors

  • 400amount is zero or negative. policy can also be wrong — anything other than block or allow. Or the metric is not a gauge type. bad-request
  • 401Missing or invalid authentication.
  • 404No customer here has this external_id, or no billable metric has this key at all. not-found
  • 409Two different bodies come back here. Under policy: block, crossing the cap sends back current, cap, and shortfall. A version clash sends back a plain conflict instead — retry it. conflict

Decrement a gauge by external ID

POST/v1/customer-by-external-id/{external_id}/entitlements/{billable_metric_key}/decrement
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesSubtracts amount from this customer’s gauge count for this metric.
Fires No webhooks.

decrementGaugeByExternalId works the same as decrementGauge. An unrecognized external_id never makes a customer here. 404 comes back instead.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

Required idempotency key for this mutating admin/public operation. Reusing the same key with a different body returns 409 Conflict; successful 2xx and client 4xx responses are cached for 24 hours.

Path parameters
FieldTypeMeaning
external_idrequiredstring

Your own ID for this customer, set when you created it. QuotaStack looks up only this value, never the QuotaStack UUID.

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

billable_metric_keyrequiredstring

The billable metric’s own key. Not a QuotaStack id.

Body parameters
FieldTypeMeaning
amountrequiredinteger (int64)1–∞

How much to change the gauge by — added on increment, subtracted on decrement.

Unit: a count of this gauge’s own units, not credits.

If you leave it out: QuotaStack rejects the call. Must be a whole number above zero.

policyoptionalstring

What to do when the change would cross the cap. Only incrementGauge and its external_id twin act on this. decrementGauge takes the field too, but ignores it — a decrement can never cross the cap upward.

If you leave it out: Left out, QuotaStack uses block.

  • blockRefuse the increment. The count stays where it was.
  • allowLet the increment through, even past the cap. headroom in the response goes negative.
metadataoptionalobject

Your own key-value tags. QuotaStack takes this field but throws it away — nothing is stored.

If you leave it out: Nothing changes either way. The field has no effect.

Request
{
  "amount": 1
}
Response 200
{
  "billable_metric_key": "seats",
  "current": 2,
  "cap": 5,
  "allowed": true,
  "headroom": 3
}
Response fields
FieldTypeMeaning
billable_metric_keyrequiredstring

The gauge metric this change touched.

currentrequiredinteger (int64)

The gauge’s count after this change.

Unit: a count of this gauge’s own units, not credits.

caprequiredinteger (int64)

The cap in force right now. Comes from the customer’s plan variant, or the metric’s own default value.

Unit: a count of this gauge’s own units, not credits.

allowedrequiredboolean

Whether current still sits below cap, after this change.

headroomrequiredinteger (int64)

cap minus current. Negative once policy: allow lets an increment cross the cap.

Unit: a count of this gauge’s own units, not credits.

Errors

  • 400amount is zero or negative, or the metric is not a gauge type. bad-request
  • 401Missing or invalid authentication.
  • 404No customer here has this external_id, or no billable metric has this key at all. not-found
  • 409The decrement would take the count below zero. Or another mutation on the same gauge ran at the same time. Retry either one. conflict