> For the complete documentation index, see [llms.txt](https://docs.limio.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.limio.com/guides/developer-guides/guide-manage-subscription-updates.md).

# Guide: Manage subscription updates

### Overview

When existing subscribers want to upgrade, downgrade, or add extras to their subscription, you need a checkout flow that understands their current state. The Subscription Checkout API provides a stateful basket that:

* Knows what the subscriber currently has (their active offer and add-ons)
* Calculates what they can change to (upgrades, downgrades, cross-sells)
* Handles the business logic automatically (removing old offers when adding new ones, checking compatibility)

This gives you several advantages:

* **No eligibility logic in the browser.** The API determines valid upgrade/downgrade paths based on your Limio configuration.
* **Automatic state management.** When a subscriber selects a new offer, the system auto-generates the remove action for their current offer.
* **Cleaner add-on handling.** Incompatible add-ons are automatically flagged for removal when switching offers.

### Prerequisites

* Access to the Limio Commerce API with a valid Bearer token (for standard flow) or Partner token (for partner integrations)
* An active subscription
* (Optional) Upgrade/downgrade paths configured on your Offers in Limio

### What you'll build

1. **Initiate** a subscription checkout session by providing the subscription ID. The API returns available upgrades, downgrades, and cross-sells.
2. **Update** the basket by adding offers or add-ons. The system calculates prices and generates any required remove actions.
3. **Retrieve** the basket state at any point (e.g., after page refresh) to continue the checkout.

***

### Initiate the checkout session

Use `POST api/checkout/subscription` to create a basket for the subscriber. At minimum, provide:

* `order.forSubscription.id` — the subscription ID to modify
* `order.order_type` — must be `"update_subscription"`

#### Example — initiate checkout (curl)

```bash
curl -X POST 'https://your-tenant.limio.com/api/checkout/subscription' \
  -H 'Authorization: Bearer <YOUR_TOKEN>' \
  -H 'xLimioRecaptcha: <RECAPTCHA_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{
    "order": {
      "forSubscription": {
        "id": "sub_abc123"
      },
      "order_type": "update_subscription"
    }
  }'
```

**Notes:**

* `forSubscription` accepts either `id` or `name` to identify the subscription

#### Response

```json
{
  "order": {
    "checkoutId": "basket-8cf72b2a-eb57-462d-8e55-981c3b5e5364",
    "forSubscription": { "id": "sub_abc123", "name": "Premium Monthly" },
    "forSubscriptionOffer": { "offerId": "offer_premium_monthly" },
    "orderItems": [],
    "total": { "currency": "USD", "amount": 0 }
  },
  "nextActions": {
    "upgrades": [
      { "id": "offer_premium_annual", "name": "Premium Annual" }
    ],
    "downgrades": [
      { "id": "offer_basic_monthly", "name": "Basic Monthly" }
    ],
    "crossSells": [
      { "id": "addon_extra_storage", "name": "Extra Storage" }
    ],
    "subscriptionAddOns": []
  },
  "completed": false
}
```

The response tells you:

* `checkoutId` — the basket ID (also set in the `lmo_ls` landing-state cookie)
* `forSubscriptionOffer` — what the subscriber currently has
* `nextActions.upgrades` / `downgrades` — valid offer changes based on your Limio config
* `nextActions.crossSells` — available add-ons
* `nextActions.subscriptionAddOns` — add-ons they already own

#### Error responses

See [Error reference](#error-reference) below for the response body and the one status you can branch on.

***

### Update the basket with offers or add-ons

Use `PUT api/checkout/subscription` to add or remove items. The landing state `lmo_ls` cookie (set by POST) must be present.

#### Example — select an upgrade (curl)

```bash
curl -X PUT 'https://your-tenant.limio.com/api/checkout/subscription' \
  -H 'Authorization: Bearer <YOUR_TOKEN>' \
  -H 'xLimioRecaptcha: <RECAPTCHA_TOKEN>' \
  -H 'Cookie: lmo_ls=...' \
  -H 'Content-Type: application/json' \
  -d '{
    "order": {
      "forSubscription": { "id": "sub_abc123" },
      "order_type": "update_subscription",
      "orderItems": [
        {
          "orderItemActionType": "add",
          "type": "offer",
          "offerId": "offer_premium_annual"
        }
      ]
    }
  }'
```

#### What happens automatically

When you add a new subscription offer, the system:

1. **Generates a remove action** for the current offer (`forSubscriptionOffer`) if relevant
2. **Checks add-on compatibility** and generates remove actions for incompatible add-ons
3. **Calculates prices** and line items for all order items
4. **Recalculates cross-sells** based on the new offer selection

#### Response

```json
{
  "order": {
    "checkoutId": "basket-8cf72b2a-eb57-462d-8e55-981c3b5e5364",
    "orderItems": [
      {
        "id": "item-uuid-1",
        "orderItemActionType": "add",
        "type": "offer",
        "offerId": "offer_premium_annual",
        "price": { "amount": 99.0, "currency": "USD" }
      },
      {
        "id": "item-uuid-2",
        "orderItemActionType": "remove",
        "type": "offer",
        "offerId": "offer_premium_monthly"
      }
    ],
    "effectiveDate": "2025-01-15T00:00:00.000Z",
    "total": { "currency": "USD", "amount": 99.0 }
  },
  "nextActions": {
    "upgrades": [],
    "downgrades": [],
    "crossSells": []
  }
}
```

#### Adding multiple add-ons

Each PUT **replaces** `orderItems` entirely (upgrades/downgrades persist from POST, crossSells are recalculated). To add multiple add-ons, include them all in one request:

```bash
curl -X PUT 'https://your-tenant.limio.com/api/checkout/subscription' \
  -H 'Authorization: Bearer <YOUR_TOKEN>' \
  -H 'xLimioRecaptcha: <RECAPTCHA_TOKEN>' \
  -H 'Cookie: lmo_ls=...' \
  -H 'Content-Type: application/json' \
  -d '{
    "order": {
      "forSubscription": { "id": "sub_abc123" },
      "order_type": "update_subscription",
      "orderItems": [
        { "orderItemActionType": "add", "type": "addon", "offerId": "addon_storage" },
        { "orderItemActionType": "add", "type": "addon", "offerId": "addon_support" }
      ]
    }
  }'
```

#### Removing a subscription offer (Release 116)

To remove an offer from a multi-offer subscription without cancelling it, include one item per offer being removed, with `orderItemActionType: "remove"` and `type: "subscription_offer"`. The `offer.id` references the **subscription offer** — the offer instance on the subscription — not the catalog offer:

```bash
curl -X PUT 'https://your-tenant.limio.com/api/checkout/subscription' \
  -H 'Authorization: Bearer <YOUR_TOKEN>' \
  -H 'xLimioRecaptcha: <RECAPTCHA_TOKEN>' \
  -H 'Cookie: lmo_ls=...' \
  -H 'Content-Type: application/json' \
  -d '{
    "order": {
      "forSubscription": { "id": "sub_abc123" },
      "order_type": "update_subscription",
      "orderItems": [
        {
          "orderItemActionType": "remove",
          "type": "subscription_offer",
          "offer": { "id": "subscription_offer-xyz789" }
        }
      ]
    }
  }'
```

Batch **all** removals into the same request. Removals take effect at the end of the current term; if the same order also adds an offer (a swap), the effective date follows the added offer's upgrade or downgrade classification instead.

#### Validation rules

* **One offer add per request** — you can only add one subscription offer at a time (add-ons have no limit)
* **Basket ownership** — the basket must belong to the authenticated user/partner
* **At least one active offer must remain** — an order that would remove every offer on the subscription is rejected; an offer added in the same order offsets a removal (a swap)
* **Removals require an initiated basket** — the server resolves the subscription's current offer versions when the checkout session is initiated
* **Batch removals into a single order** — submitting an update order sets a pending change that locks further update checkouts until the effective date passes (the `423 subscription_locked` error described below)

***

### Retrieve the basket

Use `GET api/checkout/subscription` to fetch the current basket state. Useful after page refresh or to verify state before payment.

```bash
curl -X GET 'https://your-tenant.limio.com/api/checkout/subscription' \
  -H 'Authorization: Bearer <YOUR_TOKEN>' \
  -H 'Cookie: lmo_ls=...'
```

Response structure is identical to POST/PUT.

***

### Authentication modes

| Mode           | Endpoint                            | Auth Header                             |
| -------------- | ----------------------------------- | --------------------------------------- |
| Standard (MMA) | `api/checkout/subscription`         | `Authorization: Bearer <token>`         |
| Partner        | `api/partner/checkout/subscription` | `Authorization: Bearer <partner token>` |

Both modes use a bearer token in the `Authorization` header; they differ in the endpoint and in the token you present. Standard auth validates that the user owns the subscription. Partner auth is for admin and CRM integrations acting on behalf of customers.

***

### End-to-end example (Node.js)

Below is a minimal service that initiates a subscription upgrade flow and returns a checkout URL.

```javascript
import express from "express";

const app = express();
app.use(express.json());

const LIMIO_BASE = "https://your-tenant.limio.com";

app.post("/upgrade", async (req, res) => {
  const { subscriptionId, upgradeOfferId, token, recaptchaToken } = req.body;

  // 1) Initiate checkout session
  const initResp = await fetch(`${LIMIO_BASE}/api/checkout/subscription`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      xLimioRecaptcha: recaptchaToken,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      order: {
        forSubscription: { id: subscriptionId },
        order_type: "update_subscription",
      },
    }),
  });

  if (!initResp.ok) {
    return res.status(initResp.status).send(await initResp.text());
  }

  const initData = await initResp.json();
  const cookies = initResp.headers.get("set-cookie");

  // 2) Add upgrade offer to basket
  const updateResp = await fetch(`${LIMIO_BASE}/api/checkout/subscription`, {
    method: "PUT",
    headers: {
      Authorization: `Bearer ${token}`,
      xLimioRecaptcha: recaptchaToken,
      Cookie: cookies,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      order: {
        forSubscription: { id: subscriptionId },
        order_type: "update_subscription",
        orderItems: [
          { orderItemActionType: "add", type: "offer", offerId: upgradeOfferId },
        ],
      },
    }),
  });

  if (!updateResp.ok) {
    return res.status(updateResp.status).send(await updateResp.text());
  }

  const basket = await updateResp.json();

  // 3) Return checkout URL
  const checkoutUrl = `https://your-shop.limio.com/manage/checkout?basket=${basket.order.checkoutId}`;
  res.json({ basketId: basket.order.checkoutId, checkoutUrl, basket });
});

app.listen(3000);
```

***

### Error reference

Every error from this endpoint has the same body:

```json
{
  "requestId": "8f3c…",
  "code": "generic_error",
  "message": "An application error occurred"
}
```

There is no `error` field. Quote the `requestId` when contacting support — it is how we find your request in the logs.

**Only one failure is individually identifiable.** When the subscription has a pending change scheduled, you get a `423` with a specific code:

```json
{
  "requestId": "8f3c…",
  "code": "subscription_locked",
  "message": "This subscription has pending changes and cannot be modified"
}
```

Branch on `code === "subscription_locked"` (or the `423` status) and tell the subscriber to try again after `pendingChangeDate`.

{% hint style="warning" %}
Every other failure — subscription not found, more than one offer added, missing basket, invalid body — currently returns **`500`** with the generic `generic_error` body above. You cannot yet distinguish them programmatically, so do not write logic that branches on a `400` or `404` from this endpoint. Treat any non-`423` failure as retryable-once, then surface a generic message.
{% endhint %}

***

### Security & best practices

* **Send reCAPTCHA on `POST` and `PUT`.** Enforcement is controlled by the `analytics.recaptcha_on_landing_page` tenant setting, so it is not rejected when disabled — but send it, because enabling that setting must not break your integration.
* **Never trust client-side pricing or eligibility.** The API is the authority on both; recompute server-side.
* **Pass cookies through.** `GET` and `PUT` need the `lmo_ls` landing-state cookie set by the initial `POST`.
* **Use `external_id` to tie a basket to your CRM or analytics.** Maximum 128 characters, and only letters, digits, `-` and `_`. A value outside that is rejected.
* **Baskets last 14 days**, and on this endpoint that is fixed. (`expiresAfter` is configurable on `/api/checkout/initiate`, but it is *not* read here — passing it does nothing.)

***

### Related resources

* [Basket (Cart)](https://docs.limio.com/developers/limio-sdk/basket) — front-end basket integration with the Limio SDK
* [Authentication](https://docs.limio.com/developers/api/authentication-overview) — obtaining and refreshing API tokens

When contacting support about a failed request, quote the `requestId` from the error response.

***

### Tips & troubleshooting

**Why am I getting a 423 error?** The subscription has a scheduled change (e.g., a pending upgrade that takes effect next billing cycle). You can't modify it until that date passes. Check `subscription.data.pendingChangeDate`.

**Can I add multiple offers at once?** No — only one subscription offer can be added per request. The system enforces a 1:1 swap (add new, remove current). Add-ons don't have this restriction.

**Do I need to send remove actions manually?** No — when you add a new subscription offer, the system auto-generates the remove action for the current offer and any incompatible add-ons.

**How long do baskets last?** 14 days, fixed on this endpoint. The `lmo_ls` cookie tracks the active basket.

**What's the difference between standard and partner auth?** Standard auth (`BEARER_MMA`) validates the user owns the subscription. Partner auth should be used for admin/CRM integrations where you're acting on behalf of customers. Both present a bearer token in the `Authorization` header.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.limio.com/guides/developer-guides/guide-manage-subscription-updates.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
