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.