Plan Tools
Plans are what a project sells, and every plan has a kind that decides its shape: a subscription that renews on a cycle, a pass that sells dated windows, or a series that sells a slate of windows at once.
Plans are what a project sells, and every plan has a kind that decides its shape: a subscription that renews on a cycle, a pass that sells dated windows, or a series that sells a slate of windows at once. These tools create and edit plans in that discriminated shape, put them on and off sale, arrange the storefront, and manage the dated windows a pass generates.
Tools
cancel_pass_windowCancel Pass WindowDESTRUCTIVEcreate_pass_windowCreate Pass WindowDESTRUCTIVEcreate_planCreate PlanDESTRUCTIVEdelete_planDelete PlanREADget_pass_windowGet Pass WindowREADget_planGet PlanREADlist_pass_windowsList Pass WindowsREADlist_plansList PlansDESTRUCTIVEpublish_planPublish or Unpublish PlanDESTRUCTIVEremind_pass_window_queueRemind Pass Window QueueDESTRUCTIVEreorder_plansReorder PlansDESTRUCTIVEstart_next_seasonStart Next SeasonDESTRUCTIVEupdate_planUpdate Plancancel_pass_window
DESTRUCTIVECancel one dated access window and resettle everyone holding it. Destructive — holders are messaged and money may be owed.
Take a window off the schedule and deal with the people who bought it, the way the dashboard's cancel button does. Each holder is resettled in one of three ways, decided per holder:
- rebound — moved to the plan's next window that is on sale;
- leg dropped — for a season-ticket holder, the one date is removed from their series and the rest stands;
- refund due — when the schedule has nothing left to move them to, their pass is ended and flagged for a refund.
Subscriby never moves the money itself: meta.refund_due is the count of holders the creator now
owes a refund, and the creator settles it with their payment provider.
Destructive — confirm the window with a human first
Holders are messaged the moment this runs and refunds may be owed. Read the
window back with get_pass_window, check its
holders, and have the creator confirm the exact window_id before calling.
Emits pass.window_cancelled once, plus one of
pass.holder_moved,
pass.holder_stranded or
pass_series.leg_dropped per holder. Idempotent: an
already-cancelled window is answered with zero tallies and emits nothing.
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 events
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.
Arguments
project_id*stringUUID of the project whose pass plans own the window.
window_id*stringUUID of the window to cancel.
What it returns
{ "data": { "id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738", "plan_id": "9b7c2e15-4d63-4f80-a2b1-7e5d0c9f3a46", "plan_name": "Saturday session", "starts_at": "2026-10-03T09:00:00+00:00", "ends_at": "2026-10-03T11:00:00+00:00", "timezone": "Europe/London", "local_range": "Sat 3 Oct, 10:00–12:00 BST", "duration_minutes": 120, "status": "canceled", "sellable": false, "holders": 3 }, "meta": { "rebound": 2, "refund_due": 1, "legs_dropped": 0 }}holders is the count at the moment of cancellation, so the three tallies under meta add up to
it. status is spelled canceled.
How it fails
VALIDATION_FAILEDthe window cannot be cancelled in its current state (the resettlement refused).
RESOURCE_NOT_FOUNDunknown project_id or window_id, a window of another project, or a project outside the token's scope.
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks pass-window:delete.
create_pass_window
DESTRUCTIVEPlace one dated access window by hand on a time-limited pass plan. Marked manual, so a schedule rebuild keeps it; emits pass.window_scheduled.
Add a single window to a pass plan's schedule without touching the rule that generates the rest — a one-off extra session, a make-up date, a special. The window is stored as manual, which is what protects it: when the plan's schedule is rebuilt from its slots, generated windows are pruned and regenerated, manual ones are left alone.
Synchronous — the new window comes back in the get_pass_window row
shape, and pass.window_scheduled emits once.
A bare `starts_at` is read in the plan's timezone
An ISO 8601 instant with an offset (2026-10-03T09:00:00+01:00 or Z) is
taken as given. One without an offset is read in the plan's own pass
timezone, not UTC and not the agent's clock, because that is the time the
creator means when they say "ten o'clock". The future check runs in that zone
too.
The plan's configured floor and ceiling on window length apply, so duration_minutes outside those
bounds is refused rather than clamped.
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
plan_id*stringUUID of the pass plan to place the window on.
starts_at*stringISO 8601 instant the window opens. Without an offset it is read in the plan's own timezone.
duration_minutes*integerHow long the window runs, in whole minutes.
1What it returns
{ "data": { "id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738", "plan_id": "9b7c2e15-4d63-4f80-a2b1-7e5d0c9f3a46", "plan_name": "Saturday session", "starts_at": "2026-10-03T09:00:00+00:00", "ends_at": "2026-10-03T11:00:00+00:00", "timezone": "Europe/London", "local_range": "Sat 3 Oct, 10:00–12:00 BST", "duration_minutes": 120, "status": "scheduled", "sellable": true, "holders": 0 }}How it fails
VALIDATION_FAILEDthe plan is not of kind pass, starts_at is in the past, duration_minutes is outside the plan's minimum and maximum, or the plan already has a window starting at that instant.
RESOURCE_NOT_FOUNDunknown plan_id, or the plan's project is outside the token's scope.
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks pass-window:create.
create_plan
DESTRUCTIVECreate 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.
delete_plan
DESTRUCTIVESoft-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.
Retire a plan for good. The row is soft-deleted: it disappears from the portal, the bot and every
list, nobody can buy it again, and the subscriptions already sold keep their history and keep
running to their end. Emits plan.deleted with a snapshot taken
before the row goes.
Confirm the target with a human
Deletion cannot be undone from the API. The tool is annotated destructive so a
client can prompt for confirmation; read the plan back with
get_plan and confirm the plan_id before calling.
A pass plan with windows still to run is refused
Deleting a pass plan would cascade away access windows customers have paid
for. While any holder has a window that has not run yet, the service refuses
with VALIDATION_FAILED. Call publish_plan with
active: false instead: new sales stop while the purchased windows still open
on schedule. Delete once the last of them has closed.
Idempotent: a second call on the same id finds nothing and answers RESOURCE_NOT_FOUND, exactly as
an unknown or out-of-scope id does.
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. Sending the same arguments twice changes nothing the second time.
Arguments
plan_id*stringUUID of the plan to delete.
What it returns
{ "data": { "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "deleted": true }}id echoes the argument as sent.
How it fails
VALIDATION_FAILEDa pass plan whose customers still hold access windows that have not run
RESOURCE_NOT_FOUNDunknown plan_id, a plan on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-subscription-plan:delete.
get_pass_window
READOne dated access window of a project's pass plans by UUID — when it runs, whether it is still on sale and how many holders bought it.
Read one window back before acting on it. The row is the same shape
list_pass_windows returns: the instant it opens and closes, the
local range in the plan's own timezone, its lifecycle status, whether it is sellable right now,
and how many holders bought it.
`sellable` and `status` answer different questions
status is the lifecycle — scheduled, open, closed, canceled.
sellable is whether a customer can buy the window at this moment, after the
plan's sales cutoff. A scheduled window can already be closed to sales, and
a plan anchored to the window's end keeps selling while it is open. Check
sellable before pointing a customer at a window, and holders before
cancelling one.
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.
Annotations
It reads and never changes anything.
Arguments
project_id*stringUUID of the project whose pass plans own the window.
window_id*stringUUID of the window to fetch.
What it returns
{ "data": { "id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738", "plan_id": "9b7c2e15-4d63-4f80-a2b1-7e5d0c9f3a46", "plan_name": "Saturday session", "starts_at": "2026-10-03T09:00:00+00:00", "ends_at": "2026-10-03T11:00:00+00:00", "timezone": "Europe/London", "local_range": "Sat 3 Oct, 10:00–12:00 BST", "duration_minutes": 120, "status": "scheduled", "sellable": true, "holders": 12 }}starts_at and ends_at are UTC instants; local_range is the same span rendered in timezone,
which is the plan's pass timezone, so it is what a creator or a member would say out loud.
How it fails
RESOURCE_NOT_FOUNDunknown project_id or window_id, a window that belongs to another project, or a project outside the token's scope.
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks pass-window:view.
get_plan
READFetch one plan by UUID — the same kind-tagged row list_plans emits, with its one nested block and its cadence string.
Read one plan in full. The row is the same one list_plans emits, so an
agent that found a plan in the list reads exactly the same fields back, and the row
update_plan returns after a change is comparable field for field.
Reads go through the plan service, so the tenant scope and the token's scope:project: allow-list
both apply. An id the token cannot see answers RESOURCE_NOT_FOUND, indistinguishable from an id
that never existed.
`kind` names the ONE block you will find
Every plan carries a kind — subscription, pass or pass_series — and
the kind decides which single nested object accompanies it: billing, pass
or pass_series. The other two are absent, not null. Read the tag and you
know what you are holding; there is no nullable object to probe.
Every row also carries cadence, the human-readable duration the portal and the bot show — "Per Month", "Per 3 Hours", "For all 10 passes". Use it rather than deriving a duration yourself.
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.
Annotations
It reads and never changes anything.
Arguments
plan_id*stringUUID of the plan to fetch.
What it returns
{ "data": { "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Premium Monthly", "description": "All-access pass", "price": "29.00", "currency": "USD", "kind": "subscription", "cadence": "Per Month", "active": true, "sales_cap": 10, "sales_cap_sold": 3, "position": 0, "paused_reason": null, "created_at": "2026-05-18T10:05:00+00:00", "billing": { "billing_cycle": "months", "billing_cycle_count": 1, "recurring": true, "trial_days": 7, "trial_cardless": false } }}price is a decimal string; currency is an ISO code. The block changes with the kind:
kind: pass
{
"kind": "pass",
"cadence": "Per 3 Hours",
"pass": {
"timezone": "America/New_York",
"recurrence": "weekly",
"sales_cutoff_minutes": 60,
"sales_cutoff_anchor": "before_start",
"next_window": {
"id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738",
"starts_at": "2026-09-20T13:00:00+00:00",
"ends_at": "2026-09-20T16:00:00+00:00"
}
}
}pass.next_window is the next window still on sale, in UTC, or null when the schedule has nothing
left to sell — which also means the plan is not purchasable right now. For the full schedule call
list_pass_windows.
kind: pass_series
{
"kind": "pass_series",
"cadence": "For all 10 passes",
"pass_series": {
"timezone": "America/New_York",
"window_count": 10,
"starts_at": "2026-09-20T13:00:00+00:00",
"ends_at": "2026-11-29T22:00:00+00:00",
"seat_cap": 50,
"seats_remaining": 13,
"successor_plan_id": null
}
}seat_cap and seats_remaining are null on an uncapped season. successor_plan_id is set once
start_next_season has run; it is the plan current holders are
offered in the presale.
How it fails
RESOURCE_NOT_FOUNDunknown plan_id, a plan on another team's project, one outside the token's
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-subscription-plan:view.
The dated access windows a pass plan generates, with their UUIDs — the ids a Pass Series is built from.
List the dated access windows on time-limited pass plans, with their UUIDs, status, local times and holder counts.
This is what makes a Pass Series authorable
create_plan with kind: pass_series takes
pass_series.window_ids. A series points at windows that already exist
rather than creating any of its own, so those UUIDs have to come from
somewhere — and this is the only tool that exposes them. Call it first, pick
the dates, then create the series.
It is also the tool for reconciling a schedule into an external calendar, and for checking what is actually still on sale before pointing a customer at a plan.
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.
Annotations
It reads and never changes anything.
Arguments
plan_idstringoptionalUUID of one time-limited pass plan. Takes precedence over project_id.
project_idstringoptionalUUID of a project, to list windows across every pass plan on it.
statusstringoptionalFilter by lifecycle state: scheduled (not yet open), open (running now), closed (finished), canceled.
fromstringoptionalOnly windows starting at or after this ISO-8601 timestamp.
tostringoptionalOnly windows starting at or before this ISO-8601 timestamp.
limitintegeroptionalMaximum windows to return per page (1..100).
1max100pageintegeroptional1-indexed page number.
1What it returns
{ "data": [ { "id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738", "plan_id": "pln_01HZ...", "plan_name": "Match Day Pass", "starts_at": "2026-09-20T13:00:00Z", "ends_at": "2026-09-20T16:00:00Z", "timezone": "America/New_York", "local_range": "Sun 20 Sep 2026, 09:00 – 12:00 EDT", "duration_minutes": 180, "status": "scheduled", "sellable": true, "holders": 12 } ], "meta": { "page": 1, "limit": 50, "total": 10, "has_more": false }}| Field | Type | Notes |
|---|---|---|
id | string UUID | The id a Pass Series points at. |
plan_id | string UUID | The pass plan the window belongs to. A series can draw from several. |
starts_at | ISO 8601 | Always UTC. |
ends_at | ISO 8601 | Always UTC. |
timezone | string | The zone the schedule was authored in. |
local_range | string | The window rendered in that zone, ready to show. See below. |
duration_minutes | integer | Length. Slots carry their own, so one plan can mix a 3-hour and a 14-hour window. |
status | string | scheduled, open, closed or canceled. |
sellable | boolean | Whether it is on sale right now, after applying the plan's sales cutoff. Not the same as status. |
holders | integer | How many purchases hold this window, series holders included. |
Both zones are returned on purpose
The UTC pair is what an integration stores. local_range is what the creator
authored and what a subscriber is shown — so an agent asked "which one is
Sunday's window" can answer without converting anything, and cannot get the
conversion wrong.
How it fails
TOKEN_MISSING_ABILITYtoken lacks pass-window:view-any (or the project-subscription-plan:view-any alias).
RESOURCE_NOT_FOUNDplan_id or project_id names something the token cannot see: unknown, another team's, or outside the token's scope:project: allow-list. Without either, the list spans only the projects the allow-list admits.
VALIDATION_FAILEDstatus is not one of scheduled, open, closed, canceled, or from / to is not a parseable timestamp (reason: not_a_timestamp).
status and sellable are different questions
A scheduled window is not necessarily buyable: the plan's sales cutoff may have closed it
already. An open window is not necessarily unbuyable either — a plan anchored to
before_end keeps selling while the window runs.
Filter on status to reason about the schedule. Read sellable to reason about what a
customer can actually buy.
Building a season ticket, end to end
list_planswithproject_id— find thekind: passplans to draw from.list_pass_windowswithplan_idand afrom/torange — collect the window UUIDs.create_planwithkind: pass_seriesand those ids inpass_series.window_ids.
Add a pass_series.rules entry in step 3 if the season should keep absorbing new windows as
they are scheduled — those are granted to existing holders automatically, at no charge.
list_plans
READPaginated list of subscription plans visible to the current token, optionally scoped to one project.
List subscription plans. Results are always scoped to the caller's team; pass an optional project_id to narrow further. Ordered by newest-first.
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.
Annotations
It reads and never changes anything.
Arguments
project_idstringoptionalOptional project UUID to narrow the list to a single project.
limitintegeroptionalMaximum plans to return per page (1..100).
1max100pageintegeroptional1-indexed page number.
1What it returns
{ "data": [ { "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Premium Monthly", "description": "All-access pass", "price": "29.00", "currency": "USD", "kind": "subscription", "cadence": "Per Month", "active": true, "sales_cap": null, "sales_cap_sold": 0, "position": null, "paused_reason": null, "created_at": "2026-05-18T10:05:00Z", "billing": { "billing_cycle": "months", "billing_cycle_count": 1, "recurring": true, "trial_days": 7, "trial_cardless": false } } ], "meta": { "page": 1, "limit": 25, "total": 3, "has_more": false }}Every row is tagged by kind
kind names the ONE nested block that accompanies it — billing, pass or pass_series.
The other two are absent, not null. So a billing cycle never appears on a plan where a
cycle means nothing, and you never have to probe for a nullable object to work out what you
are holding.
Every row also carries cadence, 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 a duration yourself; deriving one from a pass's cycle fields is how integrations end
up printing "1 Month" beside a three-hour window.
kind: pass
{
"kind": "pass",
"cadence": "Per 3 Hours",
"pass": {
"timezone": "America/New_York",
"recurrence": "weekly",
"sales_cutoff_minutes": 60,
"sales_cutoff_anchor": "before_start",
"next_window": {
"id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738",
"starts_at": "2026-09-20T13:00:00Z",
"ends_at": "2026-09-21T03:00:00Z"
}
}
}Window timestamps are UTC; pass.timezone is the zone the schedule was authored in and the
one a buyer should see. pass.next_window is null when the schedule has no window left to
sell, which also means the plan is not purchasable right now. On a plan whose sales cutoff is
anchored to the window end, that window may be one already running rather than a future one.
kind: pass_series
{
"kind": "pass_series",
"cadence": "For all 10 passes",
"pass_series": {
"timezone": "America/New_York",
"window_count": 10,
"starts_at": "2026-09-20T13:00:00Z",
"ends_at": "2026-11-29T22:00:00Z",
"seat_cap": 50,
"seats_remaining": 13,
"successor_plan_id": null
}
}A series sells a slate of other pass plans' windows for one payment. It owns none of its
own, so there is no schedule here — use list_pass_windows
to see the individual dates, and the Plans API for the full slate with
the plan each date came from.
How it fails
TOKEN_MISSING_ABILITYtoken lacks project-subscription-plan:view-any.
RESOURCE_NOT_FOUNDproject_id names a project the token cannot see: unknown, another team's, or outside the token's scope:project: allow-list. Without a project_id, the list spans only the projects the allow-list admits.
publish_plan
DESTRUCTIVEPublish or unpublish a subscription plan by flipping its active flag. Emits plan.activated or plan.deactivated.
Toggle a plan's active flag. Active plans are purchasable and listed on the portal; inactive plans stay on the books but hide from new buyers. Noop when the plan is already in the requested state.
Free plans can only be published on a paid plan
A plan priced at 0 can only be sold on the Starter and Growth plans. On the
Free plan the call is refused with TEAM_TIER_REQUIRED and the plan stays off
sale — give it a price first, or upgrade the creator.
Unpublishing is always allowed whatever the price, so a plan can always be wound down.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same actions as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Fires events
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time. It reaches beyond Subscriby: a connector, a provider or a member.
Arguments
plan_id*stringUUID of the plan to flip.
activebooleanoptionalTarget state — true publishes, false unpublishes. Defaults to true.
What it returns
{ "data": { "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "active": true }}How it fails
RESOURCE_NOT_FOUNDunknown plan_id, or the plan belongs to a team outside the token's scope.
TOKEN_MISSING_ABILITYtoken lacks project-subscription-plan:update.
TEAM_TIER_REQUIREDactive=true on a plan priced at 0 while the creator is on the Free plan. error.context.reason carries the remedy: price the plan, or upgrade the creator to Starter or Growth.
remind_pass_window_queue
DESTRUCTIVENudge everyone who bought a window but has not come through their grant yet, re-sending it. Messages real people.
A pass holder who bought a window still has to come through their grant (on Telegram, tap the invite link and send a join request) before the window opens; the ones who have not are the window's queue. This tool re-sends the grant to every holder still in that queue, the way the dashboard's "remind everyone" action on a window does, and answers with how many holders the connector accepted the message for.
A window that has ended or been cancelled has nobody left to remind and answers 0. Holders who
already queued are skipped.
This messages real people
Confirm with the creator before calling, and do not repeat it within the same
window — the queue does not change because it was nudged twice. To remind one
holder, use remind_pass_holder.
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.
Annotations
A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.
Arguments
project_id*stringUUID of the project whose pass plans own the window.
window_id*stringUUID of the window whose missing holders to nudge.
What it returns
{ "data": { "window_id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738", "reminded": 4 }}reminded counts messages the connector accepted, not holders in the queue: a holder who has blocked
the bot is in the queue but cannot be reached, and is not counted.
How it fails
VALIDATION_FAILEDthe reminder was refused for the window's current state.
RESOURCE_NOT_FOUNDunknown project_id or window_id, a window of another project, or a project outside the token's scope.
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks pass-window:update.
reorder_plans
DESTRUCTIVEPin the order a project's plans appear in on the portal and in the project's bot, or reset it to the built-in order. Emits plan.order_changed.
Arrange the storefront. Buyers see plans in the order you pass; anything you leave out keeps the built-in order (passes, then seasons, then subscriptions, cheapest first) after the pinned plans. An empty list clears every pin. This is the same write the dashboard's Arrange Storefront Order drag-and-drop performs.
Inactive plans may be pinned
Only active plans are shown to buyers, but you may include an inactive plan so it takes its place the moment it is published. The response lists active plans only, in the order buyers see them.
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. Sending the same arguments twice changes nothing the second time. It reaches beyond Subscriby: a connector, a provider or a member.
Arguments
project_id*stringUUID of the project whose storefront is arranged.
plan_ids*arrayPlan UUIDs in the order buyers should see them. Empty clears every pin and restores the built-in order.
What it returns
{ "data": { "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "plan_ids": [ "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "b1a7c3d5-2e48-4f60-9a1b-7c5d3e820f94" ], "plans": [ { "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "name": "Weekly Football Pass", "kind": "pass_series", "active": true, "position": 0 } ] }}Each entry in plans is the same row shape list_plans and get_plan return.
Errors
| Code | When |
|---|---|
VALIDATION_FAILED | An id in plan_ids is not a plan of this project. |
RESOURCE_NOT_FOUND | The project does not exist or the token cannot see it. |
start_next_season
DESTRUCTIVEDuplicate a finished pass series into its next season — a new inactive pass_series plan with the same settings, an empty slate, and the old season linked to it. Not idempotent.
The plan list's "start next season" button, for agents. It creates a new plan of kind
pass_series copied from the one you name — same price, currency, description, eligibility,
linked resources, overlap rule, sales cutoff and seat cap — with two deliberate differences: it is
created inactive, and its slate is empty. A season that went straight on sale with last
year's dates in it would be selling something that has already happened.
The step a hand-built successor forgets is the link: the old season's successor_plan_id is pointed
at the new plan (and a presale window is set if the old season had none), which is what lets current
holders be offered the next season first. Rules are copied forward with their date bounds shifted by
the length of the finished season, so "every pass in September" becomes next September; the slate
itself and any blackouts are not copied, because both name specific windows that have run.
Not idempotent — call it once per season
Every call creates another plan, named after the source with a season number
appended — Match Day Season (Season 2), then (Season 3), and so on — and
re-points the old season's successor at the newest one. Check
pass_series.successor_plan_id on the source with
get_plan before calling; if it is already set, the
next season exists.
Emits plan.created for the new plan. The new season is then
composed and published like any other: add dates with update_plan —
pass_series.window_ids from list_pass_windows, or
pass_series.rules — and put it on sale with publish_plan.
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
plan_id*stringUUID of the finished pass_series plan to duplicate into its next season.
What it returns
{ "data": { "id": "9e12f0b4-7c3a-4d58-b2e6-0a5f81c4d739", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Match Day Season (Season 2)", "description": "Every home match this season", "price": "180.00", "currency": "USD", "kind": "pass_series", "cadence": "For all 0 passes", "active": false, "created_at": "2026-12-01T09:00:00+00:00", "pass_series": { "timezone": null, "window_count": 0, "starts_at": null, "ends_at": null, "seat_cap": 50, "seats_remaining": 50, "successor_plan_id": null } }}The row is the new plan's, in the get_plan shape. active is false
and window_count is 0 until you compose it; starts_at, ends_at and timezone are null
because a slate with no dates has no span. Its own successor_plan_id is null — it is the source
plan whose successor_plan_id now points here.
How it fails
VALIDATION_FAILEDplan_id names a plan whose kind is not pass_series
RESOURCE_NOT_FOUNDunknown plan_id, a plan on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-subscription-plan:create.
update_plan
DESTRUCTIVEChange one or more fields of a plan. Partial by design — anything omitted keeps its stored value; the kind is fixed. Emits plan.updated.
Change an existing plan without restating it. Only the arguments present in the call reach the
write, the way the REST PATCH behaves, so repricing a plan is one field, not a re-creation. The
shared fields are checked the way create_plan checks them, and the one
nested block the plan's kind names is shaped exactly as create_plan shapes it.
Synchronous — the row after the change comes back, and
plan.updated emits once with the fields that actually moved. A
call whose arguments change nothing returns the current row and emits nothing.
The kind is fixed, and so is which block you may send
A plan's kind is set at creation and cannot change; a kind argument that
disagrees with the stored one is refused rather than converting a subscription
into a pass — create a new plan instead. The same rule decides the nested
block: billing only on a subscription, pass only on a pass,
pass_series only on a pass_series. A block of another kind fails with
VALIDATION_FAILED naming the stray key; it is never silently ignored.
`resources` replaces, it does not append
Send the full list of resource UUIDs the plan should grant. Whatever is
missing from the list is unlinked. A subscription or pass must keep at
least one; a pass_series may be empty, because each window in a series
grants its own plan's resources.
Flipping `active` here is not the same as publish_plan
active is accepted, and it works, but it is announced as a field inside
plan.updated. publish_plan flips the same flag
with its own plan.activated / plan.deactivated event, which is what an
automation reacting to plans going on or off sale should key on.
Two tiers of entitlement are checked on the way in: a price of 0 makes the plan free, which
needs the Starter or Growth tier, and anything touching a pass or pass_series needs Time-Limited
Passes. Either failing surfaces as TEAM_TIER_REQUIRED with nothing written.
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. Sending the same arguments twice changes nothing the second time. It reaches beyond Subscriby: a connector, a provider or a member.
Arguments
plan_id*stringUUID of the plan to change.
namestringoptionalPlan name (min 5, max 255 chars, unique per project).
descriptionstringoptionalPlan description. Max 1000 chars, HTML is filtered. Pass an empty string to clear.
pricenumberoptionalWhat one purchase costs, as a decimal. At least the $1.00 USD equivalent unless 0, which makes the plan free and requires the Starter or Growth tier.
currency_idstringoptionalUUID of the currency. Must be supported by an active payment method on the project.
activebooleanoptionaltrue publishes the plan, false takes it off sale; existing subscriptions keep running either way.
sales_capintegeroptionalPause the plan automatically after this many successful purchases (1-100000). Changing it restarts the count; null removes the 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.
resourcesarrayoptionalThe full list of project_resources UUIDs this plan grants access to; replaces the current list. Must not be empty on kind subscription or kind pass.
billingobjectoptionalOnly on a plan of kind subscription. {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.
passobjectoptionalOnly on a plan of kind pass. {timezone: IANA zone, 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}; the same shape `create_plan` documents.
pass_seriesobjectoptionalOnly on a plan of kind pass_series. {window_ids?: array of pass-window UUIDs, prevent_overlaps?: bool, sales_cutoff_minutes?: int, sales_cutoff_anchor?: string, seat_cap?: int, successor_plan_id?: UUID, presale_hours?: int, blackout_window_ids?: array, rules?: array}; the same shape `create_plan` documents. Window ids come from `list_pass_windows`.
What it returns
{ "data": { "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "name": "Premium Monthly", "description": "All-access pass", "price": "34.00", "currency": "USD", "kind": "subscription", "cadence": "Per Month", "active": true, "created_at": "2026-05-18T10:05:00+00:00", "billing": { "billing_cycle": "months", "billing_cycle_count": 1, "recurring": true, "trial_days": 14, "trial_cardless": false } }}How it fails
TEAM_TIER_REQUIREDthe change needs a tier the project owner lacks: a price of 0 without
VALIDATION_FAILEDone entry per offending field in error.context: a kind argument that
RESOURCE_NOT_FOUNDunknown plan_id, a plan on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-subscription-plan:update.
Related
How is this guide?