Plans API¶
Base: /org/plans
Address plans by key.
Every plan object includes archived_at: null, or the UTC time the plan was archived (2026-03-01T00:00:00.000Z). Archiving does not change existing subscriptions, move subscribers, or send a webhook. Grants on an archived plan can still be edited.
Create plan¶
{
"key": "pro",
"name": "Pro",
"kind": "catalog",
"grants": [
{ "feature": "sso", "kind": "flag" },
{ "feature": "csv_export", "kind": "flag" },
{ "feature": "seats", "kind": "limit", "limit": 10 },
{ "feature": "storage_gb", "kind": "limit", "limit": null },
{ "feature": "support_tier", "kind": "config", "value": "priority" }
]
}
| Field | Required | Rules |
|---|---|---|
name |
yes | 1–255 |
kind |
yes | catalog | custom |
key |
yes | unique per org; max 64; lowercase letters, numbers, underscores, dashes; immutable |
grants |
no | array of grants; default [] |
Grant object¶
| Feature kind | Body |
|---|---|
flag |
{ "feature": "<key>", "kind": "flag" } |
limit |
{ "feature": "<key>", "kind": "limit", "limit": <int≥0 \| null> } |
config |
{ "feature": "<key>", "kind": "config", "value": <value> } |
limit: null = unlimited. A config value must match the feature's value_type (an enum's must be one of its options) and can't be null; to leave a config unset on a plan, leave the grant out. See config value types. Grant kind must match the catalog feature’s kind. Duplicate feature keys in one request → 422.
201 — plan object (archived_at is null)
409 — key already exists
422 — unknown feature key, kind mismatch, config value of the wrong type, invalid grant, or an unknown field on the plan or on a grant. Properties that are not in the tables above are rejected.
List plans¶
Query (optional):
| Param | Description |
|---|---|
kind |
catalog or custom |
include_archived |
true or 1 includes archived plans. false, 0, or omitted leaves them out. Any other value → 422, one detail at include_archived: include_archived must be true, false, 1, or 0. |
Unknown query parameters on this route are 422, one detail each. ?archived=true → Unknown query parameter archived; did you mean include_archived?
200 { "plans": [ … ] } — archived plans left out unless include_archived is true or 1
Get plan¶
200 — plan, including when it is archived
{
"key": "pro",
"name": "Pro",
"kind": "catalog",
"grants": [
{ "feature": "csv_export", "kind": "flag" },
{ "feature": "seats", "kind": "limit", "limit": 10 },
{ "feature": "sso", "kind": "flag" }
],
"created_at": "2026-01-15T12:00:00.000Z",
"updated_at": "2026-01-15T12:00:00.000Z",
"archived_at": null
}
Update plan¶
{
"name": "Pro (2026)",
"grants": [
{ "feature": "sso", "kind": "flag" },
{ "feature": "seats", "kind": "limit", "limit": 25 }
]
}
| Field | Notes |
|---|---|
name |
optional |
grants |
optional; replaces full grant list when present |
key / kind |
immutable after create |
Unknown fields on the plan or on a grant are 422. archived_at is not a body field: Unknown field archived_at; use POST /org/plans/{key}/archive or /unarchive.
200 — plan
Warning
Changing grants immediately affects entitlement resolution for all active subscribers on this plan. That includes an archived plan: its grants still drive existing subscribers.
Archive plan¶
No body. Sets archived_at if it was null. Idempotent: archiving a plan that is already archived returns it unchanged (archived_at and updated_at stay as they were).
200 — plan
{
"key": "pro",
"name": "Pro",
"kind": "catalog",
"grants": [],
"created_at": "2026-01-15T12:00:00.000Z",
"updated_at": "2026-04-01T00:00:00.000Z",
"archived_at": "2026-04-01T00:00:00.000Z"
}
404 — plan not found
Existing subscriptions stay on this plan. No webhook.
Unarchive plan¶
No body. Sets archived_at to null. Idempotent: unarchiving a plan that is not archived returns it unchanged.
200 — plan (archived_at is null)
404 — plan not found
No webhook.
Delete plan¶
204 — deleted
409 — any subscription (including canceled) still references this plan. Message: This plan is still in subscription history; archive it instead (POST /org/plans/{key}/archive).