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.
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
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
| Parameter | Location | Required | Default | Description |
|---|---|---|---|---|
customer_id / external_id | path | yes | — | The customer to check, in either ID form. |
billable_metric_key | path | yes | — | The metric key (e.g. chat_message, look, api_call). |
units | query | no | 1 | How 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
| Field | Type | Description |
|---|---|---|
allowed | boolean | Whether the customer can perform the action. |
balance | int64 | Total millicredits in the account. |
reserved_balance | int64 | Millicredits held by active reservations. |
effective_balance | int64 | balance - reserved_balance - pending_balance. The usable amount. |
estimated_cost | int64 | Millicredits this operation would cost, computed from the active metering rule. |
balance_after | int64 | effective_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_status | string or null | The customer’s subscription status (active, trialing, overdue, etc.), or null if no subscription. |
overage_policy | string | The 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
allowornotify,allowed = trueregardless of balance. A negativebalance_aftersignals 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
overdueand the plan variant hasallow_usage_while_overdue = false,allowed = falseeven 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:
| Where | Scope |
|---|---|
overage on the consume response | The single consume that accrued it |
total_overage / overage_by_billable_metric on subscription.renewed | The 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 type | Formula | Example |
|---|---|---|
flat | base_cost (fixed, ignores units) | Plan purchase: base_cost = 99000 mc. Checking 1 unit costs 99,000 mc. |
per_unit | unit_cost * units | Chat message: unit_cost = 1000 mc. Checking 5 units costs 5,000 mc. |
tiered | Graduated or volume pricing across tiers | See 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:
| Field | Type | Description |
|---|---|---|
allowed | boolean | Whether the customer can perform at least 1 unit. |
estimated_cost_per_unit | int64 | Millicredits 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_units | int64 | How 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_type | string | The 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:
| Endpoint | Freshness | Typical latency |
|---|---|---|
Single-metric (/entitlements/{metric}) | Always live — reflects the balance at request time | 5-15ms |
Bulk (/entitlements) | Up to 30 seconds stale | sub-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
- A billable metric is created with a
typeanddefault_value. - A plan-variant entitlement attaches that metric to a specific plan variant with a value override.
- 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.
Loading…