Plans

Create a plan

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

Request

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

Path parameters

projectstring uuid required

The project, resolved by the route binder.

Headers

Idempotency-Keystring uuid required

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.

Request body

kind'subscription' | 'pass' | 'pass_series' required

What a purchase of a creator's plan actually buys. Plan shape had no discriminator before this. It was inferred from pass_enabled plus recurring plus billing_cycle, which is why a third shape kept falling through filters written as where('pass_enabled', false): a boolean cannot express three things, so every query site had to remember which combination it meant and one of them always forgot. A match over this enum with no default is exhaustive, so a fourth shape later fails loudly at every site that has to care rather than silently taking whichever branch happened to be last.

subscription <br/> Access starts at payment and runs on a billing cycle.
pass <br/> One purchase buys one of the plan's own scheduled access windows.
pass_series <br/> One purchase buys a curated slate of other plans' access windows.
namestring required

The name buyers see, 5 to 255 characters, unique within the project without regard to case or surrounding spaces. Required on create.

descriptionstring nullable

The pitch under the name, up to 1,000 characters. HTML is filtered to a safe subset before it is stored.

currency_idstring required

The id of a currency that at least one of the project's active payment methods can charge. Required on create.

pricenumber required

What one purchase costs: at least the $1.00 USD equivalent in currency_id at the current rate, or exactly 0 for a free plan, which only a Starter or Growth account may put on sale. Required on create.

activeboolean

Whether the plan is on sale. Defaults to true on create; prefer the publish and unpublish endpoints to change it later.

sales_capinteger nullable

Pause the plan after this many purchases, 1 to 100,000; null for no limit. Changing it restarts sales_cap_sold at 0.

resourcesstring[] required

Ids of the project's resources the plan grants: at least one, required on create, except on kind: pass_series, where it is optional and names a lounge open for the whole span. Omit on an update to keep the existing links.

Example request

{
  "billing": {
    "billing_cycle": "month",
    "trial_type": "plan"
  }
}

Response

The new plan.

Example response

{
  "data": {
    "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b",
    "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13",
    "kind": "subscription",
    "name": "Premium Monthly",
    "description": "Unlimited access to the research channel.",
    "price": "29.00",
    "currency": {
      "id": "8f27a0d4-63be-4915-8c07-1a5d9e34b628",
      "iso": "USD",
      "symbol": "$"
    },
    "active": true,
    "sales_cap": 10,
    "sales_cap_sold": 3,
    "cadence": "Per Month",
    "resources": [
      {
        "id": "b73c5f21-9d80-4a6e-8215-4f70ce13a9d6",
        "name": "Research channel",
        "kind": "telegram:supergroup",
        "connector": "telegram"
      }
    ],
    "billing": {
      "billing_cycle": "month",
      "billing_cycle_count": 1,
      "recurring": true,
      "trial_days": 7,
      "trial_type": "plan"
    },
    "pass": {
      "timezone": "America/New_York",
      "schedule_mode": "repeating",
      "recurrence": "weekly",
      "sales_cutoff_minutes": 60,
      "sales_cutoff_anchor": "before_start",
      "slots": [
        {
          "start_time": "09:00",
          "duration_minutes": 840
        }
      ],
      "upcoming_windows": [
        {
          "id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738",
          "starts_at": "2026-09-20T13:00:00Z",
          "ends_at": "2026-09-21T03:00:00Z",
          "status": "scheduled"
        }
      ]
    },
    "pass_series": {
      "timezone": "America/New_York",
      "prevent_overlaps": true,
      "sales_cutoff_anchor": "before_start",
      "seat_cap": 50,
      "seats_taken": 37,
      "seats_remaining": 13,
      "starts_at": "2026-09-20T13:00:00Z",
      "ends_at": "2026-11-29T22:00:00Z",
      "window_count": 10,
      "presale_hours": 48,
      "windows": [
        {
          "id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738",
          "plan_id": "0b8e6a2f-4c1d-4e3a-9f52-7d6c1b2a3e45",
          "plan_name": "Match Day Pass",
          "starts_at": "2026-09-20T13:00:00Z",
          "ends_at": "2026-09-20T16:00:00Z",
          "status": "scheduled"
        }
      ],
      "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"
        }
      ]
    },
    "created_at": "2026-05-18T10:05:00Z",
    "updated_at": "2026-05-18T10:05:00Z"
  }
}

Changes

No recorded changes to this operation across all 1 revision of this API.