quotastack Docs
Docs / Concepts / Entitlement Management

Entitlement Management

Entitlement management for usage-based products — check whether a customer can perform an action with sub-millisecond, cached checks on the hot path.

Mental model — An entitlement check is the bouncer at the door. Before your app starts an expensive operation, it asks QuotaStack "can this customer afford this?" — answer comes back in milliseconds, cached and ready.

Quick take

  • One endpoint answers: "Can this customer do this right now?"
  • Returns allowed, estimated cost, and remaining balance
  • Cost computed from metering rules: flat, per-unit, or tiered
  • Non-metered entitlements (boolean, gauge, static) are resolved from plan-variant attachments, not credit balance
  • Hot-path checks return in sub-millisecond latency, safe to gate every action
Check Request Has feature? Yes No Balance > 0? Yes No allowed: true allowed: false allowed: false

Entitlement Management

Entitlement management answers one question on the hot path: “Can this customer perform this action right now?”

You call it before starting work — before generating an image, sending a message, making an API call. The response tells you whether to proceed, how much it will cost, and how many more units the customer can afford.

The check endpoint

Two URL forms — pick the one that matches the ID you have (see Customer identification):

GET /v1/customers/{customer_id}/entitlements/{billable_metric_key}?units=N
GET /v1/customer-by-external-id/{external_id}/entitlements/{billable_metric_key}?units=N
ParameterLocationRequiredDefaultDescription
customer_id / external_idpathyesThe customer to check, in either ID form.
billable_metric_keypathyesThe metric key (e.g. chat_message, look, api_call).
unitsqueryno1How many units the customer wants to consume.

Example request

Check if a customer can generate 1 outfit look (which costs 1,000 mc per the metering rule). Using the external-id form:

curl "https://api.quotastack.io/v1/customer-by-external-id/user_abc/entitlements/look?units=1" \
  -H "X-API-Key: qs_live_..."

Example response

{
  "allowed": true,
  "customer_id": "019d6258-07ba-7418-83be-58f5fde53e4e",
  "external_customer_id": "user_abc",
  "billable_metric_key": "look",
  "units": 1,
  "balance": 150000,
  "reserved_balance": 10000,
  "effective_balance": 140000,
  "estimated_cost": 1000,
  "balance_after": 139000,
  "subscription_status": "active",
  "overage_policy": "block"
}

Response fields

FieldTypeDescription
allowedbooleanWhether the customer can perform the action.
balanceint64Total millicredits in the account.
reserved_balanceint64Millicredits held by active reservations.
effective_balanceint64balance - reserved_balance - pending_balance. The usable amount.
estimated_costint64Millicredits this operation would cost, computed from the active metering rule.
balance_afterint64effective_balance - estimated_cost, a projection. A negative value means the cost exceeds spendable balance: under allow/notify the excess would be recorded as overage (the stored balance itself never goes negative); under block the consume would be denied.
subscription_statusstring or nullThe customer’s subscription status (active, trialing, overdue, etc.), or null if no subscription.
overage_policystringThe effective policy for this customer: their own overage_policy when set, otherwise the tenant default. block denies when insufficient. allow permits usage beyond balance and records the excess as overage. notify behaves like allow; no webhook is sent.

When is allowed true?

  • If effective_balance >= estimated_cost, the customer has enough credits. allowed = true.
  • If the customer’s effective overage policy is allow or notify, allowed = true regardless of balance. A negative balance_after signals that consuming would drain the balance to zero and record the remainder as overage for arrears billing. See Billing Overage in Arrears for how overage is reported.
  • If the customer’s subscription is overdue and the plan variant has allow_usage_while_overdue = false, allowed = false even if balance is sufficient.

Configuring overage policy

overage_policy has two levels. The tenant sets a default. A single customer can hold its own value, which overrides that default.

Set the tenant default:

curl -X PATCH https://api.quotastack.io/v1/tenants/{tenant_id}/config \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: config-overage:{tenant_id}" \
  -H "Content-Type: application/json" \
  -d '{ "overage_policy": "block" }'

Set an override for one customer:

curl -X PATCH https://api.quotastack.io/v1/customers/{customer_id} \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: customer-overage:{customer_id}" \
  -H "Content-Type: application/json" \
  -d '{ "overage_policy": "block" }'

The rule is short: the customer’s own value wins when it is set. The tenant default applies when it is not.

This works both ways. A tenant that allows overage can still block one risky customer. A tenant that blocks overage can still allow one trusted customer.

On the customer endpoint, a bad value now returns 422 and names the field. It used to return a database error.

To clear a customer’s override, send an empty string. The stored value goes back to unset, and the tenant default applies again:

curl -X PATCH https://api.quotastack.io/v1/customers/{customer_id} \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: customer-overage-clear:{customer_id}" \
  -H "Content-Type: application/json" \
  -d '{ "overage_policy": "" }'

Sending null, or leaving the field out of the request body, leaves the stored value unchanged. This is the same rule display_name already follows.

Overage policy applies to consumption (usage events) and entitlement checks. It does not apply to manual grant or adjust operations, which are always strict.

Customers without any active subscription still have an overage policy applied.

Reading recorded overage

Overage accrued under allow/notify surfaces in three places:

WhereScope
overage on the consume responseThe single consume that accrued it
total_overage / overage_by_billable_metric on subscription.renewedThe just-ended billing period
GET /v1/customers/{customer_id}/overage?from=&to=Any window you ask for

The endpoint is the one to reach for when reconciling arrears — it does not depend on catching a webhook. from and to are required RFC3339 timestamps forming a half-open window [from, to); a bad window is a 422 and an unknown customer is a 404. A by-external-id twin exists at GET /v1/customer-by-external-id/{external_id}/overage. See Billing Overage in Arrears.

Consuming: POST /v1/entitlements/consume

check is a read-only projection. POST /v1/entitlements/consume is the atomic check-and-debit — it evaluates the same policy and commits the debit in one step, so two concurrent consumes cannot both pass a check and overdraw.

Because it moves credits, its deduplication is stronger than the standard 24-hour idempotency cache: receipts are permanent. Reusing an Idempotency-Key after the cache expires returns 200 with reason: duplicate_replay carrying the original debit’s exact outcome — including the original overage split — rather than debiting again. Reusing a key with a different body returns 409.

This is the contract to build client retry logic against. See Replay semantics on consume for the full details.

How cost is computed

The entitlement check looks up the active metering rule for the given billable_metric_key and computes cost based on the rule’s cost_type:

Cost typeFormulaExample
flatbase_cost (fixed, ignores units)Plan purchase: base_cost = 99000 mc. Checking 1 unit costs 99,000 mc.
per_unitunit_cost * unitsChat message: unit_cost = 1000 mc. Checking 5 units costs 5,000 mc.
tieredGraduated or volume pricing across tiersSee metering rules for details.

If no active metering rule exists for the metric key, the check returns a 404.

Bulk entitlement check

Retrieve entitlements for all active metrics at once. Two URL forms:

GET /v1/customers/{customer_id}/entitlements
GET /v1/customer-by-external-id/{external_id}/entitlements
curl https://api.quotastack.io/v1/customer-by-external-id/user_abc/entitlements \
  -H "X-API-Key: qs_live_..."

Response:

{
  "customer_id": "019d6258-07ba-7418-83be-58f5fde53e4e",
  "external_customer_id": "user_abc",
  "environment": "live",
  "balance": 150000,
  "reserved_balance": 10000,
  "effective_balance": 140000,
  "subscription_status": "active",
  "plan_name": "Pro",
  "entitlements": {
    "look": {
      "billable_metric_key": "look",
      "allowed": true,
      "estimated_cost_per_unit": 1000,
      "affordable_units": 140,
      "cost_type": "per_unit"
    },
    "chat_message": {
      "billable_metric_key": "chat_message",
      "allowed": true,
      "estimated_cost_per_unit": 500,
      "affordable_units": 280,
      "cost_type": "per_unit"
    }
  },
  "cached_at": "2025-01-15T10:30:00Z"
}

Each entry in the entitlements map includes:

FieldTypeDescription
allowedbooleanWhether the customer can perform at least 1 unit.
estimated_cost_per_unitint64Millicredits for a single unit of this metric. For tiered rules, this is the cost of the next unit at the customer’s current tier position (see caveat below).
affordable_unitsint64How many units the customer can afford at the current balance, computed as effective_balance / estimated_cost_per_unit. For flat rules: either 1 or 0. For a free metric (cost 0): int64 max.
cost_typestringThe metering rule type: flat, per_unit, or tiered.

Caveat for tiered rules: affordable_units assumes the per-unit cost stays constant. Crossing a tier boundary during consumption changes the rate, so the actual number of affordable units may be higher (cheaper upper tier) or lower (more expensive upper tier). Treat it as a guide, not a guarantee.

Latency and freshness

Entitlement checks are designed for the hot path. The two endpoints trade staleness for speed differently:

EndpointFreshnessTypical latency
Single-metric (/entitlements/{metric})Always live — reflects the balance at request time5-15ms
Bulk (/entitlements)Up to 30 seconds stalesub-1ms on the fast path, 5-15ms otherwise

Use the single-metric endpoint on the hot path — usage-gating, reserve→check→commit flows, anywhere a few-second-stale answer would be wrong. Use the bulk endpoint for dashboards, profile screens, and other places where 30-second staleness is acceptable.

Freshness after balance changes

Any credit mutation — a usage event, topup, grant, reservation, block expiry, or adjustment — immediately refreshes the customer’s bulk-endpoint result. Your next check after a balance change sees the up-to-date numbers without waiting for the staleness window to elapse.

Forcing a fresh result

To force the bulk endpoint to skip its staleness window, send the Cache-Control: no-cache header:

curl https://api.quotastack.io/v1/customer-by-external-id/user_abc/entitlements \
  -H "X-API-Key: qs_live_..." \
  -H "Cache-Control: no-cache"

The single-metric endpoint always computes live — no header needed.

Using entitlements on the hot path

Entitlement checks are designed for the hot path. Common patterns:

Gate UI elements. Before rendering a “Generate” button, check if the user is entitled. If allowed is false, show a disabled button with an upgrade prompt.

Pre-check before expensive operations. Before kicking off an AI generation that will cost compute resources, verify the user has credits. This avoids wasting infrastructure on work you cannot charge for.

Display affordable units. Use affordable_units to show the user how many actions they have remaining: “You have 140 looks left this month.”

Determine upsell moments. When affordable_units drops below a threshold, prompt the user to purchase more credits or upgrade their plan.

Example: checking if a user can generate an outfit

A fashion SaaS charges 1,000 mc (1 credit) per outfit look. Before starting the AI pipeline:

curl "https://api.quotastack.io/v1/customer-by-external-id/user_xyz/entitlements/look?units=1" \
  -H "X-API-Key: qs_live_..."

If allowed is true, proceed with generation. After generation completes, record the usage event to debit the credits. If you need to hold credits during the generation, use a reservation instead.

If allowed is false, return an error to the user and suggest they purchase a credit pack or upgrade.

Non-metered entitlements

Not every entitlement is about credit balance. Billable metrics with types boolean, gauge, or static represent feature access, limits, and configuration that customers inherit from their plan variant — no credits involved.

Boolean — feature flags

“Does this customer have SSO?” The answer comes from the plan variant’s entitlement attachment on the sso metric. If the variant attaches {"enabled": true}, the customer has SSO. Otherwise, the metric’s default_value applies (typically {"enabled": false}).

Gauge — count-with-cap

“How many seats does this customer get?” The answer is the {"cap": N} value attached to the max_seats metric on their plan variant. A Free plan might attach {"cap": 5}, while Pro attaches {"cap": 50}.

Static — arbitrary configuration

“What rate limits apply to this customer?” The answer is a JSON config blob — {"config": {"rpm": 1000, "models": ["gpt-4", "claude-sonnet"]}} — attached to the plan variant. Your app reads it and applies the constraints.

How non-metered entitlements resolve

  1. A billable metric is created with a type and default_value.
  2. A plan-variant entitlement attaches that metric to a specific plan variant with a value override.
  3. When a customer subscribes, they inherit entitlements from their subscription’s plan variant.

If no plan-variant entitlement exists for a metric, the metric’s default_value is the fallback. This means you can define sensible defaults (SSO off, 5 seats) and only override them on higher-tier variants.

The metered type continues to work exactly as described above — all existing credit-balance entitlement checking behavior is unchanged. Metered entitlements don’t need explicit plan-variant attachments; they’re governed by credit grants and metering rules.

Common mistakes

Don't skip entitlement checks "for performance"

Cached checks are sub-millisecond. Skipping them lets a customer keep consuming past zero — silently accruing overage you have to bill in arrears, or getting work you never charged for. Usually worse than the latency you saved.

Don't treat allowed: true as a binding promise

Between the check and the actual usage, another request could drain the balance. For long-running operations, reserve credits instead.

Don't assume all entitlement checks hit the credit balance

Boolean, gauge, and static entitlements are resolved from plan-variant attachments — no credits involved.