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-6e0f27a94d3bto $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-…and9e12f0b4-…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: apriceof0without Starter or Growth, or apass/pass_serieschange without Time-Limited Passes.error.context.reasonsays which.VALIDATION_FAILED— one entry per offending field inerror.context: akindargument that differs from the stored kind; a nested block belonging to another kind;nameoutside 5–255 characters;pricenegative or not a number;currency_idunknown; more than one ofeligibility.newcomers_only,customers_only,churned_only;resourcesnot a list, or empty on asubscriptionorpass;billing.billing_cycle_countother than1with alifetimecycle; or a rule the write itself refuses — a duplicate name, a currency no active payment method supports, apass.windowsentry in the past or colliding with an existing start, a window id inpass_series.window_idsthe project does not own. Aplanentry means the caller's role lacks the team's plan-update permission.RESOURCE_NOT_FOUND— unknownplan_id, a plan on another team's project, one outside the token'sscope:project:allow-list, or a plan that has been deleted.AUTHENTICATION_REQUIRED— no authenticated user on the request.TOKEN_MISSING_ABILITY— token lacksproject-subscription-plan:update.
Related
get_plan— read the current values and the kind first.publish_plan— flipactivewith its own event.create_plan— the same blocks, on a new plan; the only way to get a different kind.list_pass_windows— resolve ids forpass_series.window_ids.list_resources— resolve ids forresources.- Plans API — the REST equivalent and the per-kind rules.
- Subscription Plans — the dashboard walkthrough.
How is this guide?
start_next_season
Duplicate a finished pass series into its next season — a new inactive pass_series plan with the same settings, an empty slate, and the old season linked to it. Not idempotent.
cancel_subscription
Queue a subscription cancellation. Provider-side cancellation and local state mutation run asynchronously on the webhooks queue.