create_plan
Create a subscription, a time-limited pass or a pass series on a project — one tool, tagged by kind.
Purpose
Create a plan on a project. Delegates to an Action that enforces resource-sync, the plan policy, and cache invalidation before a plan.created event fires.
Every plan has a kind, and the kind decides which one nested block you send with it:
kind | Send | Sells |
|---|---|---|
subscription | billing | Access that begins at payment and renews on a cycle. The default. |
pass | pass | One dated access window per purchase. |
pass_series | pass_series | A slate of other pass plans' windows, sold once — a season ticket. |
Sending the wrong block is refused
A pass carrying billing, or a subscription carrying pass_series, fails
with VALIDATION_FAILED naming the offending key. It is not silently ignored
— that would let you believe you had set a billing cycle on a pass, where a
cycle means nothing.
The following rules are enforced — identical inputs are rejected by the dashboard too:
nameis 5–255 chars and unique per project.currency_idmust be supported by an active payment method on the project.pricemust be at least the $1.00 USD equivalent in the plan currency.0publishes a free plan, which only the Starter and Growth plans can sell; on Free the call is rejected withTEAM_TIER_REQUIRED.eligibility.newcomers_only,eligibility.customers_onlyandeligibility.churned_onlyare mutually exclusive.resourcesmust link to at least one existing project resource — except onkind: pass_series, where it is optional. Each window in a series grants that window's own plan's resources, so a series with none still delivers what was sold; anything linked there is a lounge open for the whole span. Never pass a resource one of the slate's own windows opens: a lounge is granted at purchase with no window and kept all season, so the holder gets that channel permanently and the date it was scheduled for stops gating anything. Pass a holders-only room, or nothing.kind: passandkind: pass_seriesboth require Time-Limited Passes — bundled with Growth, or the Passes Addon on Free and Starter. On any other tier the call is rejected.
Required ability
project-subscription-plan:create
Input schema
{
"type": "object",
"required": ["project_id", "kind", "name", "currency_id", "price"],
"properties": {
"project_id": {
"type": "string",
"description": "UUID of the parent project."
},
"kind": {
"type": "string",
"description": "subscription, pass or pass_series. Defaults to subscription. Decides which nested block is required."
},
"name": {
"type": "string",
"description": "Plan name (min 5, max 255 chars, unique per project)."
},
"currency_id": {
"type": "string",
"description": "UUID of the currency. Must be supported by an active payment method on the project."
},
"price": {
"type": "number",
"description": "What one purchase costs. On a pass this buys one window; on a pass_series it buys the whole slate, once."
},
"resources": {
"type": "array",
"description": "UUIDs of project resources. Required on subscription and pass; optional on pass_series, where it means a lounge."
},
"description": {
"type": "string",
"description": "Optional. Max 1000 chars, HTML filtered."
},
"active": {
"type": "boolean",
"description": "Defaults to true. Set false to create in draft state."
},
"sales_cap": {
"type": "integer",
"description": "Optional. Pause the plan automatically after this many successful purchases (1-100000); the creator publishes it again for another batch. Omit or null for no limit."
},
"eligibility": {
"type": "object",
"description": "{newcomers_only?, customers_only?, churned_only?, single_use?, access_codes_only?}"
},
"billing": {
"type": "object",
"description": "Required when kind=subscription, refused otherwise."
},
"pass": {
"type": "object",
"description": "Required when kind=pass, refused otherwise."
},
"pass_series": {
"type": "object",
"description": "Required when kind=pass_series, refused otherwise."
}
}
}Output shape
{
"data": {
"id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b",
"kind": "subscription",
"name": "Premium Monthly",
"project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13",
"price": "29.00",
"cadence": "Per Month",
"active": true
}
}cadence is the human-readable duration string the portal and the bot show — "Per Month",
"Per 3 Hours", "For all 10 passes". Use it rather than deriving one yourself.
kind: subscription
{
"kind": "subscription",
"billing": {
"billing_cycle": "months",
"billing_cycle_count": 1,
"recurring": true,
"trial_days": 7
}
}| Field | Type | Notes |
|---|---|---|
billing.billing_cycle | string | days, weeks, months, years, lifetime. |
billing.billing_cycle_count | integer | 1–99. Forced to 1 when billing_cycle is lifetime. |
billing.recurring | boolean | Rejected as true for crypto or platform currencies. |
billing.disabled_renewal | boolean | Charges once, then lapses. |
billing.trial_days | integer | 0–365. |
billing.trial_cardless | boolean | Whether the trial starts without a payment method. |
billing.trial_type | string | A TrialModeType value. |
kind: pass
Sells one scheduled access window per purchase. The plan owns its own windows and generates them from a recurrence.
{
"kind": "pass",
"pass": {
"timezone": "America/New_York",
"schedule_mode": "repeating",
"recurrence": "weekly",
"sales_cutoff_minutes": 60,
"sales_cutoff_anchor": "before_start",
"slots": [
{ "weekday": 4, "start_time": "19:00", "duration_minutes": 180 },
{ "weekday": 0, "start_time": "09:00", "duration_minutes": 840 }
]
}
}| Field | Type | Notes |
|---|---|---|
pass.timezone | string | IANA zone, e.g. America/New_York. Slot times are local wall-clock in this zone and survive daylight saving. Legacy names are resolved, so Asia/Calcutta is stored as Asia/Kolkata. Required. |
pass.schedule_mode | string | repeating (generate from slots) or fixed. Defaults to repeating. |
pass.recurrence | string | daily, weekly or monthly. Decides which slot fields apply. |
pass.recurrence_ends_at | timestamp | Optional. When window generation stops. |
pass.sales_cutoff_minutes | integer | null | Stop selling a window this many minutes before the moment sales_cutoff_anchor names. Omit to sell until the window begins. |
pass.sales_cutoff_anchor | string | before_start (default) closes sales before a window opens and nothing is sold once it is running. before_end keeps it on sale while it runs, so a buyer can join a session already in progress. before_end requires at least 5, must be under the shortest slot duration_minutes, and is refused when that shortest slot is 5 or less. |
pass.slots[] | array | Window definitions. Replaces the whole schedule on update. |
pass.slots[].weekday | integer | null | 0–6, 0 = Sunday. Weekly recurrence only. |
pass.slots[].day_of_month | integer | null | 1–31. Monthly only. Values of 29–31 skip months that lack the day. |
pass.slots[].start_time | string HH:MM | Local wall-clock start in pass.timezone. |
pass.slots[].duration_minutes | integer | Each slot has its own, so one plan can mix a 3-hour and a 14-hour window. |
pass.windows[] | array | Explicitly dated windows for fixed mode. Added, never replacing. Each: {starts_at, duration_minutes}. |
Updating pass.slots rebuilds future windows. Windows a customer has already bought keep their
original times and are never moved or deleted; only unsold future windows are regenerated.
kind: pass_series
Sells a curated slate of other pass plans' windows for one payment — a season ticket. It
owns no windows of its own, which is the whole distinction from kind: pass.
Get the window ids first
pass_series.window_ids names windows that already exist. Call
list_pass_windows to find them — there is no
other way to obtain a window UUID, so a series cannot be authored without it.
{
"kind": "pass_series",
"pass_series": {
"window_ids": ["3d5a8c72-b016-4e94-8fa7-61c209d4e738", "psw_01HY..."],
"prevent_overlaps": true,
"seat_cap": 50,
"presale_hours": 48,
"rules": [
{
"source_plan_id": "pln_01HZ...",
"kind": "date_range",
"from_at": "2026-09-01T00:00:00Z",
"to_at": "2026-12-01T00:00:00Z"
}
]
}
}| Field | Type | Notes |
|---|---|---|
pass_series.window_ids[] | array | Pass-window UUIDs from list_pass_windows. Max 120. Needs at least two, unless a rule will supply them. |
pass_series.rules[] | array | Automatic inclusion. Max 20. See below. |
pass_series.blackout_window_ids[] | array | Windows a rule matches but you want permanently excluded. |
pass_series.prevent_overlaps | boolean | Defaults true. Refuses to absorb a window clashing with one already on the slate. |
pass_series.seat_cap | integer | null | Concurrent holder limit. null is unlimited; 0 is refused — take a plan off sale with publish_plan instead. |
pass_series.sales_cutoff_minutes | integer | Measured against the whole season. |
pass_series.sales_cutoff_anchor | string | before_start closes sales before the first window opens. before_first_end keeps the season on sale into that opening window and closes at least 5 minutes before it ends, so a latecomer is still admitted to it. before_end keeps the season on sale until the last window ends. All three charge full price for whatever remains. Series-only values are refused on a pass plan. |
pass_series.successor_plan_id | string | null | Another kind: pass_series plan on the same project. A series cannot lead to itself. |
pass_series.presale_hours | integer | null | 1–8760. How long the successor is held for this season's holders before general sale. |
Rules keep working after the save
A rule describes windows rather than naming them, and it keeps matching: a window
scheduled later is absorbed into the slate and granted to everyone already holding the series,
at no extra charge. That is the behaviour handpicked window_ids deliberately do not have.
They compose — most real seasons use both.
Rule kind | Takes |
|---|---|
date_range | Every window on source_plan_id starting between from_at and to_at. Either bound may be omitted. |
next_n | The next take windows, then stops. Never tops itself back up — a window freeing up later does not make it reach for another. |
take is required when kind is next_n. A count rule with no count is incomplete, not
"all of them", and is refused.
source_plan_id must be a kind: pass plan on the same project.
Example prompts
"Create a $29/month Premium plan on project
7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13with a 7-day trial, linked to resourcersc_01HY...."
"Add a one-time 'Starter Access' plan priced at $49 with lifetime duration, linked to the Onboarding Pack resource."
"Create a weekly Sunday pass on project
7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13at $15, 9am New York, three hours long."
"Build a season ticket from the next ten Match Day windows and price it at $99 with 50 seats."
Failure modes
VALIDATION_FAILED— a nested block that does not matchkind; missingresourceson a kind that requires them; lifetime-count mismatch; mutual-exclusion violated; anext_nrule with notake; a slate under two windows; a seat cap of0; a successor that is not another series; or the underlying Action policy rejecting (name collision, unsupported currency, recurring-vs-crypto conflict).RESOURCE_NOT_FOUND—project_iddoesn't exist in your token's scope.TOKEN_MISSING_ABILITY— token lacksproject-subscription-plan:create.TEAM_TIER_REQUIRED—kind: passorkind: pass_serieswithout Time-Limited Passes, or a free plan on the Free tier.
Related
list_pass_windows— where a series' window ids come from.publish_planlist_plans- Pass Series guide
- Plans API
How is this guide?
create_pass_window
Place one dated access window by hand on a time-limited pass plan. Marked manual, so a schedule rebuild keeps it; emits pass.window_scheduled.
delete_plan
Soft-delete a plan so it leaves the portal, the bot and every list. Existing subscriptions run to their end. Refused while pass holders have windows ahead.