Setting Up a SaaS Pricing Matrix with Typed Entitlements
End-to-end walkthrough of defining typed billable metrics and attaching them to plan variants to build a complete Free vs Pro feature matrix.
Setting Up a SaaS Pricing Matrix with Typed Entitlements
This recipe walks through building a complete pricing matrix using all four metric types. By the end, you’ll have a Free and Pro tier with boolean feature flags, seat caps, model access config, and credit-metered API calls.
Step 1: Create the billable metrics
Define the four metrics that make up your pricing matrix. Each has a type and a sensible default value.
SSO access (boolean):
curl -X POST https://api.quotastack.io/v1/billable-metrics \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: metric-sso" \
-d '{
"key": "sso",
"name": "SSO Access",
"description": "Single sign-on via SAML/OIDC",
"type": "boolean",
"default_value": {"enabled": false}
}'
Team seats (gauge):
curl -X POST https://api.quotastack.io/v1/billable-metrics \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: metric-max-seats" \
-d '{
"key": "max_seats",
"name": "Team Seats",
"description": "Maximum team members allowed",
"type": "gauge",
"default_value": {"cap": 1}
}'
API calls (metered):
curl -X POST https://api.quotastack.io/v1/billable-metrics \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: metric-api-call" \
-d '{
"key": "api_call",
"name": "API Call",
"description": "One API request to the service"
}'
No type needed — defaults to metered.
Model access (static):
curl -X POST https://api.quotastack.io/v1/billable-metrics \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: metric-model-access" \
-d '{
"key": "model_access",
"name": "AI Model Access",
"description": "Which AI models are available",
"type": "static",
"default_value": {"config": {"models": []}}
}'
Step 2: Attach metrics to the Free variant
Assuming you have a plan plan_01 with a Free variant var_free:
# SSO off
curl -X POST https://api.quotastack.io/v1/plans/plan_01/variants/var_free/entitlements \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attach-free-sso" \
-d '{"billable_metric_key": "sso", "value": {"enabled": false}}'
# 5 seats
curl -X POST https://api.quotastack.io/v1/plans/plan_01/variants/var_free/entitlements \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attach-free-seats" \
-d '{"billable_metric_key": "max_seats", "value": {"cap": 5}}'
# Basic models only
curl -X POST https://api.quotastack.io/v1/plans/plan_01/variants/var_free/entitlements \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attach-free-models" \
-d '{"billable_metric_key": "model_access", "value": {"config": {"models": ["gpt-3.5"]}}}'
No attachment needed for api_call — it’s metered, governed by credit grants and the metering rule.
Step 3: Attach metrics to the Pro variant
# SSO on
curl -X POST https://api.quotastack.io/v1/plans/plan_01/variants/var_pro/entitlements \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attach-pro-sso" \
-d '{"billable_metric_key": "sso", "value": {"enabled": true}}'
# 50 seats
curl -X POST https://api.quotastack.io/v1/plans/plan_01/variants/var_pro/entitlements \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attach-pro-seats" \
-d '{"billable_metric_key": "max_seats", "value": {"cap": 50}}'
# All models
curl -X POST https://api.quotastack.io/v1/plans/plan_01/variants/var_pro/entitlements \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attach-pro-models" \
-d '{"billable_metric_key": "model_access", "value": {"config": {"models": ["gpt-4", "claude-sonnet", "gpt-3.5"]}}}'
Step 4: Verify the matrix
List entitlements for each variant to confirm the setup:
# Free variant
curl https://api.quotastack.io/v1/plans/plan_01/variants/var_free/entitlements \
-H "X-API-Key: qs_live_..."
{
"data": [
{"billable_metric_key": "sso", "value": {"enabled": false}, "metric_type": "boolean"},
{"billable_metric_key": "max_seats", "value": {"cap": 5}, "metric_type": "gauge"},
{"billable_metric_key": "model_access", "value": {"config": {"models": ["gpt-3.5"]}}, "metric_type": "static"}
]
}
# Pro variant
curl https://api.quotastack.io/v1/plans/plan_01/variants/var_pro/entitlements \
-H "X-API-Key: qs_live_..."
{
"data": [
{"billable_metric_key": "sso", "value": {"enabled": true}, "metric_type": "boolean"},
{"billable_metric_key": "max_seats", "value": {"cap": 50}, "metric_type": "gauge"},
{"billable_metric_key": "model_access", "value": {"config": {"models": ["gpt-4", "claude-sonnet", "gpt-3.5"]}}, "metric_type": "static"}
]
}
Step 5: How customers inherit entitlements
When a customer subscribes to the Pro variant, they “have”:
- SSO: enabled (boolean check)
- Seats: up to 50 (gauge check)
- Models: GPT-4, Claude Sonnet, GPT-3.5 (static config read)
- API calls: as many as their credit balance allows (metered, standard entitlement check)
Your application code reads these values and enforces them:
- At login: check the
ssoentitlement to decide whether to redirect to the SAML provider - At team invite: check
max_seatsagainst current team size - At model selection: read
model_accessconfig and filter the model picker - Before each API call: run a standard credit-based entitlement check on
api_call
Updating entitlements mid-cycle
If you upgrade Pro from 50 to 100 seats mid-cycle:
curl -X PATCH https://api.quotastack.io/v1/plans/plan_01/variants/var_pro/entitlements/max_seats \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-d '{"value": {"cap": 100}}'
All customers on the Pro variant immediately inherit the updated cap — no migration, no per-customer update needed.
Common pitfalls
- Duplicate attach: If you try to attach
ssoto the Pro variant a second time, you get a409. Use PATCH to update the value, or DELETE then re-attach. - Wrong value shape: Attaching
{"cap": 50}to abooleanmetric returns422. The value shape must match the metric type. - Float caps:
{"cap": 5.5}is invalid for gauge metrics. Caps must be non-negative integers. - Forgetting defaults: If you don’t attach a metric to a variant, the metric’s
default_valueapplies. Make sure your defaults make sense for the “no attachment” case.
Step 6: Price the metered metric
Steps 1–5 set up the feature matrix, but the api_call metric has no pricing yet. Without a metering rule, usage events for it would be rejected. Add one:
curl -X POST https://api.quotastack.io/v1/metering-rules \
-H "X-API-Key: qs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rule-api-call" \
-d '{
"billable_metric_key": "api_call",
"cost_type": "per_unit",
"unit_cost": 500
}'
500mc per call (0.5 credits). A Pro customer with 50,000 monthly credits gets 100,000 API calls before running out.
Step 7: See it work end-to-end
Grant 100,000mc to a test customer, then fire 10 API calls via POST /v1/usage with "billable_metric_key": "api_call", "units": 10. The debit processes asynchronously (202 Accepted) — 10 × 500mc = 5,000mc deducted, leaving 95,000mc.
Step 8: Check all four entitlement types
Each metric type returns a different response shape from the entitlement check endpoint. Here’s what your app code sees for a Pro customer:
| Metric | Check | Response | Your app does… |
|---|---|---|---|
api_call (metered) | GET .../entitlements/api_call | { "allowed": true, "balance": 95000, "estimated_cost": 500 } | Gate the request, show “X calls remaining” |
sso (boolean) | GET .../entitlements/sso | { "allowed": true, "value": { "enabled": true } } | Toggle SAML login on/off |
max_seats (gauge) | GET .../entitlements/max_seats | { "allowed": true, "value": { "cap": 50 }, "current": 12 } | Block invite when current >= cap |
model_access (static) | GET .../entitlements/model_access | { "value": { "config": { "models": ["gpt-4", "claude-sonnet", "gpt-3.5"] } } } | Filter the model picker UI |
The key pattern: metered metrics answer “can they afford it?”, boolean answers “is it on?”, gauge answers “is there room?”, and static gives your app a config blob to interpret however it wants.
Loading…