Entitlements API
Manage entitlement definitions, plan attachments, and per-customer overrides. See the Entitlements concept for the data model and value-precedence rules.
Authentication
Every entitlement endpoint accepts either an admin session token (Authorization: Bearer <JWT>) or an admin-role API key in X-API-Key. See Authentication.
Type Formats
| Type | Wire format | Example |
|---|---|---|
boolean | JSON boolean | true |
number | JSON number | 25 |
date | "YYYY-MM-DD" | "2026-12-31" |
datetime | "YYYY-MM-DDTHH:MM:SSZ" (UTC required) | "2026-12-31T23:59:00Z" |
enum | One of the option value strings | "pro" |
date is interpreted in the customer's configured timezone on the billing engine side. Use datetime for tz-agnostic moments.
Library
The library holds all entitlement definitions for the current tenant and environment.
List Definitions
GET /api/v1/entitlements
Response:
{
"entitlements": [
{
"slug": "max_seats",
"display_name": "Maximum Seats",
"description": "Number of user seats this plan allows.",
"type": "number",
"default_value": 5,
"options": null,
"is_required": false,
"created_at": "2026-04-21T09:00:00Z"
},
{
"slug": "feature_tier",
"display_name": "Feature Tier",
"description": null,
"type": "enum",
"default_value": "basic",
"options": [
{"value": "basic", "label": "Basic"},
{"value": "pro", "label": "Professional"}
],
"is_required": false,
"created_at": "2026-04-22T14:30:00Z"
}
]
}
Get a Definition
GET /api/v1/entitlements/{slug}
Returns the same item shape as the list endpoint. Returns 404 entitlement_not_found when the slug is unknown.
Create a Definition
POST /api/v1/entitlements
Request:
{
"slug": "max_seats",
"display_name": "Maximum Seats",
"description": "Number of user seats this plan allows.",
"type": "number",
"default_value": 5,
"is_required": false
}
For enum types, also include options:
{
"slug": "feature_tier",
"display_name": "Feature Tier",
"type": "enum",
"default_value": "basic",
"options": [
{"value": "basic", "label": "Basic"},
{"value": "pro", "label": "Professional"},
{"value": "enterprise", "label": "Enterprise"}
]
}
Response: 201 with the created definition.
Update a Definition
PATCH /api/v1/entitlements/{slug}
Request body (all fields optional):
{
"display_name": "Maximum User Seats",
"description": "Updated description.",
"default_value": 10,
"is_required": true,
"options": [...]
}
type and slug are immutable and cannot be patched. For enum options:
- Renaming a
labelis safe. - Changing an option's
valueis rejected. - Removing an option that is currently in use as a default returns
409.
Delete a Definition
DELETE /api/v1/entitlements/{slug}
The definition can only be deleted when no plan still has it attached. Otherwise returns 409 entitlement_in_use_cannot_delete. Detach from each plan first (see below).
Response: 204 on success.
Plan Attachments
Each plan can attach any subset of the library's entitlements, with optional plan-level default values.
List a Plan's Entitlements
GET /api/v1/products/{product_id}/entitlements
Response:
{
"entitlements": [
{
"slug": "max_seats",
"display_name": "Maximum Seats",
"type": "number",
"default_value": 5,
"product_value": 25,
"effective_value": 25,
"value_source": "product"
}
]
}
value_source is either default (no plan-level value set; library default applies) or product (plan default in effect).
Attach to a Plan / Set Plan Default
PATCH /api/v1/products/{product_id}/entitlements/{slug}
Attaches the entitlement when the plan does not carry it yet, and sets the plan default either way.
| Body | Effect |
|---|---|
{"default_value": <value>} | Set or update the plan default. |
{} or {"default_value": null} | No plan default: the library default applies. An existing plan default is removed. |
Detach from a Plan
DELETE /api/v1/products/{product_id}/entitlements/{slug}
Removes the entitlement from this plan. Other plans using the same slug are unaffected. Customer-level override rows for this slug on this plan are no longer surfaced.
Response: 204.
Customer Overrides
Per-customer overrides take precedence over the plan default. PATCH uses merge semantics: only the slugs you list are modified, and a previous override for an unlisted slug is preserved.
List a Customer's Entitlements
GET /api/v1/customers/{customer_id}/entitlements
{customer_id} takes the numeric id or the customer's external_id, as on every other customer
route. An external_id made only of digits is read as the number.
Response:
{
"entitlements": [
{
"slug": "max_seats",
"display_name": "Maximum Seats",
"type": "number",
"value": 50,
"value_source": "override"
},
{
"slug": "feature_tier",
"display_name": "Feature Tier",
"type": "enum",
"value": "pro",
"value_source": "product"
}
]
}
Set or Update Overrides
PATCH /api/v1/customers/{customer_id}/entitlements
Request:
{
"entitlements": {
"max_seats": 50,
"advanced_analytics": true,
"api_calls": null
}
}
Only the listed slugs are modified. Existing overrides on slugs not present in the body are preserved. Each value must match the entitlement's type; mismatches return 400 entitlement_invalid_type.
| Value | Effect |
|---|---|
| A scalar | Sets or replaces the customer's override. |
null | Clears the override — the effective value falls back to the plan default (or library default). A no-op if there was no override. |
Because a single call can carry both new values and resets, this is the recommended way to apply several changes at once — including resetting several overrides together.
Reset One Override
DELETE /api/v1/customers/{customer_id}/entitlements/{slug}
Clears the override for a single slug. The effective value falls back to the plan default (or library default).
Send one PATCH with null values rather than issuing several of these calls in parallel. Each request rewrites the customer's entire override list, so concurrent calls overwrite one another and an override you cleared can come back.
Reset All Overrides
DELETE /api/v1/customers/{customer_id}/entitlements
Clears every override for this customer.
Response: 204.
Inline Entitlements on Plan Create
POST /api/v1/products accepts an optional entitlements array so you can define and attach entitlements in the same call as the plan itself.
Request:
{
"name": "Pro Plan",
"subscription": { "monthly_fee": 99.00 },
"services": [...],
"entitlements": [
{ "slug": "max_seats", "default_value": 25 },
{
"slug": "feature_tier",
"default_value": "pro",
"definition": {
"display_name": "Feature Tier",
"type": "enum",
"options": [
{"value": "basic", "label": "Basic"},
{"value": "pro", "label": "Professional"}
]
}
}
]
}
Each array entry may take one of two forms:
- Existing slug —
{slug, default_value?}. The slug must already exist in the library; the entitlement is attached to the new plan with the given default. Ifdefinitionis provided alongside an existing slug, it is ignored. - New slug +
definition— when the slug does not yet exist, include adefinitionblock with the same fields asPOST /api/v1/entitlements. The library entry is created and attached in the same request.
If the slug is unknown and no definition is provided, the request fails with 400 entitlement_definition_required.
If any entitlement step fails, the whole request is rolled back: the newly-created plan is deleted, and any library entries that were freshly created in this request are removed (best-effort).
Clone a Plan
POST /api/v1/products/{product_id}/clone
Cloning a plan across environments (sandbox → live or live → sandbox) automatically replicates all entitlements attached to the source plan:
- If the target environment lacks a slug → it is created with the same type, options, and display name, then attached to the cloned plan. The source plan's per-plan default value is copied.
- If the target environment already has the slug with the same type → the existing definition is reused and attached. The source plan's per-plan default value is copied.
- If the target environment already has the slug with a different type → the clone is refused before any write with
409 entitlement_slug_type_conflict. No partial state is left in the target environment.
Request:
{
"target_environment": "live",
"name": "Pro Plan (Live)"
}
Success response: 200 — the newly created plan object in the target environment.
Conflict response (type mismatch):
{
"error": {
"code": "entitlement_slug_type_conflict",
"message": "Cannot clone entitlement 'max_seats': target environment already has this slug with type 'boolean', but source has 'number'.",
"details": {
"slug": "max_seats",
"existing_type": "boolean",
"source_type": "number"
}
}
}
To resolve a type conflict, either rename the conflicting slug in the source plan or delete (or rename) the target-environment definition first, then retry the clone.
Error Codes
| HTTP | Code | When |
|---|---|---|
| 400 | entitlement_slug_invalid | Slug fails the validation regex. |
| 400 | entitlement_slug_reserved | Slug starts with _sys_. |
| 400 | entitlement_invalid_type | Value does not match the entitlement's type. |
| 400 | entitlement_enum_options_missing | enum type created without options. |
| 400 | entitlement_enum_value_unknown | Value is not one of the defined options. |
| 400 | entitlement_required_needs_default | is_required=true set with no default. |
| 400 | entitlement_datetime_tz_unsupported | Datetime payload has a non-UTC offset. |
| 400 | entitlement_number_precision | A number value carries more than 5 decimal places. Round it first. |
| 400 | entitlement_definition_required | Inline plan-create references an unknown slug without a definition. |
| 400 | entitlement_enum_value_immutable | PATCH replaces an existing option value string (rename detected). Keep all existing values; only label may change. |
| 404 | entitlement_not_found | Slug unknown in this tenant + environment. |
| 404 | entitlement_not_attached_to_product | Customer override targets a slug not attached to the customer's plan. |
| 409 | entitlement_already_exists | Slug collision on create. |
| 409 | entitlement_slug_type_conflict | Clone: target environment already has the slug with a different type. details.slug, details.existing_type, details.source_type identify the conflict. |
| 409 | entitlement_in_use_cannot_remove_option | Enum option is currently in use as a default. |
| 409 | entitlement_in_use_cannot_delete | Definition is still attached to one or more plans. |
| 409 | entitlement_reset_would_violate_required | Reset would leave a required entitlement with no value at any layer. |
| 409 | entitlement_stored_default_unwritable | The entitlement's own stored default cannot be written to the billing backend, so a request that is otherwise valid cannot proceed. Fix the entitlement's default first. |
| 422 | entitlement_pb_error | The billing backend would not accept the write. The message carries its reason. |
| 502 | entitlement_pb_error | The billing backend could not serve the write at all. Safe to retry. |