Version
GET

Get a plan

/v1/projects/{project}/plans/{plan} in the Plans API.

One plan in the same discriminated shape as the list. kind names the one block the plan carries, billing, pass or pass_series; the other two are absent. A plan of another project is 404 RESOURCE_NOT_FOUND, because {plan} is resolved within {project}.

Common fields

Present on every kind.

FieldTypeNotes
kindstringsubscription, pass or pass_series. Names the one block below it.
pricestringWhat one purchase costs. On a pass that buys one window; on a series it buys the whole slate, once.
currencyobject{ id, iso, symbol }.
cadencestringThe human-readable duration string: "Per Month", "Per 3 Hours", "For all 10 passes".
eligibilityobjectAudience restrictions. The first three are mutually exclusive.
resourcesarrayPresent when the relation is loaded: each resource's id, name, kind (manual or connector:kind) and connector. On a series these are the lounge, not the thing being sold.

Why cadence exists. Both the portal and the bot already compute exactly this string, and every integrator without it reinvents it, wrongly for a dated plan, printing "1 Month" beside a three-hour window. Use it rather than deriving a duration from the cycle fields.

kind: subscription

FieldTypeNotes
billing.billing_cyclestringday, week, month, year, lifetime.
billing.billing_cycle_countinteger1–99. Must be 1 when billing_cycle is lifetime.
billing.recurringbooleanRejected as true for crypto or platform currencies.
billing.disabled_renewalbooleanCharges once, then lapses rather than renewing.
billing.trial_daysinteger0–365.
billing.trial_cardlessbooleanWhether the trial starts without a payment method.
billing.trial_typestringproject or plan: whose trial rule applies.

kind: pass

A plan that owns its own dated windows and sells one per purchase.

FieldTypeNotes
pass.timezonestringIANA zone the schedule was authored in. Render window times in this, never UTC.
pass.schedule_modestringrepeating or fixed.
pass.recurrencestring | nulldaily, weekly or monthly. Null in fixed mode.
pass.recurrence_ends_attimestamp | nullWhen window generation stops. Renamed, see the note below.
pass.sales_cutoff_minutesinteger | nullStops sales this many minutes before the moment sales_cutoff_anchor names.
pass.sales_cutoff_anchorstringbefore_start or before_end. Never null: a plan that never set it reads as before_start. before_first_end and before_last_start are series-only and refused here.
pass.slots[].start_timestring HH:MMLocal wall-clock time in timezone.
pass.slots[].duration_minutesintegerEach slot carries its own length, so one plan can mix a 3-hour and a 14-hour window.
pass.upcoming_windows[]arrayPresent only when the relation is loaded. Timestamps are UTC. The pass windows endpoints list and manage a plan's dates.

series_ends_at was renamed to recurrence_ends_at. Same field, same meaning: how long this plan keeps generating windows from its recurrence. It never had anything to do with a Pass Series, but now that a series is a real plan kind, a field called series_ends_at sitting on a pass read as "when this plan's series ends", which is a different thing and one that does not exist here. Update any reader to the new key; there is no alias.

kind: pass_series

A season ticket. It owns no windows: it points at windows that already exist on your pass plans, which is the whole distinction from kind: pass.

FieldTypeNotes
pass_series.timezonestringBorrowed from the source plans; a series has no zone of its own. A series whose sources disagree is refused at authoring time.
pass_series.prevent_overlapsbooleanRefuses to absorb a window clashing with one already on the slate.
pass_series.sales_cutoff_minutesintegerMeasured against the whole season, not one window.
pass_series.sales_cutoff_anchorstringFour anchors, earliest deadline first. before_start closes before the first window opens. before_first_end closes during that opening window. before_last_start closes as the last window opens, so a buyer always gets one whole window. before_end closes as the last window ends.
pass_series.seat_capinteger | nullConcurrent holder limit. null is unlimited.
pass_series.seats_takenintegerHolders currently counted against the cap.
pass_series.seats_remaininginteger | nullnull when uncapped.
pass_series.starts_attimestamp | nullFirst window's start, UTC.
pass_series.ends_attimestamp | nullLast window's end, UTC.
pass_series.window_countintegerSlate length. Capped at 120.
pass_series.successor_plan_idstring | nullThe next season, offered to holders first when this one finishes.
pass_series.presale_hoursinteger | nullHow long that offer is held for holders only. 1–8760.
pass_series.windows[]arrayThe slate. Each entry names the plan the window belongs to; a series can mix several.
pass_series.windows[].added_by_rulebooleantrue when a rule absorbed it rather than the creator picking it by hand.
pass_series.rules[]arrayAutomatic inclusion rules. kind is date_range or next_n; take is required on next_n.
pass_series.blackout_window_idsarrayWindows a rule matches but the creator has permanently excluded.
GET
/v1/projects/{project}/plans/{plan}

The token must hold this ability, or the call is refused with 403.

MCP tool

Runs the same action from an agent, behind the same ability.

Authorization

bearerToken
AuthorizationBearer <token>

A personal access token minted on the dashboard under Settings, then Tokens, sent as Authorization: Bearer sbt_live_…. The token carries the abilities each endpoint lists under Requires ability and is frozen to one team.

In: header

Path Parameters

project*string

The project, resolved by the route binder.

Formatuuid
plan*string

The plan, resolved within the project by the route binder.

Formatuuid

Responses

200OK

The plan.

401Unauthorized

The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).

403Forbidden

The token is valid but does not carry the ability this endpoint requires; error.context.required_ability names the one to grant. An endpoint that also checks who owns a row or which tier the account is on answers FORBIDDEN, TEAM_TIER_REQUIRED or CONNECTOR_TIER_REQUIRED with the same status, and says so in its own description.

404Not found

An id in the path names nothing the token can see. TENANT_MISMATCH: the project sits outside the token's scope:project: allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred.

429Too many requests

The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.

How is this guide?

Last updated on