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 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.

The REST endpoint and this tool share one action, so validation, permissions and events are identical.

Delivered to every endpoint subscribed to it once the change is made.

Annotations

DestructiveIdempotent

A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.

Arguments

project_id*string

UUID of the project whose pass plans own the window.

window_id*string

UUID 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_FAILED

the window cannot be cancelled in its current state (the resettlement refused).

RESOURCE_NOT_FOUND

unknown project_id or window_id, a window of another project, or a project outside the token's scope.

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token lacks pass-window:delete.

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.

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.

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

DestructiveOpen world

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*string

UUID of the pass plan to place the window on.

starts_at*string

ISO 8601 instant the window opens. Without an offset it is read in the plan's own timezone.

duration_minutes*integer

How long the window runs, in whole minutes.

min1

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": 0  }}

How it fails

VALIDATION_FAILED

the 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_FOUND

unknown plan_id, or the plan's project is outside the token's scope.

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token lacks pass-window:create.

create_plan

DESTRUCTIVE

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:

kindSendSells
subscriptionbillingAccess that begins at payment and renews on a cycle. The default.
passpassOne dated access window per purchase.
pass_seriespass_seriesA 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:

  • name is 5–255 chars and unique per project.
  • currency_id must be supported by an active payment method on the project.
  • price must be at least the $1.00 USD equivalent in the plan currency. 0 publishes a free plan, which only the Starter and Growth plans can sell; on Free the call is rejected with TEAM_TIER_REQUIRED.
  • eligibility.newcomers_only, eligibility.customers_only and eligibility.churned_only are mutually exclusive.
  • resources must link to at least one existing project resource — except on kind: 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: pass and kind: pass_series both require Time-Limited Passes — bundled with Growth, or the Passes Addon on Free and Starter. On any other tier the call is rejected.

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

DestructiveOpen world

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*string

UUID of the parent project.

kind*string

subscription (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*string

Plan name (min 5, max 255 chars, unique per project).

currency_id*string

UUID of the currency. Must be supported by an active payment method on the project.

price*number

What 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.

resourcesarrayoptional

UUIDs 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.

descriptionstringoptional

Optional plan description. Max 1000 chars, HTML is filtered.

activebooleanoptional

Defaults to true. Set false to create in draft state.

sales_capintegeroptional

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.

eligibilityobjectoptional

Audience restrictions: {newcomers_only?: bool, customers_only?: bool, churned_only?: bool, single_use?: bool, access_codes_only?: bool}. The first three are mutually exclusive.

billingobjectoptional

REQUIRED 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.

passobjectoptional

REQUIRED 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_seriesobjectoptional

REQUIRED 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_FAILED

a 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_FOUND

project_id doesn't exist in your token's scope.

TOKEN_MISSING_ABILITY

token lacks project-subscription-plan:create.

TEAM_TIER_REQUIRED

kind: 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
  }
}
FieldTypeNotes
billing.billing_cyclestringdays, weeks, months, years, lifetime.
billing.billing_cycle_countinteger1–99. Forced to 1 when billing_cycle is lifetime.
billing.recurringbooleanRejected as true for crypto or platform currencies.
billing.disabled_renewalbooleanCharges once, then lapses.
billing.trial_daysinteger0–365.
billing.trial_cardlessbooleanWhether the trial starts without a payment method.
billing.trial_typestringA 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 }
    ]
  }
}
FieldTypeNotes
pass.timezonestringIANA 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_modestringrepeating (generate from slots) or fixed. Defaults to repeating.
pass.recurrencestringdaily, weekly or monthly. Decides which slot fields apply.
pass.recurrence_ends_attimestampOptional. When window generation stops.
pass.sales_cutoff_minutesinteger | nullStop selling a window this many minutes before the moment sales_cutoff_anchor names. Omit to sell until the window begins.
pass.sales_cutoff_anchorstringbefore_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[]arrayWindow definitions. Replaces the whole schedule on update.
pass.slots[].weekdayinteger | null06, 0 = Sunday. Weekly recurrence only.
pass.slots[].day_of_monthinteger | null131. Monthly only. Values of 29–31 skip months that lack the day.
pass.slots[].start_timestring HH:MMLocal wall-clock start in pass.timezone.
pass.slots[].duration_minutesintegerEach slot has its own, so one plan can mix a 3-hour and a 14-hour window.
pass.windows[]arrayExplicitly 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"
      }
    ]
  }
}
FieldTypeNotes
pass_series.window_ids[]arrayPass-window UUIDs from list_pass_windows. Max 120. Needs at least two, unless a rule will supply them.
pass_series.rules[]arrayAutomatic inclusion. Max 20. See below.
pass_series.blackout_window_ids[]arrayWindows a rule matches but you want permanently excluded.
pass_series.prevent_overlapsbooleanDefaults true. Refuses to absorb a window clashing with one already on the slate.
pass_series.seat_capinteger | nullConcurrent holder limit. null is unlimited; 0 is refused — take a plan off sale with publish_plan instead.
pass_series.sales_cutoff_minutesintegerMeasured against the whole season.
pass_series.sales_cutoff_anchorstringbefore_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_idstring | nullAnother kind: pass_series plan on the same project. A series cannot lead to itself.
pass_series.presale_hoursinteger | null1–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 kindTakes
date_rangeEvery window on source_plan_id starting between from_at and to_at. Either bound may be omitted.
next_nThe 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

DESTRUCTIVE

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.

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.

The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.

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

DestructiveIdempotent

A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.

Arguments

plan_id*string

UUID 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_FAILED

a pass plan whose customers still hold access windows that have not run

RESOURCE_NOT_FOUND

unknown plan_id, a plan on another team's project, one outside the

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token lacks project-subscription-plan:delete.

One 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.

The REST endpoint and this tool share one action, so validation, permissions and events are identical.

Annotations

Read-only

It reads and never changes anything.

Arguments

project_id*string

UUID of the project whose pass plans own the window.

window_id*string

UUID 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_FOUND

unknown project_id or window_id, a window that belongs to another project, or a project outside the token's scope.

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token lacks pass-window:view.

Fetch 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 kindsubscription, 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.

The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.

The REST endpoint and this tool share one action, so validation, permissions and events are identical.

Annotations

Read-only

It reads and never changes anything.

Arguments

plan_id*string

UUID 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_FOUND

unknown plan_id, a plan on another team's project, one outside the token's

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token 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.

The REST endpoint and this tool share one action, so validation, permissions and events are identical.

Annotations

Read-only

It reads and never changes anything.

Arguments

plan_idstringoptional

UUID of one time-limited pass plan. Takes precedence over project_id.

project_idstringoptional

UUID of a project, to list windows across every pass plan on it.

statusstringoptional

Filter by lifecycle state: scheduled (not yet open), open (running now), closed (finished), canceled.

fromstringoptional

Only windows starting at or after this ISO-8601 timestamp.

tostringoptional

Only windows starting at or before this ISO-8601 timestamp.

limitintegeroptional

Maximum windows to return per page (1..100).

min1max100
pageintegeroptional

1-indexed page number.

min1

What 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  }}
FieldTypeNotes
idstring UUIDThe id a Pass Series points at.
plan_idstring UUIDThe pass plan the window belongs to. A series can draw from several.
starts_atISO 8601Always UTC.
ends_atISO 8601Always UTC.
timezonestringThe zone the schedule was authored in.
local_rangestringThe window rendered in that zone, ready to show. See below.
duration_minutesintegerLength. Slots carry their own, so one plan can mix a 3-hour and a 14-hour window.
statusstringscheduled, open, closed or canceled.
sellablebooleanWhether it is on sale right now, after applying the plan's sales cutoff. Not the same as status.
holdersintegerHow 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_ABILITY

token lacks pass-window:view-any (or the project-subscription-plan:view-any alias).

RESOURCE_NOT_FOUND

plan_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_FAILED

status 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

  1. list_plans with project_id — find the kind: pass plans to draw from.
  2. list_pass_windows with plan_id and a from/to range — collect the window UUIDs.
  3. create_plan with kind: pass_series and those ids in pass_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.

Paginated 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.

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

Read-only

It reads and never changes anything.

Arguments

project_idstringoptional

Optional project UUID to narrow the list to a single project.

limitintegeroptional

Maximum plans to return per page (1..100).

min1max100
pageintegeroptional

1-indexed page number.

min1

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": 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_ABILITY

token lacks project-subscription-plan:view-any.

RESOURCE_NOT_FOUND

project_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

DESTRUCTIVE

Publish 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.

The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.

The REST endpoint and this tool share one action, so validation, permissions and events are identical.

Delivered to every endpoint subscribed to it once the change is made.

Annotations

DestructiveIdempotentOpen world

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*string

UUID of the plan to flip.

activebooleanoptional

Target 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_FOUND

unknown plan_id, or the plan belongs to a team outside the token's scope.

TOKEN_MISSING_ABILITY

token lacks project-subscription-plan:update.

TEAM_TIER_REQUIRED

active=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.

Nudge 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.

The REST endpoint and this tool share one action, so validation, permissions and events are identical.

Annotations

DestructiveIdempotent

A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.

Arguments

project_id*string

UUID of the project whose pass plans own the window.

window_id*string

UUID 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_FAILED

the reminder was refused for the window's current state.

RESOURCE_NOT_FOUND

unknown project_id or window_id, a window of another project, or a project outside the token's scope.

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token lacks pass-window:update.

reorder_plans

DESTRUCTIVE

Pin 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.

The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.

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

DestructiveIdempotentOpen world

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*string

UUID of the project whose storefront is arranged.

plan_ids*array

Plan 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

CodeWhen
VALIDATION_FAILEDAn id in plan_ids is not a plan of this project.
RESOURCE_NOT_FOUNDThe project does not exist or the token cannot see it.

Duplicate 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_planpass_series.window_ids from list_pass_windows, or pass_series.rules — and put it on sale with publish_plan.

The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.

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

DestructiveOpen world

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*string

UUID 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_FAILED

plan_id names a plan whose kind is not pass_series

RESOURCE_NOT_FOUND

unknown plan_id, a plan on another team's project, one outside the

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token lacks project-subscription-plan:create.

update_plan

DESTRUCTIVE

Change 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.

The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.

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

DestructiveIdempotentOpen world

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*string

UUID of the plan to change.

namestringoptional

Plan name (min 5, max 255 chars, unique per project).

descriptionstringoptional

Plan description. Max 1000 chars, HTML is filtered. Pass an empty string to clear.

pricenumberoptional

What 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_idstringoptional

UUID of the currency. Must be supported by an active payment method on the project.

activebooleanoptional

true publishes the plan, false takes it off sale; existing subscriptions keep running either way.

sales_capintegeroptional

Pause the plan automatically after this many successful purchases (1-100000). Changing it restarts the count; null removes the limit.

eligibilityobjectoptional

Audience restrictions: {newcomers_only?: bool, customers_only?: bool, churned_only?: bool, single_use?: bool, access_codes_only?: bool}. The first three are mutually exclusive.

resourcesarrayoptional

The 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.

billingobjectoptional

Only 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.

passobjectoptional

Only 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_seriesobjectoptional

Only 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    }  }}

The full get_plan row, re-read after the write so a schedule or slate change is reflected in the block. price is a decimal string. The block present depends on the plan's kind; see get_plan for the pass and pass_series shapes.

How it fails

TEAM_TIER_REQUIRED

the change needs a tier the project owner lacks: a price of 0 without

VALIDATION_FAILED

one entry per offending field in error.context: a kind argument that

RESOURCE_NOT_FOUND

unknown plan_id, a plan on another team's project, one outside the

AUTHENTICATION_REQUIRED

no authenticated user on the request.

TOKEN_MISSING_ABILITY

token lacks project-subscription-plan:update.

How is this guide?

On this page

Subscriby is a product designed by you — for you.No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request