---
title: "Arrange the storefront order"
method: POST
path: "/v1/projects/{project}/plans/order"
tags: ["Plans"]
---

# Arrange the storefront order

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

## Request

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

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

## Path parameters

- `project` string, uuid, required

## Headers

- `Idempotency-Key` string, uuid, required

## Request body

- ReorderPlansRequest: The storefront order: every plan to pin, first to last.
  - `plan_ids` string[], required: Plan ids in storefront order, at most 200, each a plan of this project and none repeated. An empty list clears every pin.

## Response `200`

Array of `PlanResource`

- object
  - `data` PlanResource[], required: The items.
    - `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

- `400`: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation.
- `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.
- `409`: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body.
- `422`: `VALIDATION_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.
- `425`: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed.
- `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)
