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.
| Field | Type | Notes |
|---|---|---|
kind | string | subscription, pass or pass_series. Names the one block below it. |
price | string | What one purchase costs. On a pass that buys one window; on a series it buys the whole slate, once. |
currency | object | { id, iso, symbol }. |
cadence | string | The human-readable duration string: "Per Month", "Per 3 Hours", "For all 10 passes". |
eligibility | object | Audience restrictions. The first three are mutually exclusive. |
resources | array | Present 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
cadenceexists. 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
| Field | Type | Notes |
|---|---|---|
billing.billing_cycle | string | day, week, month, year, lifetime. |
billing.billing_cycle_count | integer | 1–99. Must be 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 rather than renewing. |
billing.trial_days | integer | 0–365. |
billing.trial_cardless | boolean | Whether the trial starts without a payment method. |
billing.trial_type | string | project or plan: whose trial rule applies. |
kind: pass
A plan that owns its own dated windows and sells one per purchase.
| Field | Type | Notes |
|---|---|---|
pass.timezone | string | IANA zone the schedule was authored in. Render window times in this, never UTC. |
pass.schedule_mode | string | repeating or fixed. |
pass.recurrence | string | null | daily, weekly or monthly. Null in fixed mode. |
pass.recurrence_ends_at | timestamp | null | When window generation stops. Renamed, see the note below. |
pass.sales_cutoff_minutes | integer | null | Stops sales this many minutes before the moment sales_cutoff_anchor names. |
pass.sales_cutoff_anchor | string | before_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_time | string HH:MM | Local wall-clock time in timezone. |
pass.slots[].duration_minutes | integer | Each slot carries its own length, so one plan can mix a 3-hour and a 14-hour window. |
pass.upcoming_windows[] | array | Present only when the relation is loaded. Timestamps are UTC. The pass windows endpoints list and manage a plan's dates. |
series_ends_atwas renamed torecurrence_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 calledseries_ends_atsitting 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.
| Field | Type | Notes |
|---|---|---|
pass_series.timezone | string | Borrowed 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_overlaps | boolean | Refuses to absorb a window clashing with one already on the slate. |
pass_series.sales_cutoff_minutes | integer | Measured against the whole season, not one window. |
pass_series.sales_cutoff_anchor | string | Four 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_cap | integer | null | Concurrent holder limit. null is unlimited. |
pass_series.seats_taken | integer | Holders currently counted against the cap. |
pass_series.seats_remaining | integer | null | null when uncapped. |
pass_series.starts_at | timestamp | null | First window's start, UTC. |
pass_series.ends_at | timestamp | null | Last window's end, UTC. |
pass_series.window_count | integer | Slate length. Capped at 120. |
pass_series.successor_plan_id | string | null | The next season, offered to holders first when this one finishes. |
pass_series.presale_hours | integer | null | How long that offer is held for holders only. 1–8760. |
pass_series.windows[] | array | The slate. Each entry names the plan the window belongs to; a series can mix several. |
pass_series.windows[].added_by_rule | boolean | true when a rule absorbed it rather than the creator picking it by hand. |
pass_series.rules[] | array | Automatic inclusion rules. kind is date_range or next_n; take is required on next_n. |
pass_series.blackout_window_ids | array | Windows a rule matches but the creator has permanently excluded. |
Requires ability
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 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
The project, resolved by the route binder.
uuidThe plan, resolved within the project by the route binder.
uuidResponses
200OKapplication/json
The plan.
401UnauthorizedAUTHENTICATION_REQUIREDapplication/json
The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).
403ForbiddenTOKEN_MISSING_ABILITYapplication/json
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 foundRESOURCE_NOT_FOUNDapplication/json
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 requestsRATE_LIMITEDapplication/json
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