Preview a plan or quantity change
The portal.subscriptions.preview_change operation: POST /api/v1/portal/me/subscriptions/{subscriptionId}/preview_change in the REST API and the portal_subscriptions_preview_change tool in the MCP server.
/api/v1/portal/me/subscriptions/{subscriptionId}/preview_changeShows exactly what switching one item of the signed-in customer's subscription to another plan, or changing its
quantity, would do, without changing anything: an upgrade applies now and its invoice is issued and charged now
(invoice: the credit for the unused time, the charge for the time left, tax and the amount due); a downgrade
applies at the end of the period with nothing to pay now. The brand's portal settings decide what may change:
a plan switch only to a plan its plan-change matrix lists for the item's plan, a quantity change only when the
brand allows it. Confirm with portal.subscriptions.update and the preview's version.
Proration is exact to the second, so the real change may differ by the time that passed. Customer portal only
(portal:read).
At a glance
- REST:
POST /api/v1/portal/me/subscriptions/{subscriptionId}/preview_change - MCP tool:
portal_subscriptions_preview_change - Operation ID:
portal.subscriptions.preview_change - Scope: Brand
- Permission:
portal:read - Risk tier:
read - Dry run: Not supported
- Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: A customer signed in to the customer portal (API keys can't call it)
- Since: M9
Request
Path parameters
| Field | Type | Description |
|---|---|---|
subscriptionId | sub_… ID | One of your subscriptions (sub_…). |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
itemId | si_… ID | Yes | The subscription item to change (si_…). |
priceId | price_… ID | No | Switch the item to this price: one the brand's plan-change matrix allows from its price. |
quantity | integer (1 to 1000000) | No | The item's new quantity (seats), when the brand lets customers change it. |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
object | "portal_subscription_change_preview" | |
subscriptionId | sub_… ID | |
direction | one of upgrade, downgrade, lateral | |
effective | one of now, period_end | Upgrades apply now; downgrades at the period end. |
effectiveAt | date-time | |
settlement | one of invoice_now, pending_items, credit_balance, pending_update, none | invoice_now: invoiced and charged now (invoice); pending_items: added to your next invoice; credit_balance: credited to your balance; pending_update: applies at effectiveAt; none: nothing to bill. |
invoice | object or null | |
pendingItems | array of object | |
pendingItems[].description | string | |
pendingItems[].amount | money | |
pendingItems[].periodStart | date-time or null | |
pendingItems[].periodEnd | date-time or null | |
credit | money or null | |
items | array of object | The subscription's items after the change. |
items[].priceId | price_… ID | |
items[].productId | prod_… ID | |
items[].productName | string | |
items[].priceName | string or null | The price's nickname, e.g. “Pro, monthly”. |
items[].interval | object | |
items[].interval.interval | one of day, week, month, year | |
items[].interval.intervalCount | integer | |
items[].usageType | one of licensed, metered | |
items[].unitAmount | money or null | A per-unit price's amount per unit and period (null for tiered and metered prices). |
items[].taxBehavior | one of inclusive, exclusive | exclusive: tax is added on the invoice. |
items[].id | si_… ID | |
items[].quantity | integer | |
items[].amount | money or null | The item's full-period amount at its quantity, before discounts (null for metered items). |
periodAmount | money | The licensed items' full-period amount after the change, before tax. |
version | integer | Confirm with this as expectedVersion. |
Examples
Upgrading from Plus to Pro mid-period
POST /api/v1/portal/me/subscriptions/sub_01m4tx5nj8emwbyf7y9vcf48c0/preview_change
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"itemId": "si_01m4tx5nj8emwbyf7y9vcf48c1",
"priceId": "price_01m4tx5nj8emwbyf7y9vcf48c3"
}
Response (200):
{
"object": "portal_subscription_change_preview",
"subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf48c0",
"direction": "upgrade",
"effective": "now",
"effectiveAt": "2026-11-05T09:30:00.000Z",
"settlement": "invoice_now",
"invoice": {
"currency": "EUR",
"lines": [
{
"description": "Unused time on Genno Plus (Plus, monthly)",
"quantity": 1,
"amount": {
"amount": "-4500000",
"currency": "EUR"
},
"proration": true,
"periodStart": "2026-11-05T09:30:00.000Z",
"periodEnd": "2026-11-20T10:00:00.000Z"
},
{
"description": "Remaining time on Genno Pro (Pro, monthly)",
"quantity": 1,
"amount": {
"amount": "14500000",
"currency": "EUR"
},
"proration": true,
"periodStart": "2026-11-05T09:30:00.000Z",
"periodEnd": "2026-11-20T10:00:00.000Z"
}
],
"subtotal": {
"amount": "8196721",
"currency": "EUR"
},
"tax": {
"amount": "1803279",
"currency": "EUR"
},
"total": {
"amount": "10000000",
"currency": "EUR"
},
"creditApplied": null,
"amountDue": {
"amount": "10000000",
"currency": "EUR"
}
},
"pendingItems": [],
"credit": null,
"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"
}
}
],
"periodAmount": {
"amount": "29000000",
"currency": "EUR"
},
"version": 4
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | A field is malformed, or neither priceId nor quantity was sent. | Fix the fields listed in details.issues and send the request again. |
resource_not_found | 404 | You 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_found | 404 | itemId 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_allowed | 403 | The 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_invalid | 400 | The 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_invalid | 409 | The 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_invalid | 400 | The 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_blocked | 422 | The 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_required | 422 | The 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. |
livemode_not_allowed | 403 | The 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_denied | 403 | The 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, dry_run_not_supported, rate_limited, internal_error.