Model plans and entitlements
Before you sell anything, you decide what your product can gate and what each tier unlocks. In Licet that's two concepts:
- Entitlement definitions — the vocabulary of things your application
understands. Two kinds: a feature (boolean, e.g.
white_label) or a limit (numeric, e.g.seats). - Plans — named tiers (Free, Pro, Enterprise…) that assign values to those entitlements and map to a Stripe price for checkout.
The names you choose here are the exact strings your product checks with the
SDK — license.Has("white_label"), license.Limit("seats") — so treat them
like an API you're committing to.
All the endpoints below need a key with the admin scope; the portal does the
same things interactively.
Define the vocabulary
curl -X POST https://api.licet.app/v1/apps/{app_id}/entitlement-defs \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "key": "seats", "kind": "limit", "display_name": "Seats", "default": "1" }'
{
"id": "ent_…",
"key": "seats",
"kind": "limit",
"display_name": "Seats",
"default": "1"
}
keymust match^[a-z][a-z0-9_]*$— lowercase, digits, underscores. It can't be renamed later without breaking installs that check it, so choose carefully.kindisfeature(default must betrue/false) orlimit(default must be an integer).- The
defaultapplies wherever a plan doesn't set its own value.
Deleting a definition (DELETE …/entitlement-defs/{ent_id}) also removes any
values plans had for it — but licences already issued keep their snapshot
(more on snapshots below).
Create plans
curl -X POST https://api.licet.app/v1/apps/{app_id}/plans \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro",
"code": "pro",
"entitlements": [
{ "key": "white_label", "value": "true" },
{ "key": "seats", "value": "10" }
]
}'
{
"id": "pln_…",
"name": "Pro",
"code": "pro",
"status": "active",
"active_licences": 0,
"entitlements": [{ "entitlement_def_id": "ent_…", "value": "true" }]
}
- Every entitlement
keymust already exist as a definition. codeis unique per application and purely vendor-facing — everything in the platform (prices, licences, checkout) binds to theid.- To sell the plan, map it to a Stripe price in the portal (Payments → Plan prices) — see Sell a plan with Checkout.
Editing a plan
PATCH /v1/apps/{app_id}/plans/{plan_id} takes the same body. One rule worth
underlining: when you include entitlements, the list is the complete set
of overrides — values you list are applied, values you omit revert to their
definition defaults, and an empty list [] clears every override. Omit the
field entirely to leave entitlements untouched.
Snapshots: what existing customers experience
A licence captures its entitlements at issue time and keeps that snapshot. Editing a plan, or deleting a definition, changes what future buyers get — it does not rewrite licences already in the field. If you need to change what an existing customer has, act on their licence, not the plan.
This is deliberate: a plan edit can't accidentally downgrade thousands of paying customers, and a licence always means what it meant when it was sold.
Retiring a plan
Plans are never deleted — they're archived:
curl -X POST https://api.licet.app/v1/apps/{app_id}/plans/{plan_id}/archive \
-H "Authorization: Bearer sk_live_…"
Archiving does three things:
- The plan's status becomes
retired— new licences can't be issued against it, and checkout for it stops. - Every active licence on the plan is suspended (not revoked), each
emitting a
licence.suspendedwebhook withreason: "plan_archived". - The response tells you how many licences that touched
(
"suspended_licences": …).
POST …/plans/{plan_id}/restore brings the plan back — but deliberately does
not reactivate the suspended licences. Reactivation is a per-licence
decision (POST /v1/licences/{id}/reactivate), so a plan restored for one
customer doesn't silently re-enable a thousand others.
If archiving-suspends is not what you want — say you're sunsetting a tier but existing customers keep it for life — leave the plan active and simply remove its price mapping so it can't be bought.
Naming advice
- Name entitlements after capabilities, not tiers:
api_access, notpro_features. Tiers change; capabilities don't. - Prefer a
limitover afeaturewhen there's any chance of "how many" mattering later —seats: 1today beats migratinghas_seatstomorrow. - Keep the set small. Every key is a string your product checks forever.