Plans
List and manage club plans synchronized with a connected Stripe account.
Stripe provider failures return a masked INTERNAL_SERVER_ERROR with a
server-generated extensions.requestId. Use that ID to correlate the sanitized
server diagnostic. Expected business validation errors keep their public codes;
provider messages and payloads are never returned to clients.
The plans query returns active plans. Pass clubId to list only plans mapped to
the selected Stripe merchant for that club. Omitting it retains the global active
catalog used by the public application flow. Plans replaced by a split
are omitted unless includeReplaced: true; the back-office Plans page requests them.
query Plans($clubId: ID) {
plans(clubId: $clubId) {
id
name
description
amount
currency
interval
type
stripeId
subscriberPrices {
amount
subscribers
}
}
}The response is a non-null list with non-null entries:
{
"data": {
"plans": [
{
"id": "60000000-0000-4000-8000-000000000001",
"name": "Adhésion 51 IÉNA (2026) - Abonnement Membre 2026",
"description": "Adhésion annuelle donnant accès aux services et événements réservés aux membres.",
"amount": 180000,
"currency": "eur",
"interval": "year",
"type": "subscription",
"stripeId": "champagne_iena_membre_2026",
"subscriberPrices": [
{ "amount": 150000, "subscribers": 4 },
{ "amount": 180000, "subscribers": 12 }
]
}
]
}
}Return fields
| Field | Type | Meaning |
|---|---|---|
id | ID! | Internal UUID for the plan and membership relationship. |
name | String! | Display name. |
description | String | Optional plan description. |
amount | Int! | Nonnegative price: currency minor units for memberships, credit hundredths for add-ons, euro cents for credit packs. |
currency | String! | Lowercase three-letter currency code. |
interval | String! | Billing interval: month or year. |
creditAmount | Int | Fixed pack quantity in hundredths of a credit; required for credit packs, null for other plans. |
type | PlanType! | subscription, addon, or credit_pack. |
stripeId | String | Optional catalogue reference; not a universal connected Product/Price ID. |
subscriberPrices | [PlanSubscriberPrice!] | Memberships with a Stripe subscription and a running paid period on this plan, grouped by the amount they currently pay and ordered by amount. Populated by plans for staff only; null without a staff session, on plans returned by mutations or nested in memberships. |
replacedBy | PlanReplacement | { id, name } of the plan that replaced this one in a split; null otherwise. Populated by plans. |
Stripe price references belong to memberships rather than plans. See the
person query for membership fields. The back office converts
amount from minor units for display. The back office always supplies its selected
club ID. The editor offers searchable, localized currency names from Stripe's card
presentment currency list; availability can still depend on the connected account
and payment method. It preserves the entered major-unit price when changing the
currency and rejects unsupported fractional amounts rather than rounding them.
For example, 1,200 JPY is sent as amount: 1200, while 12 EUR is sent as
amount: 1200. Stripe's ISK and UGX compatibility rules still use a factor of
100 but only permit whole major-unit amounts.
The back-office table can sort its returned plans by name, type, amount, or
billing interval in either direction. This sorts the loaded list (up to 100
plans); the GraphQL plans query still returns its default name/ID order.
Create and update
mutation CreatePlan($input: PlanInput!) {
createPlan(input: $input) {
id
name
amount
currency
interval
type
}
}PlanInput contains clubId, a retry-stable UUID requestId, name, optional
description, nonnegative minor-unit amount, lowercase three-letter currency,
interval (month or year) and type (subscription, addon, or credit_pack). Names are
trimmed and limited to 200 characters; descriptions are limited to 1,000 characters;
amounts cannot exceed 100000000 minor units.
createPlan creates a Product and recurring Price on the club's selected connected
Stripe account before saving their local mapping. Retrying the same request uses the
same Stripe idempotency keys and local plan ID.
mutation UpdatePlan($id: ID!, $input: PlanInput!) {
updatePlan(id: $id, input: $input) {
id
name
amount
currency
interval
}
}updatePlan updates the plan's name and description and its Stripe Product. A plan's
currency, interval and type are fixed once it exists, and so is a subscription's
amount: an update that changes any of them returns a BAD_USER_INPUT error before any
Stripe call, and the back-office editor only shows those fields when creating a plan. To
give a subscription a new price, split it.
Add-ons and credit packs keep an editable amount. Stripe Prices are immutable, so a new
amount creates a replacement Price, updates the local mapping and archives the previous
Price. Subscriptions keep renewing on the Price they were created with: every Price created
for a plan is recorded, and renewals are validated against the Price the member is
subscribed to.
Fields that cannot be updated
Configuration tools that compare a declared plan with the stored one cannot call
updatePlan when one of these fields changes:
| Field | Subscription | Add-on, credit pack |
|---|---|---|
amount | Split the plan | Updated in place, with a new requestId |
type | Delete, then create | Delete, then create |
currency | Delete, then create | Delete, then create |
interval | Delete, then create | Delete, then create |
name, description and a credit pack's creditAmount are always updated in place. An
in-place amount change creates a Stripe Price under the update's requestId, so that id
must differ from the ones already used for the plan.
A subscription changes price with splitPlan, passing its current amount as
subscriberAmount: its subscribers keep the plan and the price they pay, and the new plan
takes its place for everyone else. A change of type, currency or interval needs the
plan deleted and created again, which is refused while members hold it.
Read one plan
query Plan($clubId: ID!, $id: ID!) {
plan(clubId: $clubId, id: $id) {
id
name
description
amount
creditAmount
currency
interval
type
}
}Returns one plan offered by the club's selected merchant, including a plan replaced by a
split, or null. Like plans, it is public. Both ids must be UUIDs. subscriberPrices is
null unless the caller manages the catalog of that club.
Split a plan
Splitting gives new and current subscribers different prices. The split plan keeps its name, ID and memberships and is reserved to its current subscribers; a new plan with a name chosen by staff takes its place for everyone else.
mutation SplitPlan($input: SplitPlanInput!) {
splitPlan(input: $input) {
plan {
id
name
amount
}
previous {
id
name
amount
}
scheduled
}
}{
"input": {
"clubId": "51000000-0000-4000-8000-000000000051",
"planId": "60000000-0000-4000-8000-000000000001",
"requestId": "7f7a2b3c-1d2e-4f50-8a6b-9c0d1e2f3a4b",
"name": "Adhésion 51 IÉNA (2027) - Abonnement Membre",
"amount": 200000,
"subscriberAmount": 190000
}
}SplitPlanInput contains clubId, the subscription planId to split, a retry-stable
UUID requestId (also the new plan's ID), the new plan's trimmed name (1–200
characters) and optional description, and two minor-unit prices (0–100000000):
amount for new subscribers and subscriberAmount for current subscribers. Currency
and interval are copied from the split plan. Only subscription plans can be split, and
a plan can be split once; retrying the same requestId returns the same result.
splitPlan returns the new plan, the retained previous plan and the number of
current subscribers whose price change was scheduled. It:
- creates a Stripe Product and Price for the new plan and maps it to the club's merchant;
- keeps the split plan's name, sets its
amounttosubscriberAmountand links it to the new plan throughreplacedBy. WhensubscriberAmountdiffers from its current amount, a new Price is created on the split plan's Product; - replaces the split plan with the new plan in every
PLAN_SELECTIONblock of the club's onboardings and increments their revision. Blocks offering all plans need no change; - hides the split plan from
plans(unlessincludeReplaced: true), frommembershipOffers, from Checkout and from application plan selection; - schedules the price change for current subscribers, as described below, and records
plan.splitin staff activity withreplacedBy,newSubscriberAmount,subscriberAmount,subscribersRepricedandsubscriberPriceFrom.
Current subscribers are memberships of the split plan with a Stripe subscription, a paid
period that has not ended, and a Price in the same currency and interval. Members already
paying subscriberAmount are left unchanged. The others get the new Price scheduled one
calendar month later and receive a renewal notification (in-app and email, per their
preferences) with the old and new amounts and the renewal date from which the new price
is charged.
The new price applies from the member's next payment, or one month after the split if that payment is sooner. The notification states that date. A background worker switches each subscription item to the scheduled Price once the notice month has passed:
- if the subscription has not renewed since the split, the switch uses
proration_behavior: "none"and the next renewal charges the new price; - if it renewed during the notice month (at the old price), the switch uses
proration_behavior: "always_invoice", so Stripe immediately invoices the prorated difference for the rest of the current period (a credit when the price goes down). Proration invoices do not change the membership's paid period.
Subscriptions that have ended in Stripe are dropped from the schedule.
Applications that already selected the split plan but have not paid can no longer check out with it.
Delete
mutation DeletePlan($id: ID!, $clubId: ID!) {
deletePlan(id: $id, clubId: $clubId)
}A plan members still hold cannot be deleted: the mutation answers BAD_USER_INPUT with
Members still hold this plan. End or move their memberships before deleting it. and
archives nothing. A member holds a plan while their membership has no end date, has not
expired, or is inside a paid period; ended memberships do not count. This applies to a plan
replaced by a split too, for as long as its subscribers stay on it.
Otherwise deletion archives the connected Price and Product, removes the merchant mapping and hides the local plan. Historical plans referenced by memberships or checkout attempts remain stored. If several clubs share the same merchant profile, they share its plan catalog too.
Ordering and limits
Plans are ordered by name and then ID. The query returns at most 100 records and
does not accept sorting or pagination arguments. An empty catalog
returns [].
The plan query is public for application flows. Plan mutations (createPlan, updatePlan,
deletePlan and splitPlan) require a staff session that administers the selected club or
holds its catalog_manage permission; see staff permissions.
Mutations require configured sandbox Stripe billing and a selected merchant. Checkout
resolves active subscription Product/Price mappings by club → selected merchant → local plan. Use
membershipOffers(clubId) for purchasing readiness and offers.
Club credits
See Club credits for credit packs, balances, refill history, credit-priced add-ons and reservations, and cancellation refunds. Membership subscriptions stay billed in money.
Credit packs require a positive EUR amount and create a one-time Stripe price.
Their stored interval is ignored for billing. Rate settings determine the
credits granted, snapshotted when Checkout starts. Add-ons are also purchased
once with credits; their interval is not a recurring charge.
Credit packs require creditAmount in PlanInput (1–100,000,000 hundredths). For example, amount: 10000, creditAmount: 125000 sells 1,250 credits for €100. Changing the club rate does not change this quantity.