---
title: "Get a plan"
method: GET
path: "/v1/projects/{project}/plans/{plan}"
tags: ["Plans"]
---

# Get a plan

`GET /v1/projects/{project}/plans/{plan}`

## Request

- Base URL: `https://api.subscriby.net`
- URL: `https://api.subscriby.net/v1/projects/{project}/plans/{plan}`
- Auth: HTTP bearer

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

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

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

## Path parameters

- `project` string, uuid, required
- `plan` string, uuid, required

## Response `200`

The plan.

- object
  - `data` PlanResource, required: A plan: what one purchase buys. `kind` names exactly one of `billing`, `pass` or `pass_series`; the other two are absent, not null.
    - `id` string, required: The plan's id.
    - `project_id` string, required: The project the plan belongs to.
    - `kind` string, required: `subscription`, `pass` or `pass_series`. Names the one block that accompanies it; the other two are absent, not null.
    - `name` string, required: The name buyers see, unique within the project without regard to case or surrounding spaces.
    - `description` string, nullable, required: The pitch under the name, HTML filtered to a safe subset; null when none was written.
    - `price` string, required: What one purchase costs, as a decimal string in `currency`. On a pass that buys one window; on a series it buys the whole slate, once. `"0.00"` is a free plan.
    - `currency` object, required: The currency the price is charged in.
      - `id` string, required: The currency's id, the value `currency_id` takes on a write.
      - `iso` string, required: The ISO 4217 code, such as `USD`.
      - `symbol` string, required: The symbol to print before the amount.
    - `active` boolean, required: Whether the plan is on sale. Flip it with the publish and unpublish endpoints, not with an update.
    - `sales_cap` integer, nullable, required: How many purchases the plan takes before pausing itself; null for no limit.
    - `sales_cap_sold` integer, required: Purchases counted against the cap since it was last changed or the plan was last published.
    - `position` integer, nullable, required: The plan's pinned place in the storefront order; null while it follows the built-in order (passes, then seasons, then subscriptions, cheapest first).
    - `paused` object, nullable, required: Why the plan took itself off sale; null while it is not paused.
      - `reason` string, required: `sales_cap_reached` when the cap filled, `seat_cap_reached` when a season filled its last seat.
      - `at` string, nullable, required: When it paused, ISO 8601.
    - `cadence` string, required: The duration in words the portal and the bot print, such as `Per Month`, `Per 3 Hours` or `For all 10 passes`. Use it rather than deriving one from the cycle fields.
    - `eligibility` object, required: Who may buy. The first three are mutually exclusive.
      - `newcomers_only` boolean, required: Only members who never held a subscription on the project.
      - `customers_only` boolean, required: Only members who currently hold one.
      - `churned_only` boolean, required: Only members who held one and let it lapse.
      - `single_use` boolean, required: Each member may buy the plan once.
      - `access_codes_only` boolean, required: The plan cannot be bought; it is granted by redeeming an access code.
    - `resources` object[]: The places the plan grants, present when the endpoint loaded them. On a series these are the lounge, open for the whole span, not the thing being sold.
      - `id` string, required: The resource's id.
      - `name` string, required: The resource's title.
      - `kind` string, required: `manual` for a perk the creator hands over themselves, or `<connector>:<kind>` for a place a connector hosts.
      - `connector` string, nullable, required: The connector that hosts the place; null for a manual resource.
    - `billing` object: Cycle, trial and renewal. Present only on `kind: subscription`.
      - `billing_cycle` union, required: `day`, `week`, `month`, `year` or `lifetime`.
        - string
        - 'day' | 'week' | 'month' | 'year' | 'lifetime': The unit a creator's own subscription plan renews in. Not the platform cycle: this is what a creator sells to their members, and it pairs with a count (`billing_cycle_count`) so "3 months" and "2 weeks" are both expressible. `Lifetime` is a one-time purchase that never renews.
      - `billing_cycle_count` integer, required: How many cycles one period spans, 1 to 99; always 1 when the cycle is `lifetime`.
      - `recurring` boolean, required: Whether the subscription renews itself. Never true for a crypto or platform currency.
      - `disabled_renewal` boolean, required: Charges once, then lapses at the end of the period instead of renewing.
      - `trial_days` integer, required: Free days before the first charge, 0 to 365.
      - `trial_cardless` boolean, required: Whether the trial starts without a payment method on file.
      - `trial_type` union, required: Whose trial rule applies: `project` for the project's trial settings, `plan` for this plan's own.
        - string
        - 'project' | 'plan'
    - `pass` object: The plan's own dated schedule. Present only on `kind: pass`.
      - `timezone` string, nullable, required: The IANA zone the schedule was authored in. Render window times in it, never in UTC.
      - `schedule_mode` string, nullable, required: `repeating` for windows generated from `slots`, `fixed` for explicitly dated windows.
      - `recurrence` string, nullable, required: `daily`, `weekly` or `monthly`; null in `fixed` mode.
      - `recurrence_ends_at` string, nullable, required: When window generation stops, ISO 8601; null for no end.
      - `sales_cutoff_minutes` integer, nullable, required: Sales stop this many minutes before the moment `sales_cutoff_anchor` names; null to sell until the window begins.
      - `sales_cutoff_anchor` string, required: `before_start` or `before_end`. Never null: a plan that never set one reads as `before_start`.
      - `slots` object[]: The repeating schedule, present when the endpoint loaded it. Each slot carries its own length, so one plan can mix a 3-hour and a 14-hour window.
        - `weekday` integer, nullable, required: Day of the week, 0 (Sunday) to 6, on a weekly recurrence; null otherwise.
        - `day_of_month` integer, nullable, required: Day of the month, 1 to 31, on a monthly recurrence; null otherwise. 29 to 31 skip the months that lack the day.
        - `start_time` string, required: Local wall-clock start in `timezone`, `HH:MM`.
        - `duration_minutes` integer, required: How long the window stays open.
      - `upcoming_windows` object[]: The next windows, present only where the endpoint loaded them. Timestamps are UTC.
        - `id` string, required: The window's id, the value a series names in `pass_series.window_ids`.
        - `starts_at` string, required: When the window opens, UTC.
        - `ends_at` string, required: When it closes, UTC.
        - `status` string, required: `scheduled`, `open`, `closed` or `canceled`.
    - `pass_series` union: The season ticket's slate, the rules that grow it and its seats. Present only on `kind: pass_series`.
      - object
        - `timezone` string, required: Borrowed from the source plans; a series has no zone of its own. Null while the slate is empty.
        - `prevent_overlaps` boolean, required: Whether a window clashing with one already on the slate is refused.
        - `sales_cutoff_minutes` integer, required: Sales stop this many minutes before the moment `sales_cutoff_anchor` names, measured against the whole season.
        - `sales_cutoff_anchor` string, required: Earliest deadline first: `before_start` closes before the first window opens, `before_first_end` during that opening window, `before_last_start` as the last window opens, `before_end` as it ends.
        - `seat_cap` integer, nullable, required: How many holders may hold the season at once; null for unlimited.
        - `seats_taken` integer, required: Holders currently counted against the cap.
        - `seats_remaining` integer, nullable, required: Seats still open; null when uncapped.
        - `starts_at` string, nullable, required: The first window's start, UTC; null while the slate is empty.
        - `ends_at` string, nullable, required: The last window's end, UTC; null while the slate is empty.
        - `window_count` integer, required: How many windows the slate holds, at most 120.
        - `successor_plan_id` string, nullable, required: The next season, offered to holders first when this one finishes; null when none is set.
        - `presale_hours` integer, nullable, required: How long that offer is held for holders only, 1 to 8760; null when none is set.
        - `windows` object[], required: The slate. Each entry names the plan the window belongs to, because a series can mix several.
          - `id` string, required: The window's id.
          - `plan_id` string, required: The pass plan the window belongs to.
          - `plan_name` string, required: That plan's name.
          - `starts_at` string, required: When the window opens, UTC.
          - `ends_at` string, required: When it closes, UTC.
          - `status` string, required: `scheduled`, `open`, `closed` or `canceled`.
          - `added_by_rule` boolean, required: True when a rule absorbed it rather than the creator picking it by hand.
        - `rules` object[], required: The rules that keep absorbing matching windows into the slate.
          - `source_plan_id` string, required: The pass plan the rule draws windows from.
          - `kind` string, required: `date_range` takes every window between `from_at` and `to_at`; `next_n` takes the next `take` windows.
          - `from_at` string, nullable, required: The earliest window start the rule takes, UTC; null for no bound.
          - `to_at` string, nullable, required: The latest window start the rule takes, UTC; null for no bound.
          - `take` integer, nullable, required: How many windows a `next_n` rule takes; null on a `date_range` rule.
        - `blackout_window_ids` object, required: Windows a rule matches but the creator has permanently excluded.
      - object
        - `timezone` unknown, required
        - `prevent_overlaps` boolean, required: Whether a window clashing with one already on the slate is refused.
        - `sales_cutoff_minutes` integer, required: Sales stop this many minutes before the moment `sales_cutoff_anchor` names, measured against the whole season.
        - `sales_cutoff_anchor` string, required: Earliest deadline first: `before_start` closes before the first window opens, `before_first_end` during that opening window, `before_last_start` as the last window opens, `before_end` as it ends.
        - `seat_cap` unknown, required
        - `seats_taken` integer, required: Holders currently counted against the cap.
        - `seats_remaining` unknown, required
        - `starts_at` unknown, required
        - `ends_at` unknown, required
        - `window_count` integer, required: How many windows the slate holds, at most 120.
        - `successor_plan_id` unknown, required
        - `presale_hours` unknown, required
        - `windows` string[], required: The slate. Each entry names the plan the window belongs to, because a series can mix several.
        - `rules` string[], required: The rules that keep absorbing matching windows into the slate.
        - `blackout_window_ids` string[], required: Windows a rule matches but the creator has permanently excluded.
    - `created_at` string, nullable, required: When the plan was created, ISO 8601.
    - `updated_at` string, nullable, required: When it last changed, ISO 8601.

## Other responses

- `401`: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an `sbt_test_` token on production).
- `403`: `TOKEN_MISSING_ABILITY`: 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.
- `404`: `RESOURCE_NOT_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.
- `429`: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted.

---

[API](https://skmtc.dev/memberpass/apis/subscriby-rest-api.md) · [All operations](https://skmtc.dev/memberpass/apis/subscriby-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/memberpass/apis/subscriby-rest-api/revisions/59c4df91d764?raw)
