> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.solvimon.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.solvimon.com/_mcp/server.

# Self-serve plan changes

How a customer changes what they are subscribed to from the customer portal, and what your platform has to have in place for that change to be allowed.

---

## Why this matters

A plan change is the one billing action customers want to take themselves and the one that most often lands in your support queue. The [customer portal](/platform-guides/customers-and-billing/customer-portal) lets a customer switch the pricings on their own subscription and pay for the difference in the same visit, without an agent touching the subscription.

The endpoints below sit under `/v1/portal/` and are scoped to a single customer by the portal session, so a customer can only ever change their own subscription. They are what the hosted portal and the [SDK](/sdk/frontend-components/overview) call on the customer's behalf; you use them directly when you build your own upgrade screen.

> **Note**
>
> Portal endpoints authenticate with the short-lived access token the SDK exchanges the portal token for, not with an API key. Never call them with `X-API-KEY`, and never put an API key in a browser.

## How it works

A plan change is not an edit of the running [schedule](/platform-guides/customers-and-billing/subscriptions/subscription-schedules). It is a new schedule on the same subscription, using the same pricing plan version, with a different set of enabled pricings. The previous schedule is closed at the moment the new one starts, so the subscription always has exactly one schedule in effect.

```mermaid
sequenceDiagram
    participant C as Customer
    participant P as Portal / SDK
    participant S as Solvimon
    C->>P: Picks a different set of pricings
    P->>S: POST /v1/portal/pricing-plan-schedules
    S->>S: Close current schedule, open new one
    S->>S: Refresh the next invoice
    S-->>P: New schedule
    P-->>C: Confirmation
```

The change can land at one of two moments, and which one you get depends on the subscription's billing time as much as on the request.

| Billing time | `start_at` | When the change takes effect            | Billing period                |
| ------------ | ---------- | --------------------------------------- | ----------------------------- |
| `CALENDAR`   | Omitted    | At the start of the next billing period | Unchanged                     |
| `CALENDAR`   | Now        | Rejected                                | –                             |
| `EXACT`      | Omitted    | Immediately                             | Realigns to the change moment |
| `EXACT`      | Now        | Immediately                             | Realigns to the change moment |

The only value `start_at` accepts is the current time: it is validated against the clock with a five-minute tolerance either way, so an arbitrary future date is rejected. A change scheduled for next month is a platform-side operation, not something a customer sets from the portal.

## Change at the next billing period

On a `CALENDAR` subscription, leave `start_at` out. The new schedule starts on the subscription's next invoice date, so the customer finishes the period they already paid for on the pricings they had.

**`Change pricings from the next period`**

```bash Change pricings from the next period
curl -X POST https://test.api.solvimon.com/v1/portal/pricing-plan-schedules \
  -H "Authorization: Bearer <portalAccessToken>" \
  -H "Content-Type: application/json" \
  -d '{
    "pricing_plan_subscription_id": "ppsu_example",
    "enabled_pricings": [
      { "pricing_id": "pric_premium_seat" },
      { "pricing_id": "pric_priority_support" }
    ]
  }'
```

**`Response`**

```json Response
{
  "object_type": "PRICING_PLAN_SCHEDULE",
  "id": "ppsc_example",
  "pricing_plan_subscription_id": "ppsu_example",
  "pricing_plan_version_id": "ppve_example",
  "status": "ACTIVE",
  "start_at": "2026-09-01T00:00:00Z",
  "enabled_pricings": [
    { "pricing_id": "pric_premium_seat" },
    { "pricing_id": "pric_priority_support" }
  ]
}
```

`enabled_pricings` is the complete set for the new schedule, not a delta. A pricing the customer keeps has to be listed again; anything left out is dropped from the subscription at the new start.

You do not send `pricing_plan_version_id` or `start_at`. Solvimon takes the version from the subscription's current schedule and the start from its next invoice date. A subscription with no next invoice (because it has ended, for example) is rejected: there is no date to attach the change to.

## Change immediately

On an `EXACT` subscription the change is always applied immediately, whether or not you send `start_at`. The new schedule opens straight away and the billing period realigns to that moment, so the customer is billed for the new pricings from now rather than from the old period boundary. Sending `start_at` makes that explicit.

**`Change pricings immediately`**

```bash Change pricings immediately
curl -X POST https://test.api.solvimon.com/v1/portal/pricing-plan-schedules \
  -H "Authorization: Bearer <portalAccessToken>" \
  -H "Content-Type: application/json" \
  -d '{
    "pricing_plan_subscription_id": "ppsu_example",
    "start_at": "2026-08-19T14:32:00Z",
    "enabled_pricings": [
      { "pricing_id": "pric_premium_seat" }
    ]
  }'
```

> **Warning**
>
> Realignment is only defined for `EXACT` billing. Sending `start_at` on a `CALENDAR` subscription fails with a validation error on the field, telling you to omit it and change at the next billing period instead, because realigning would move a calendar-anchored invoice date.

## Pay for the upgrade in the same step

The call above changes the subscription and leaves the money to the normal invoicing cycle. When the upgrade has to be paid before it applies, drive it from the payment instead: authorize the payment with a `CREATE_PRICING_PLAN_SCHEDULE` context and Solvimon creates the schedule once the payment is authorized.

**`Authorize a payment that creates the schedule`**

```bash Authorize a payment that creates the schedule
curl -X POST https://test.api.solvimon.com/v1/portal/payments/authorize \
  -H "Authorization: Bearer <portalAccessToken>" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_acceptor_id": "paya_example",
    "payment_gateway_variant": "ADYEN",
    "customer_id": "cust_example",
    "payment_method_id": "pmet_example",
    "amount": { "quantity": "49.00", "currency": "EUR" },
    "context": {
      "type": "CREATE_PRICING_PLAN_SCHEDULE",
      "create_pricing_plan_schedule": {
        "pricing_plan_subscription_id": "ppsu_example",
        "pricing_plan_version_id": "ppve_example",
        "pricing_id": "pric_premium_seat"
      }
    }
  }'
```

| Field                          | Required | Description                                       |
| ------------------------------ | -------- | ------------------------------------------------- |
| `pricing_plan_subscription_id` | Yes      | The subscription to add the schedule to           |
| `pricing_plan_version_id`      | Yes      | The pricing plan version the new schedule runs on |
| `pricing_id`                   | Yes      | The single pricing to enable on the new schedule  |
| `start_at`                     | No       | Defaults to the moment the payment was created    |

The schedule is only created when the payment comes back `AUTHORIZED`; a declined payment leaves the subscription untouched. The step is idempotent on the start date: if a schedule for that subscription and start already exists, because a retried payment ran the action twice, it is linked to the payment rather than duplicated.

Note the difference in shape from `POST /v1/portal/pricing-plan-schedules`: the payment context enables exactly one `pricing_id`, so it fits a single-product upgrade. Use the schedule endpoint when the customer picks a combination.

## What must be true

Both routes validate the same preconditions before anything changes, because a self-serve change is only safe when Solvimon can bill for it without you intervening.

| Check               | Requirement                                                                       |
| ------------------- | --------------------------------------------------------------------------------- |
| Subscription        | Status `ACTIVE`                                                                   |
| Customer            | Status `ACTIVE`                                                                   |
| Payment method      | An `ACTIVE` payment method on the subscription, belonging to the same customer    |
| Payment method type | `CARD`, `DIGITAL_WALLET`, `DIRECT_DEBIT`, `ONLINE_BANKING` or `BUY_NOW_PAY_LATER` |

`PREPAID` and unrecognized payment method types are rejected. Each failure returns a validation error naming the field that blocked it, so your upgrade screen can tell the customer to add a payment method rather than failing silently.

## Via Desk and the hosted portal

In the hosted portal the customer reaches this from **My subscription** through **Upgrade subscription**, which prices the change with the correct tax and shows what is due today before they confirm. That flow is described on the [customer portal](/platform-guides/customers-and-billing/customer-portal) page.

An agent making the same change in Desk edits the subscription's schedules directly and is not bound by the constraints above; the payment method and billing time rules exist to make the change safe when nobody is supervising it.

## Edge cases

* **The next invoice is refreshed, not reissued.** After the schedule is created, Solvimon runs a full refresh of the subscription's next invoice so the change is reflected immediately. If the refresh fails the schedule change still stands; the invoice picks the change up on its next regular refresh.
* **The pricing plan version is fixed.** A portal change swaps pricings within the version the customer is already on. Moving them to a different version, or a different plan, is a platform-side operation.
* **`enabled_pricings` replaces the set.** Sending only the new pricing removes everything the customer had before. Read the current schedule first and send the full intended set.
* **One schedule per start moment.** Creating a second schedule with the same start closes the first one at that same instant, which leaves a zero-length schedule. Check for an existing pending change before creating one.