create_plan
Create a subscription, a time-limited pass or a pass series on a project — one tool, tagged by kind.
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.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Fires one event
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. It reaches beyond Subscriby: a connector, a provider or a member.
Arguments
project_id*stringUUID of the parent project.
kind*stringsubscription (access starts at payment and renews on a cycle), pass (one dated access window per purchase), or pass_series (a season ticket over many of your pass plans' windows). Defaults to subscription. Decides which nested block below is required.
name*stringPlan name (min 5, max 255 chars, unique per project).
currency_id*stringUUID of the currency. Must be supported by an active payment method on the project.
price*numberWhat one purchase costs, as a decimal. On a pass this buys one window; on a pass_series it buys the whole slate, once. At least the $1.00 USD equivalent — payment providers reject smaller amounts. 0 publishes a free plan and requires the Starter or Growth tier.
resourcesarrayoptionalUUIDs of project_resources this plan grants access to. Required on kind subscription and kind pass. OPTIONAL on kind pass_series, where it means a lounge the holder keeps for the whole span — each pass in a series already grants its own plan's resources.
descriptionstringoptionalOptional plan description. Max 1000 chars, HTML is filtered.
activebooleanoptionalDefaults to true. Set false to create in draft state.
sales_capintegeroptionalOptional. 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.
eligibilityobjectoptionalAudience restrictions: {newcomers_only?: bool, customers_only?: bool, churned_only?: bool, single_use?: bool, access_codes_only?: bool}. The first three are mutually exclusive.
billingobjectoptionalREQUIRED when kind=subscription, refused otherwise. {billing_cycle: days|weeks|months|years|lifetime, billing_cycle_count: int 1-99, recurring?: bool, disabled_renewal?: bool, trial_days?: int 0-365, trial_cardless?: bool, trial_type?: string}. lifetime requires billing_cycle_count=1. recurring must be false for crypto / platform currencies.
passobjectoptionalREQUIRED when kind=pass, refused otherwise. {timezone: IANA zone e.g. America/New_York, schedule_mode?: repeating|fixed, recurrence?: daily|weekly|monthly, recurrence_ends_at?: ISO-8601, sales_cutoff_minutes?: int, sales_cutoff_anchor?: before_start|before_end, slots?: array, windows?: array}. Slot times are local wall-clock in `timezone` and survive daylight saving. Each slot is {weekday?: 0-6 with 0=Sunday, day_of_month?: 1-31, start_time: "HH:MM", duration_minutes: int} — each carries its own duration, so a plan can mix lengths, and a day_of_month of 29-31 skips months that lack the day. `windows` places explicit dates for schedule_mode=fixed: {starts_at: "YYYY-MM-DD HH:MM", duration_minutes: int}. sales_cutoff_anchor before_start (default) stops sales that many minutes before a window opens; before_end keeps it on sale while it runs, requires at least 1, and must be under the shortest slot duration_minutes.
pass_seriesobjectoptionalREQUIRED when kind=pass_series, refused otherwise. {window_ids: array of pass-window UUIDs, prevent_overlaps?: bool, sales_cutoff_minutes?: int, sales_cutoff_anchor?: before_start|before_first_end|before_last_start|before_end, seat_cap?: int, successor_plan_id?: UUID, presale_hours?: int 1-8760, blackout_window_ids?: array, rules?: array}. A series points at windows that ALREADY EXIST on your pass plans — it never creates any — so get their UUIDs from the `list_pass_windows` tool first. Needs at least two windows, or a rule that will find them. `rules` grows the slate automatically and keeps doing so after holders have bought, granting new matches to them at no charge: each rule is {source_plan_id: UUID of a pass plan, kind: date_range|next_n, from_at?: ISO-8601, to_at?: ISO-8601, take?: int}, where date_range takes every window starting in the period and next_n takes the next `take` windows and then stops. sales_cutoff_anchor before_start closes sales before the FIRST window opens; before_first_end keeps the season on sale into that opening window and closes a set number of minutes (at least 5) before it ends, so a latecomer can still join on the night; before_last_start keeps it on sale until the LAST window opens, so a buyer always gets at least one whole date; before_end keeps the season on sale right through, closing before the LAST window ends. All three sell at full price for whatever remains, and windows that already ran are never issued.
What it returns
{ "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.
How it fails
VALIDATION_FAILEDa nested block that does not match kind; missing resources on a kind that requires them; lifetime-count mismatch; mutual-exclusion violated; a next_n rule with no take; a slate under two windows; a seat cap of 0; a successor that is not another series; or the underlying Action policy rejecting (name collision, unsupported currency, recurring-vs-crypto conflict).
RESOURCE_NOT_FOUNDproject_id doesn't exist in your token's scope.
TOKEN_MISSING_ABILITYtoken lacks project-subscription-plan:create.
TEAM_TIER_REQUIREDkind: pass or kind: pass_series without Time-Limited Passes, or a free plan on the Free tier.
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.
How is this guide?
Last updated on
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.