Version

Plans API

A plan is what a project sells: the price, the currency, the billing cycle or the dated windows, the eligibility rules and the resources a purchase unlocks.

A plan is what a project sells: the price, the currency, the billing cycle or the dated windows, the eligibility rules and the resources a purchase unlocks. Every plan has a kind that decides its whole shape. A subscription renews on a cycle and carries a billing block; a pass sells one dated access window per purchase and carries a pass block; a pass_series sells a slate of other pass plans' windows in one go and carries a pass_series block. The kind names exactly one block, and the other two are absent.

A plan exists and, separately, is on sale. Publish and unpublish flip active and are how intake is paused while the plan stays on the books and editable; delete removes the row, and is refused while a customer still holds an unfinished window. A sales_cap pauses the plan by itself when it fills, eligibility narrows who may buy, resources are the places the plan grants, and the storefront order and a series' next season are actions of their own. Plans live under a project at /v1/projects/{project}/plans, and POST takes the same discriminated shape the reads return.

Every change announces itself as a plan.* event; a kind: pass plan also emits pass.* events as its windows are scheduled, open and close, and a series emits pass_series.* alongside them.

Background

The shape is a discriminated union

kind is always present, and it names exactly one nested object that accompanies it:

kindCarriesSells
subscriptionbillingAccess that begins at payment and renews on a cycle.
passpassOne dated access window per purchase.
pass_seriespass_seriesA slate of other pass plans' windows, sold once.

The other two blocks are absent, not null. Presence is a consequence of the tag, never a signal in its own right: read kind and you know what you are holding.

This replaced a nullable pass object. The previous shape emitted pass: null on ordinary plans and told consumers to branch on its presence. That is a type tag smuggled in as a presence check: it cannot express a third kind, and it left every integrator inferring the rule. It also emitted billing_cycle, trial_days and recurring on every plan including passes, where they mean nothing. A field that is present and lying is worse than one that is absent, so the cycle fields now appear only on the kind that has a cycle.

Writes mirror reads exactly, so a payload you read back is a payload you can send. The read endpoint shows one worked plan per kind, field by field.

Two axes: existence and availability

DELETE removes the row; publish and unpublish flip active. Prefer unpublish for "pause intake while keeping the plan on the books": members who already joined keep their access either way, and an unpublished plan stays fully editable.

Endpoints

List a project's plans

GET

Pages the project's plans, every kind together, newest first. Read kind on each row to know which of billing, pass or pass_series it carries; resources is loaded on every row. pass.upcoming_windows is not served here: the pass windows endpoints list a plan's dates.

GET
/v1/projects/{project}/plans

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

Query Parameters

page?integer

The 1-based page to return. A page past the last answers an empty data array with meta.total still filled, so a loop can stop without guessing.

Range1 <= value
Default1
per_page?integer

Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25.

Range1 <= value <= 100
Default25
sort_by?string

The column to order by. Defaults to created_at; a column the endpoint does not offer falls back to the default rather than failing.

Default"created_at"
sort_direction?string

asc or desc. Defaults to desc.

Default"desc"

Value in

  • "asc"
  • "desc"

Responses

200OK

The page, newest first.

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.

Create a plan

POST

POST takes the same shape it returns: kind plus the one matching block. Sending a block that does not match the kind is refused with VALIDATION_FAILED naming the offending key; silently ignoring it would let a caller believe they had set a billing cycle on a pass. Answers 201 with the plan in the same discriminated shape the read endpoint shows for each kind.

Create a subscription

curl -X POST https://api.subscriby.net/v1/projects/$PROJECT_ID/plans \  -H "Authorization: Bearer $SUBSCRIBY_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{    "kind": "subscription",    "name": "Premium Monthly",    "currency_id": "8f27a0d4-63be-4915-8c07-1a5d9e34b628",    "price": 29.00,    "resources": ["b73c5f21-9d80-4a6e-8215-4f70ce13a9d6"],    "billing": { "billing_cycle": "month", "billing_cycle_count": 1, "trial_days": 7 }  }'

Create a pass

curl -X POST https://api.subscriby.net/v1/projects/$PROJECT_ID/plans \  -H "Authorization: Bearer $SUBSCRIBY_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{    "kind": "pass",    "name": "Sunday Slate Pass",    "currency_id": "8f27a0d4-63be-4915-8c07-1a5d9e34b628",    "price": 15.00,    "resources": ["b73c5f21-9d80-4a6e-8215-4f70ce13a9d6"],    "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 }      ]    }  }'

Sending pass.slots replaces the whole schedule and 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. pass.windows is different: it adds explicitly dated windows for schedule_mode: fixed and never replaces anything.

Create a pass series

A series needs window ids, and those come from windows that already exist. Read them from pass.upcoming_windows on the source plan, from the pass windows endpoints, or from the list_pass_windows MCP tool.

curl -X POST https://api.subscriby.net/v1/projects/$PROJECT_ID/plans \  -H "Authorization: Bearer $SUBSCRIBY_TOKEN" \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{    "kind": "pass_series",    "name": "Autumn Season Ticket",    "currency_id": "8f27a0d4-63be-4915-8c07-1a5d9e34b628",    "price": 99.00,    "pass_series": {      "prevent_overlaps": true,      "seat_cap": 50,      "presale_hours": 48,      "window_ids": ["3d5a8c72-b016-4e94-8fa7-61c209d4e738", "9c1f4e27-5a8b-4d63-b2e0-8f7a6c5d4e31"],      "rules": [        { "source_plan_id": "0b8e6a2f-4c1d-4e3a-9f52-7d6c1b2a3e45", "kind": "date_range", "from_at": "2026-09-01T00:00:00Z", "to_at": "2026-12-01T00:00:00Z" }      ]    }  }'

resources is optional on a series only. Every other kind must link at least one resource, or a purchase buys nothing. A series is the exception: each of its windows grants that window's own plan's resources, so a series with none still delivers exactly what was sold. Anything you do link here is a lounge, open for the whole span.

Never put a slate window's resource in a series resources. A lounge is granted at purchase with no window and kept until the season ends. Send a resource that one of your slate windows already opens and every holder is handed it permanently the moment they pay: the dates it was scheduled for stop gating anything, and a season ticket becomes a permanent key to that channel. The API accepts it, because a genuinely permanent room is a legitimate thing to sell. It is simply almost never what was meant. Keep resources for a holders-only room nothing on the slate opens; the slate's own channels are already granted by their own pass plans on their own dates. The dashboard warns when the two overlap. Over the API, checking is yours to do.

Rules keep working after the save: a matching window scheduled later is absorbed into the slate and granted to everyone already holding the series, at no charge. That emits pass_series.leg_added. Handpicked window_ids never grow on their own. The two compose; most real seasons use both.

Validation

The field-by-field rules are on each request field below. The rules that cross fields:

  • name must be unique within the project, compared without regard to case or surrounding spaces; a duplicate is refused on name.
  • price must be at least the $1.00 USD equivalent in currency_id, converted at the current rate. Payment providers reject dust amounts, so anything below that is unbuyable and is refused; the error names the minimum in both the plan's currency and USD.
  • price of exactly 0 publishes a free plan. Plans priced at zero can only be sold by a Starter or Growth account, never on Free: the platform fee is a share of what you charge, so a zero-priced plan earns nothing to share. On Free the call is refused on price. The rule enforced is that a plan may not be simultaneously active and priced at 0 on a creator whose plan cannot sell one, so an update that leaves a zero-priced plan off sale is allowed.
  • currency_id must be supported by at least one active payment method on the project.
  • At most one of eligibility.newcomers_only, eligibility.customers_only and eligibility.churned_only may be true.
  • resources is required with at least one id, except on kind: pass_series, where it is optional and means a lounge.
  • billing.billing_cycle_count must be 1 when billing.billing_cycle is lifetime; billing.recurring is refused as true when the currency is a crypto or platform currency.
  • pass.sales_cutoff_anchor: before_end requires pass.sales_cutoff_minutes of at least 5 and under the shortest slot's duration_minutes, and is refused outright when that shortest slot is 5 minutes or less. before_first_end and before_last_start are series-only anchors and are rejected on a pass plan, where a lone window is both the first and the last.
  • pass_series.window_ids needs at least two windows unless a rule will supply them; every id must belong to a pass plan on this project; with prevent_overlaps on, two windows that run at the same time are refused. pass_series.rules[].take is required when kind is next_n, because a count rule with no count is incomplete rather than "all of them". pass_series.successor_plan_id must be another kind: pass_series plan on the same project.
POST
/v1/projects/{project}/plans

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

Fires one event

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

MCP tool

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

Idempotent

Send the header on every call; the same key replays the original response for 24 hours.

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

Header Parameters

Idempotency-Key*string

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Formatuuid

Request body

JSONWhat the request carries

A plan to create: kind plus the one block it names, in the shape a plan is read back in.

Responses

201Created

The new plan.

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

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. On this endpoint: TEAM_TIER_REQUIRED: when kind is pass or pass_series and the project owner's account lacks Time-Limited Passes, bundled with Growth or available as the Passes add-on.

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

422Validation failed

The payload broke a rule, and error.fields maps each offending key to its messages. A refusal from the domain, such as a plan that cannot go on sale or a member who cannot be removed, uses the same code with error.message saying why and no fields. On this endpoint: VALIDATION_FAILED: when a block does not match kind, name is taken, price is below the minimum or is 0 on a Free account, currency_id has no active payment method, two eligibility flags are set, or a per-kind rule above is broken; error.fields names the key.

425Too early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

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.

Get a plan

GET

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.

Update a plan

PATCH

The same discriminated shape as create, with every field optional. Omit kind to keep the plan's current kind; a block you send must match it. Where a cross-field rule needs a value you did not send, the billing cycle behind a lifetime count check, the currency behind the price floor, the eligibility flags behind the exclusivity check, the plan's stored value stands in, so a partial update is validated against the plan it will actually produce.

  • resources is optional; omitting it preserves the existing links, sending it replaces them.
  • sales_cap may be set, raised, lowered or cleared with null; any change restarts sales_cap_sold at 0.
  • pass.slots replaces the whole schedule and rebuilds unsold future windows; pass.windows adds dated windows and never replaces.
  • Setting an active plan's price to 0 on a Free account is refused; editing a zero-priced plan that is off sale is allowed on any account.
  • sales_cap_sold, position and paused are read-only.

Answers 200 with the whole plan after the change.

PATCH
/v1/projects/{project}/plans/{plan}

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

Fires one event

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

MCP tool

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

Idempotent

Send the header on every call; the same key replays the original response for 24 hours.

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

Header Parameters

Idempotency-Key*string

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Formatuuid

Request body

JSONWhat the request carries

The changes to a plan: any subset of the create shape. A block must match the plan's kind, and a field left out keeps its stored value.

Responses

200OK

The plan after the change.

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

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. On this endpoint: TEAM_TIER_REQUIRED: when the change needs an entitlement the project owner's account lacks, such as passes on a plan without them.

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

422Validation failed

The payload broke a rule, and error.fields maps each offending key to its messages. A refusal from the domain, such as a plan that cannot go on sale or a member who cannot be removed, uses the same code with error.message saying why and no fields. On this endpoint: VALIDATION_FAILED: when a block does not match the plan's kind, name is taken by another live plan, price is below the minimum or is 0 on an active plan of a Free account, two eligibility flags end up set, or a per-kind rule is broken; error.fields names the key.

425Too early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

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.

Delete a plan

DELETE

Removes the plan and emits plan.deleted with a snapshot taken before the row disappears. Members who already hold it keep their access until it runs out; to stop selling while keeping the plan on the books, unpublish it instead. A pass or series plan is refused while a customer still holds an unfinished window on it, because deleting it would cascade away access that was paid for.

DELETE
/v1/projects/{project}/plans/{plan}

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

Fires one event

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

MCP tool

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

Idempotent

Send the header on every call; the same key replays the original response for 24 hours.

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

Header Parameters

Idempotency-Key*string

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Formatuuid

Responses

204No content

No content

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

425Too early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

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.

500Internal server error

When a customer still holds an unfinished window on the plan; error.message says so. Cancel or wait out the windows first.

Publish a plan

POST

Puts the plan on sale: active becomes true, sales_cap_sold restarts at 0, and plan.activated fires. A plan that is already on sale is answered as it is and emits nothing. Answers 200 with the plan.

POST
/v1/projects/{project}/plans/{plan}/publish

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

Fires one event

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

MCP tool

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

Idempotent

Send the header on every call; the same key replays the original response for 24 hours.

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

Header Parameters

Idempotency-Key*string

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Formatuuid

Responses

200OK

The plan, active.

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

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. On this endpoint: TEAM_TIER_REQUIRED: when the plan is priced at 0 and the project owner's account cannot sell a free plan, or when it is a pass or series and the account lacks Time-Limited Passes.

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

425Too early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

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.

Unpublish a plan

POST

Takes the plan off sale: active becomes false and plan.deactivated fires. Members who already joined keep their access; the plan stays fully editable and can be published again. A plan that is already off sale is answered as it is and emits nothing. Answers 200 with the plan.

POST
/v1/projects/{project}/plans/{plan}/unpublish

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

Fires one event

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

MCP tool

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

Idempotent

Send the header on every call; the same key replays the original response for 24 hours.

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

Header Parameters

Idempotency-Key*string

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Formatuuid

Responses

200OK

The plan, inactive.

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

425Too early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

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.

POST
curl -X POST https://api.subscriby.net/v1/projects/7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13/plans/order \  -H "Authorization: Bearer sbt_..." \  -H "Idempotency-Key: $(uuidgen)" \  -H "Content-Type: application/json" \  -d '{"plan_ids": ["c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b", "b1a7c3d5-2e48-4f60-9a1b-7c5d3e820f94"]}'

Pins the order plans appear in on the public portal and in the bot; plan_ids is every plan you want pinned, first to last. Plans left out keep the built-in order (passes, then seasons, then subscriptions, cheapest first) after the pinned ones. Send an empty plan_ids to clear every pin. Answers 200 with the project's active plans in their new order, each in the same shape as a read, with position set on the pinned ones. The same list produces the same order and one plan.order_changed event per call.

POST
/v1/projects/{project}/plans/order

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

Fires one event

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

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

Idempotent

Send the header on every call; the same key replays the original response for 24 hours.

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

Header Parameters

Idempotency-Key*string

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Formatuuid

Request body

JSONWhat the request carries

The storefront order: every plan to pin, first to last.

Responses

200OK

Array of PlanResource

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

422Validation failed

The payload broke a rule, and error.fields maps each offending key to its messages. A refusal from the domain, such as a plan that cannot go on sale or a member who cannot be removed, uses the same code with error.message saying why and no fields. On this endpoint: VALIDATION_FAILED: when a plan id belongs to another project, is repeated, or the list exceeds 200 entries; error.fields names the entry.

425Too early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

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.

Start the next season

POST
curl -X POST https://api.subscriby.net/v1/projects/7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13/plans/$SERIES_PLAN/successor \  -H "Authorization: Bearer sbt_..." \  -H "Idempotency-Key: $(uuidgen)"

The plan list's "start next season" button. For a kind: pass_series plan only: creates a new pass_series plan copied from the one named, its 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, because a season that went straight on sale with last year's dates would be selling something that has already happened. Rules are carried forward with their date bounds shifted by the length of the finished season; the slate itself and any blackouts are not, since both name windows that have run.

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. Answers 201 with the new plan in the same discriminated shape as any other read and emits plan.created for it. Compose it with a PATCH (pass_series.window_ids or pass_series.rules) and put it on sale with publish.

Not idempotent across calls. Call it once per season. Every call creates another plan, named after the source with a season number appended, and re-points the old season's successor at the newest one. Read the source plan first: if pass_series.successor_plan_id is already set, the next season exists. The Idempotency-Key protects a retry of the same call, not a second call with a new key.

POST
/v1/projects/{project}/plans/{plan}/successor

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

Fires one event

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

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

Idempotent

Send the header on every call; the same key replays the original response for 24 hours.

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 finished season, resolved within the project by the route binder.

Formatuuid

Header Parameters

Idempotency-Key*string

A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.

Formatuuid

Responses

201Created

The new season, 201, inactive with an empty slate.

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

422Validation failed

When the plan is not a pass series; error.context.plan_id names it.

425Too early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

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?

Version

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