Skip to content

Switch plan or change the quantity

The portal.subscriptions.update operation: POST /api/v1/portal/me/subscriptions/{subscriptionId} in the REST API and the portal_subscriptions_update tool in the MCP server.

POST/api/v1/portal/me/subscriptions/{subscriptionId}

Switches one item of the signed-in customer's subscription to another plan, or changes its quantity, as portal.subscriptions.preview_change showed: an upgrade applies now, its prorated invoice is issued and charged to the subscription's card (or sent, for a subscription paid by bank transfer); a downgrade is scheduled for the end of the period (scheduledChange). The brand's portal settings decide what may change, and its billing settings how a failed payment is handled. Send the preview's version as expectedVersion. Emits subscription.updated. Customer portal only (portal:write).

At a glance

  • REST: POST /api/v1/portal/me/subscriptions/{subscriptionId}
  • MCP tool: portal_subscriptions_update
  • Operation ID: portal.subscriptions.update
  • Scope: Brand
  • Permission: portal:write
  • Risk tier: financial
  • Dry run: Supported (send Ownrecord-Dry-Run: true)
  • Idempotency: Idempotency-Key required
  • Modes: Test and live
  • Authentication: A customer signed in to the customer portal (API keys can't call it)
  • Since: M9

Request

Path parameters

FieldTypeDescription
subscriptionIdsub_… IDOne of your subscriptions (sub_…).

Body

A JSON object (Content-Type: application/json):

FieldTypeRequiredDescription
itemIdsi_… IDYesThe subscription item to change (si_…).
priceIdprice_… IDNoSwitch the item to this price: one the brand's plan-change matrix allows from its price.
quantityinteger (1 to 1000000)NoThe item's new quantity (seats), when the brand lets customers change it.
expectedVersioninteger (at least 1)NoThe subscription's version you saw (the preview's); if it changed meanwhile, version_conflict.

Response

200 OK with a JSON object:

FieldTypeDescription
idsub_… ID
object"portal_subscription"
statusone of incomplete, incomplete_expired, trialing, active, past_due, unpaid, paused, canceled
currencystring
itemsarray of object
items[].priceIdprice_… ID
items[].productIdprod_… ID
items[].productNamestring
items[].priceNamestring or nullThe price's nickname, e.g. “Pro, monthly”.
items[].intervalobject
items[].interval.intervalone of day, week, month, year
items[].interval.intervalCountinteger
items[].usageTypeone of licensed, metered
items[].unitAmountmoney or nullA per-unit price's amount per unit and period (null for tiered and metered prices).
items[].taxBehaviorone of inclusive, exclusiveexclusive: tax is added on the invoice.
items[].idsi_… ID
items[].quantityinteger
items[].amountmoney or nullThe item's full-period amount at its quantity, before discounts (null for metered items).
collectionMethodone of charge_automatically, send_invoicecharge_automatically (your card is charged) or send_invoice (paid by bank transfer).
paymentMethodIdpm_… ID or nullThe card this subscription charges (null: your default card).
currentPeriodStartdate-time
currentPeriodEnddate-time
trialEnddate-time or null
cancelAtPeriodEndboolean
cancelAtdate-time or null
canceledAtdate-time or null
endedAtdate-time or null
cancellationobject or null
discountsarray of object
discounts[].iddisc_… ID
discounts[].namestringThe coupon's name.
discounts[].percentOffPpmstring or nullParts per million as a string (20% = "200000").
discounts[].amountOffmoney or null
discounts[].startdate-time
discounts[].enddate-time or nullWhen it stops applying (null: for as long as the subscription runs).
scheduledChangeobject or nullA change that applies at effectiveAt (a downgrade waits for the period end).
actionsobjectWhat you can do with this subscription now, as the brand's portal settings allow.
actions.switch_planboolean
actions.change_quantityboolean
actions.apply_promotion_codeboolean
actions.cancelboolean
actions.reactivateboolean
actions.change_payment_methodboolean
cancelBehaviorone of period_end, immediateHow cancelling here ends the subscription.
collectCancelReasonbooleanCancelling asks why (optional answers).
versionintegerSend it back as expectedVersion to change what you saw.
createdAtdate-time

Examples

Move up to Pro now

HTTP
POST /api/v1/portal/me/subscriptions/sub_01m4tx5nj8emwbyf7y9vcf48c0
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "itemId": "si_01m4tx5nj8emwbyf7y9vcf48c1",
  "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3",
  "expectedVersion": 4
}

Response (200):

JSON
{
  "id": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
  "object": "portal_subscription",
  "status": "active",
  "currency": "EUR",
  "items": [
    {
      "priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3",
      "productId": "prod_01m4tx5nj8emwbyf7y9vcf48c5",
      "productName": "Genno Pro",
      "priceName": "Pro, monthly",
      "interval": {
        "interval": "month",
        "intervalCount": 1
      },
      "usageType": "licensed",
      "unitAmount": {
        "amount": "29000000",
        "currency": "EUR"
      },
      "taxBehavior": "inclusive",
      "id": "si_01m4tx5nj8emwbyf7y9vcf48c1",
      "quantity": 1,
      "amount": {
        "amount": "29000000",
        "currency": "EUR"
      }
    }
  ],
  "collectionMethod": "charge_automatically",
  "paymentMethodId": null,
  "currentPeriodStart": "2026-10-20T09:00:00.000Z",
  "currentPeriodEnd": "2026-11-20T10:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "cancelAt": null,
  "canceledAt": null,
  "endedAt": null,
  "cancellation": null,
  "discounts": [],
  "scheduledChange": null,
  "actions": {
    "switch_plan": true,
    "change_quantity": false,
    "apply_promotion_code": true,
    "cancel": true,
    "reactivate": false,
    "change_payment_method": true
  },
  "cancelBehavior": "period_end",
  "collectCancelReason": true,
  "version": 5,
  "createdAt": "2026-09-20T09:00:00.000Z"
}

Errors

CodeHTTP statusWhenWhat to do
validation_failed400A field is malformed, or neither priceId nor quantity was sent.Fix the fields listed in details.issues and send the request again.
resource_not_found404You have no such subscription (another customer's reads the same).Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.
subscription_item_not_found404itemId isn't one of the subscription's items.Read the subscription and use one of its items[].id (si_…); to add a new price, leave out id.
portal_action_not_allowed403The brand's portal settings or the subscription's state don't allow this change: plan switches need the brand's plan-change matrix to list the target, quantity changes need the brand's permission and a licensed item (details.action).The brand's portal settings don't allow it for this subscription now (details.action): contact the brand.
subscription_change_invalid400The change can't be made now (for example a change already waits for its invoice to be paid).Check details.reason and adjust the request.
subscription_status_invalid409The subscription can't change in its status.Check details.status: items change only while trialing, active or past due; pausing needs an active subscription and resuming a paused one; reactivating needs a cancellation scheduled for the period end; canceled and expired subscriptions don't change.
subscription_price_invalid400The new plan's price is archived.Use active recurring prices of one currency whose intervals are whole multiples of each other (monthly with yearly, weekly with 2-weekly). details.reason says what's wrong and param names the item.
jurisdiction_blocked422The brand can't sell in your country now (an invoice due now can't be issued).This jurisdiction isn't covered by usable tax knowledge for this seller. Contact support if you need to sell there.
registration_required422The brand can't invoice your country yet (an invoice due now can't be issued).Register the entity for the regime named in details.regime (see its prerequisites and deadline), or arm the jurisdiction once the prerequisites are done. The customer can join the waitlist meanwhile.
version_conflict409The subscription changed since you saw it (expectedVersion); look again.Fetch the object again and retry the change.
livemode_not_allowed403The portal session is in the other mode than the request.Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin.
permission_denied403The caller isn't a customer signed in to this brand's portal (operators and API keys use the subscription and wallet operations).Ask an organization owner or admin for a role or scope that allows this action.

It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, rate_limited, internal_error.