update_plan

Change one or more fields of a plan. Partial by design — anything omitted keeps its stored value; the kind is fixed. Emits plan.updated.

Purpose

Change an existing plan without restating it. Only the arguments present in the call reach the write, the way the REST PATCH behaves, so repricing a plan is one field, not a re-creation. The shared fields are checked the way create_plan checks them, and the one nested block the plan's kind names is shaped exactly as create_plan shapes it.

Synchronous — the row after the change comes back, and plan.updated emits once with the fields that actually moved. A call whose arguments change nothing returns the current row and emits nothing.

The kind is fixed, and so is which block you may send

A plan's kind is set at creation and cannot change; a kind argument that disagrees with the stored one is refused rather than converting a subscription into a pass — create a new plan instead. The same rule decides the nested block: billing only on a subscription, pass only on a pass, pass_series only on a pass_series. A block of another kind fails with VALIDATION_FAILED naming the stray key; it is never silently ignored.

`resources` replaces, it does not append

Send the full list of resource UUIDs the plan should grant. Whatever is missing from the list is unlinked. A subscription or pass must keep at least one; a pass_series may be empty, because each window in a series grants its own plan's resources.

Flipping `active` here is not the same as publish_plan

active is accepted, and it works, but it is announced as a field inside plan.updated. publish_plan flips the same flag with its own plan.activated / plan.deactivated event, which is what an automation reacting to plans going on or off sale should key on.

Two tiers of entitlement are checked on the way in: a price of 0 makes the plan free, which needs the Starter or Growth tier, and anything touching a pass or pass_series needs Time-Limited Passes. Either failing surfaces as TEAM_TIER_REQUIRED with nothing written.

Required ability

project-subscription-plan:update

Input schema

{
  "type": "object",
  "required": ["plan_id"],
  "properties": {
    "plan_id": {
      "type": "string",
      "description": "UUID of the plan to change."
    },
    "name": {
      "type": "string",
      "description": "Plan name (min 5, max 255 chars, unique per project)."
    },
    "description": {
      "type": "string",
      "description": "Plan description. Max 1000 chars, HTML is filtered. Pass an empty string to clear."
    },
    "price": {
      "type": "number",
      "description": "What one purchase costs, as a decimal. At least the $1.00 USD equivalent unless 0, which makes the plan free and requires the Starter or Growth tier."
    },
    "currency_id": {
      "type": "string",
      "description": "UUID of the currency. Must be supported by an active payment method on the project."
    },
    "active": {
      "type": "boolean",
      "description": "true publishes the plan, false takes it off sale; existing subscriptions keep running either way."
    },
    "sales_cap": {
      "type": "integer",
      "description": "Pause the plan automatically after this many successful purchases (1-100000). Changing it restarts the count; null removes the limit."
    },
    "eligibility": {
      "type": "object",
      "description": "Audience restrictions: {newcomers_only?: bool, customers_only?: bool, churned_only?: bool, single_use?: bool, access_codes_only?: bool}. The first three are mutually exclusive."
    },
    "resources": {
      "type": "array",
      "description": "The full list of project_resources UUIDs this plan grants access to; replaces the current list. Must not be empty on kind subscription or kind pass."
    },
    "billing": {
      "type": "object",
      "description": "Only on a plan of kind subscription. {billing_cycle: days|weeks|months|years|lifetime, billing_cycle_count: int 1-99, recurring?: bool, disabled_renewal?: bool, trial_days?: int 0-365, trial_cardless?: bool, trial_type?: string}. lifetime requires billing_cycle_count=1."
    },
    "pass": {
      "type": "object",
      "description": "Only on a plan of kind pass. {timezone: IANA zone, schedule_mode?: repeating|fixed, recurrence?: daily|weekly|monthly, recurrence_ends_at?: ISO-8601, sales_cutoff_minutes?: int, sales_cutoff_anchor?: before_start|before_end, slots?: array, windows?: array}; the same shape `create_plan` documents."
    },
    "pass_series": {
      "type": "object",
      "description": "Only on a plan of kind pass_series. {window_ids?: array of pass-window UUIDs, prevent_overlaps?: bool, sales_cutoff_minutes?: int, sales_cutoff_anchor?: string, seat_cap?: int, successor_plan_id?: UUID, presale_hours?: int, blackout_window_ids?: array, rules?: array}; the same shape `create_plan` documents. Window ids come from `list_pass_windows`."
    }
  }
}

A pass.timezone is canonicalised before it is stored, and a pass block without schedule_mode defaults to repeating. pass.windows places hand-dated windows in the plan's own timezone, the same way create_pass_window does.

Output shape

{
  "data": {
    "id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b",
    "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13",
    "name": "Premium Monthly",
    "description": "All-access pass",
    "price": "34.00",
    "currency": "USD",
    "kind": "subscription",
    "cadence": "Per Month",
    "active": true,
    "created_at": "2026-05-18T10:05:00+00:00",
    "billing": {
      "billing_cycle": "months",
      "billing_cycle_count": 1,
      "recurring": true,
      "trial_days": 14,
      "trial_cardless": false
    }
  }
}

The full get_plan row, re-read after the write so a schedule or slate change is reflected in the block. price is a decimal string. The block present depends on the plan's kind; see get_plan for the pass and pass_series shapes.

Example prompts

"Raise plan c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b to $34 and extend the trial to 14 days."

"Rename the Match Day Pass to 'Home Match Day Pass' — leave everything else alone."

"Add windows 3d5a8c72-… and 9e12f0b4-… to the season ticket's slate, and cap it at 60 seats."

Failure modes

  • TEAM_TIER_REQUIRED — the change needs a tier the project owner lacks: a price of 0 without Starter or Growth, or a pass / pass_series change without Time-Limited Passes. error.context.reason says which.
  • VALIDATION_FAILED — one entry per offending field in error.context: a kind argument that differs from the stored kind; a nested block belonging to another kind; name outside 5–255 characters; price negative or not a number; currency_id unknown; more than one of eligibility.newcomers_only, customers_only, churned_only; resources not a list, or empty on a subscription or pass; billing.billing_cycle_count other than 1 with a lifetime cycle; or a rule the write itself refuses — a duplicate name, a currency no active payment method supports, a pass.windows entry in the past or colliding with an existing start, a window id in pass_series.window_ids the project does not own. A plan entry means the caller's role lacks the team's plan-update permission.
  • RESOURCE_NOT_FOUND — unknown plan_id, a plan on another team's project, one outside the token's scope:project: allow-list, or a plan that has been deleted.
  • AUTHENTICATION_REQUIRED — no authenticated user on the request.
  • TOKEN_MISSING_ABILITY — token lacks project-subscription-plan:update.

How is this guide?

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