Skip to main content

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​

TypeWire formatExample
booleanJSON booleantrue
numberJSON number25
date"YYYY-MM-DD""2026-12-31"
datetime"YYYY-MM-DDTHH:MM:SSZ" (UTC required)"2026-12-31T23:59:00Z"
enumOne 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 label is safe.
  • Changing an option's value is 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.

BodyEffect
{"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.

ValueEffect
A scalarSets or replaces the customer's override.
nullClears 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).

caution

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. If definition is provided alongside an existing slug, it is ignored.
  • New slug + definition — when the slug does not yet exist, include a definition block with the same fields as POST /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​

HTTPCodeWhen
400entitlement_slug_invalidSlug fails the validation regex.
400entitlement_slug_reservedSlug starts with _sys_.
400entitlement_invalid_typeValue does not match the entitlement's type.
400entitlement_enum_options_missingenum type created without options.
400entitlement_enum_value_unknownValue is not one of the defined options.
400entitlement_required_needs_defaultis_required=true set with no default.
400entitlement_datetime_tz_unsupportedDatetime payload has a non-UTC offset.
400entitlement_number_precisionA number value carries more than 5 decimal places. Round it first.
400entitlement_definition_requiredInline plan-create references an unknown slug without a definition.
400entitlement_enum_value_immutablePATCH replaces an existing option value string (rename detected). Keep all existing values; only label may change.
404entitlement_not_foundSlug unknown in this tenant + environment.
404entitlement_not_attached_to_productCustomer override targets a slug not attached to the customer's plan.
409entitlement_already_existsSlug collision on create.
409entitlement_slug_type_conflictClone: target environment already has the slug with a different type. details.slug, details.existing_type, details.source_type identify the conflict.
409entitlement_in_use_cannot_remove_optionEnum option is currently in use as a default.
409entitlement_in_use_cannot_deleteDefinition is still attached to one or more plans.
409entitlement_reset_would_violate_requiredReset would leave a required entitlement with no value at any layer.
409entitlement_stored_default_unwritableThe 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.
422entitlement_pb_errorThe billing backend would not accept the write. The message carries its reason.
502entitlement_pb_errorThe billing backend could not serve the write at all. Safe to retry.