Version
POST

Create a plan

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

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. A plan created on sale (active: true, the default) is queued for the push to the payment methods that keep a product catalogue (Stripe, PayPal, CoinPayments, Razorpay), so it is sellable through them within a minute; a draft is pushed when it is published.

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.

How is this guide?

Last updated on