> ## Documentation Index
> Fetch the complete documentation index at: https://developers.appstle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Appstle Subscriptions API changelog

> Dated changes to the Appstle Subscriptions Admin API, Storefront API, Shopify Flow triggers and actions, and JavaScript events: new endpoints, fields and values, and what to do about each.

This page lists developer-facing changes to the Subscriptions Admin API, the Storefront API, Shopify Flow and the storefront JavaScript events, newest first. Each entry is dated the day the change reached production.

* **Additive changes** (new endpoints, new response fields, new optional parameters, new values of an existing field) don't break a client that ignores what it doesn't know. Parse JSON leniently and don't treat enum fields as closed lists.
* Entries tagged **Action required** may need a code change in your integration. Each one says who is affected and what to do.
* The API reference is generated from the live API, so it always shows the current contract. The Admin API base URL is `https://subscription-admin.appstle.com`, and Storefront API paths start with `/subscriptions/cp/api/`.

**November 2, 2026**

Pagination links

## Pagination `Link` URLs start with `https://`

`GET /api/external/v2/subscription-contract-details`, `GET /api/external/v2/subscription-contract-details/customers`, `GET /api/external/v2/subscription-billing-attempts/past-orders`, `GET /api/external/v2/subscription-billing-attempts/past-orders/report`, `GET /api/external/v2/activity-logs` and `GET /api/external/v2/customer-retention-activities` return a `Link` header with the URLs of the first and last pages, and of the next and previous pages when they exist. Until now, those URLs started with `http://`, even when you called `https://subscription-admin.appstle.com`. Following one sent the request over plain HTTP and got a `301` redirect to the same URL on `https://`. They now start with `https://`.

**Who is affected:** clients that follow `Link` URLs as they are. Their requests now go straight to HTTPS, without the redirect. If your code rewrites `http://` to `https://` before it follows a `Link` URL, you can drop that step; keeping it does no harm.

Reference: [Retrieve subscription contracts](https://developers.appstle.com/subscription-admin-api/subscription-contracts/retrieve-subscription-contracts.md), [Retrieve customers with subscriptions](https://developers.appstle.com/subscription-admin-api/customers/retrieve-customers-with-subscriptions.md), [Get past orders](https://developers.appstle.com/subscription-admin-api/billing-&-payments/get-past-orders.md), [Get past orders report with detailed filtering](https://developers.appstle.com/subscription-admin-api/billing-&-payments/get-past-orders-report-with-detailed-filtering.md), [Get/Search activity logs](https://developers.appstle.com/subscription-admin-api/operations-&-settings/getsearch-activity-logs.md), [Get customer retention activities](https://developers.appstle.com/subscription-admin-api/customer-retention/get-customer-retention-activities.md).

**October 29, 2026**

Customer portal settings

## `preservePortalOrderNote`

`GET /subscriptions/cp/api/shop-infos-by-current-login` returns `preservePortalOrderNote` (boolean).

Reference: [Get shop info for the currently signed-in shop](https://developers.appstle.com/subscription-storefront-api/customer-portal/get-shop-info-for-the-currently-signed-in-shop.md).

**October 28, 2026**

Customer portal settings, free product rewards

## Pickup and delivery visibility in portal settings, and no gift cards as free products

* `GET /api/external/v2/customer-portal-settings/{id}` and `GET /subscriptions/cp/api/customer-portal-settings/{id}` return three new booleans: `apdHideDelivery`, `apdHidePickup` and `apdHideShipping`.
* `POST /api/external/v2/subscription-groups`, and `PUT` for new plans, return `400` when a free product in `appstleCycles[]` is a gift card. The title is `UserGeneratedError:` followed by the product's name and `is a gift card. Shopify doesn't allow free gift cards on subscription orders, so choose a different free product.`

Reference: [Get customer portal settings](https://developers.appstle.com/subscription-admin-api/customization/get-customer-portal-settings.md).

**October 27, 2026**

Campaigns

## Upgrade selection when redeeming a campaign offer

`POST /subscriptions/cp/api/campaigns/{id}/redeem` accepts two new optional body fields, `upgradeLineId` (string) and `upgradeSelection` (object).

Reference: [Redeem a campaign offer](https://developers.appstle.com/subscription-storefront-api/campaigns/redeem-a-campaign-offer.md).

**October 26, 2026**

Selling plans

## `repeatEvery` on cycle rewards, and plan name translations

* Each entry of `appstleCycles[]` carries a new field, `repeatEvery` (integer). It applies to repeating rewards only: they come back every this many orders, counting the first order. A free gift on order 5 (`afterCycle` 4) with `repeatEvery` 5 is given on orders 5, 10, 15 and so on. When it's absent, repeating rewards come back every N orders, where N is the highest `afterCycle` among them. It's ignored on pay-as-you-go prepaid plans.
* `repeatEvery` is in the responses of `GET /api/external/v2/subscription-groups` (and its `{id}` variant), `GET /api/external/v2/subscription-groups/all-selling-plans`, `GET /api/external/v2/subscription-contract-details/billing-interval`, `GET /subscriptions/cp/api/data/v2/products-selling-plans` and `GET /subscriptions/cp/api/subscription-groups/all-selling-plans`. It's accepted in `POST /api/external/v2/subscription-groups`, in new plans (without `idNew`) in `PUT /api/external/v2/subscription-groups`, and in `lines[].pricingPolicy[]` of `POST /api/external/v2/subscription-contract-details/create-subscription-contract`. `PUT` ignores `appstleCycles[]` on plans that already exist.
* On the subscription group calls, a `repeatEvery` that isn't a whole number from 1 to 150 returns `400` with the title `UserGeneratedError:Repeat every has to be a whole number of orders from 1 to 150.` Repeating rewards in one plan share one `repeatEvery`, so different values return `400` with the title `UserGeneratedError:The repeating rewards repeat together, so they need the same repeat every. Reload the page and try again.` A value that would start two repeating discount or shipping rewards on the same order returns `400` with a title that starts `UserGeneratedError:With repeat every`.
* `GET /subscriptions/cp/api/data/v2/products-selling-plans` returns `frequencyNameTranslations` for plans whose translations are saved by plan ID. Until now it looked translations up by plan name only, so those plans came back without them. Plans whose translations are saved by name still return theirs.

Reference: [Get all selling plans across all subscription groups](https://developers.appstle.com/subscription-admin-api/subscription-plans/get-all-selling-plans-across-all-subscription-groups.md).

**October 24, 2026**

Selling plans, product swaps

## `singleOrder` on cycle rewards, and `stopSwapEmails` on swaps

* Each entry of `appstleCycles[]` carries a new field, `singleOrder` (boolean). It applies to `SHIPPING` rewards only: the shipping price covers that reward's own order, then the subscription goes back to its normal shipping price. It's ignored on other reward types and on repeating rewards. The endpoints that return and accept `repeatEvery`, listed under October 26, return and accept it too.
* `POST /api/external/v2/subscription-contract-details/replace-variants-v3` now honors `stopSwapEmails`. With `true`, the swap sends no email notifications. Until now the endpoint ignored the field and always sent them.

Reference: [Get all selling plans across all subscription groups](https://developers.appstle.com/subscription-admin-api/subscription-plans/get-all-selling-plans-across-all-subscription-groups.md), [Replace product variants in a subscription contract](https://developers.appstle.com/subscription-admin-api/subscription-management/replace-product-variants-in-a-subscription-contract.md).

**October 23, 2026**

Customer portal settings

## `upsellOrderForAllProducts`

`GET /api/external/v2/customer-portal-settings/{id}` and `GET /subscriptions/cp/api/customer-portal-settings/{id}` return `upsellOrderForAllProducts` (boolean).

Reference: [Get customer portal settings](https://developers.appstle.com/subscription-admin-api/customization/get-customer-portal-settings.md).

**October 22, 2026**

Cancellation flows

## Free Gift offers in cancellation flows

* A cancellation flow can now offer a **Free Gift**. `GET /subscriptions/cp/api/cancellation-flows/published?contractId={contractId}` returns the flow in `configJSON`, a JSON string. In it, a `TREATMENT_SET` step's `payload`, or that payload's `fallback`, can have `action` `FREE_GIFT`, with `freeGiftVariantId` and, when the merchant set one, `freeGiftTitle`. Before this date, a flow with a Free Gift offer was rejected when the merchant saved it, so no flow returned one.
* When the reason chosen through `flowId` and `flowReasonId` carries a Free Gift offer, `POST /subscriptions/cp/api/cancellation-retention-offers/execute` runs it the way it does for a reason-based offer (see October 16): `retentionAction` `9`, the gift variant added to the next order as a free one-time item, once per subscription.

**Who is affected:** storefront code that renders a published cancellation flow and maps `action` to a fixed list of values.

**Action required:** accept `action` values you don't recognize.

Reference: [Execute a configured cancellation retention offer and record the retention activity](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/execute-a-configured-cancellation-retention-offer-and-record-the-retention-activity.md).

**October 21, 2026**

Subscription Cancelled trigger

## Cancellation note in the Subscription Cancelled trigger

The **Subscription Cancelled** trigger has a new field, **Cancellation Note** (string). It holds the note the customer or merchant wrote when the subscription was cancelled, or an empty string when there's none. Only this trigger has it. Use it in a condition, or pass it to an action, for example to send the note to your support tool.

Reference: [Subscription Cancelled properties](https://developers.appstle.com/subscription/shopify-flow.md#subscription-cancelled-properties).

**October 19, 2026**

Activity logs, shipping countries

## Activity logs accept the `X-API-Key` header, and the countries list covers every shipping zone

* `GET /api/external/v2/activity-logs` now accepts the API key in the `X-API-Key` header, like the other Admin API endpoints. Until now, a request that sent the key only in the header returned `400`, because the `api_key` query parameter was required. `api_key` still works but is deprecated.
* On a store that doesn't use Shopify Markets shipping, `GET /subscriptions/cp/api/v2/data/countries` now returns the countries of every shipping zone in the store's delivery profiles. Until now, countries in shipping zones past the first 50 were missing from the list.

Reference: [Search activity logs](https://developers.appstle.com/subscription-admin-api/operations-&-settings/getsearch-activity-logs.md), [List supported shipping countries](https://developers.appstle.com/subscription-storefront-api/subscription-data/list-supported-shipping-countries.md).

**October 17, 2026**

Bundle settings, customer portal settings

## Bundle display settings and freeze setting

* `GET /api/external/v2/subscription-bundle-settings/{id}` and `GET /subscriptions/cp/api/subscription-bundle-settings/{id}` return `hideProductTags` and `showFullProductDescription` (booleans).
* Customer portal settings return `allowAddProductWhenContractIsFreeze` (boolean). When it's `true`, `PUT /subscriptions/cp/api/v2/subscription-contracts-add-line-item` and `PUT /subscriptions/cp/api/v2/subscription-contracts-add-bundle-line-item` add products even while the store locks changes until a subscription reaches its minimum number of orders.

Reference: [Get subscription bundle settings by ID](https://developers.appstle.com/subscription-storefront-api/build-a-box-&-bundles/get-subscription-bundle-settings-by-id.md).

**October 16, 2026**

Build-a-Box events, Free Gift offers

## Build-a-Box discount cleanup event, and a Free Gift retention offer

* The storefront script dispatches `AppstleSubscription:Cart:BuildABoxDiscountRemoved` on `document` only, not on `window`, when it removes a Build-a-Box discount code from a cart that has no box items. `event.detail.codes` lists the removed codes.
* `GET /api/external/v2/cancellation-managements/{id}` and `GET /subscriptions/cp/api/cancellation-managements/{id}` can return a cancellation reason, or a reason's `fallbackOffer`, with `cancellationAction` `9` (Free Gift), `freeGiftVariantId` and, when the merchant set one, `freeGiftTitle`. The reasons are in `cancellationReasonsJSON`, a JSON string.
* `POST /subscriptions/cp/api/cancellation-retention-offers/execute` can run a **Free Gift** offer. The response then carries `retentionAction` `9` and `retentionActionLabel` `Free Gift`, and `retentionActivity.additionalInfo` (a JSON string) holds `freeGiftVariantId` and, when the merchant set one, `freeGiftTitle`. The call adds the merchant's gift variant to the subscription's next order as a free one-time item. A subscription gets the gift once.

**Who is affected:** clients that map `retentionAction` or `cancellationAction` to a fixed list of values, and storefront code that runs cancellation offers.

**Action required:** accept `retentionAction` and `cancellationAction` values you don't recognize. When a Free Gift offer returns `400`, show the title to the customer:

* `UserGeneratedError:A free gift has already been added to this subscription.` The subscription already got its gift.
* `UserGeneratedError:Your free gift is already being added. Please wait a moment.` An earlier request is still running.
* `UserGeneratedError:Another billing operation is already in progress. Please wait until all ongoing processes are completed.` An order is being placed. Try again once it's done.
* `UserGeneratedError:This free gift offer has no gift product.` The merchant hasn't set a gift product.
* `UserGeneratedError:` followed by the merchant's own message, when the store locks changes until a subscription reaches a minimum number of orders.

Reference: [JavaScript events](https://developers.appstle.com/subscription/javascript-hooks.md), [Get cancellation management configuration](https://developers.appstle.com/subscription-admin-api/operations-&-settings/get-cancellation-management-configuration.md), [Execute a configured cancellation retention offer and record the retention activity](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/execute-a-configured-cancellation-retention-offer-and-record-the-retention-activity.md).

**October 15, 2026**

Delivery price

## Keep a custom delivery price for one subscription

New `PUT /api/external/v2/subscription-contracts-update-delivery-price-override` takes `contractId` and `allowDeliveryPriceOverride` as query parameters. `true` keeps the contract's current delivery price and stops automatic shipping price syncs for it: pair it with `PUT /api/external/v2/subscription-contracts-update-delivery-price` to keep a price you set. `false` syncs the delivery price with the store's Shopify shipping settings again and recalculates it in the background.

Reference: [Turn custom delivery price on or off for a subscription contract](https://developers.appstle.com/subscription-admin-api/subscription-management/turn-custom-delivery-price-on-or-off-for-a-subscription-contract.md).

**October 14, 2026**

Customer portal settings

## `settingsVersion` and `expectedShopCustomizationData`

Customer portal settings return `settingsVersion` (string) and `expectedShopCustomizationData[]` (`id`, `labelId`, `value`).

Reference: [Get customer portal settings](https://developers.appstle.com/subscription-admin-api/customization/get-customer-portal-settings.md).

**October 12, 2026**

Gift subscriptions, immediate billing

## Gift subscriptions and schedule modes for immediate orders

* Selling plans return `giftSendMaxDays` (integer), and `POST` and `PUT /api/external/v2/subscription-groups` accept it in `subscriptionPlans[]`.
* New `GET /subscriptions/cp/api/subscription-gifts?contractIds=…` returns the gift details of the listed contracts that belong to the signed-in customer, keyed by contract ID. Without `contractIds`, it returns an empty object.
* `eventType` in `GET /api/external/v2/activity-logs` has a new value, `SEND_GIFT_PURCHASED_MERCHANT_EMAIL`.
* `PUT /subscriptions/cp/api/subscription-billing-attempts/attempt-billing/{id}` accepts an optional `scheduleMode` query parameter that sets how ordering now affects the schedule. `extra_order_only` places an extra order and leaves the upcoming orders as they are. `extra_order_recalculate` places an extra order and recalculates the upcoming order dates. Without it, the store's Manual Order Processing setting applies.
* Send `scheduleMode` only when customer portal settings return `allowOrderNowScheduleChoice` as `true`. Otherwise the call returns `400` with the title `UserGeneratedError:This schedule option is no longer available. Please refresh the page and try again.` An unrecognized value returns `400` with the title `UserGeneratedError:Invalid schedule option. Please refresh the page and try again.`
* Customer portal settings return `allowOrderNowScheduleChoice` and `hideRescheduleTimeInExtension` (booleans).
* `POST` and `PUT /api/external/v2/subscription-groups` return `400` when a plan has `gift` `true` and a `giftSendMaxDays` outside 1 to 365, with the title `UserGeneratedError:Gift invitations can be scheduled between 1 and 365 days ahead.` and the `errorKey` `invalidGiftSendMaxDays`, or a `minQuantity` above `1`, with the title `UserGeneratedError:Gifts must be purchased one at a time. Leave minimum quantity empty or set it to 1.` and the `errorKey` `invalidGiftMinimumQuantity`.
* `PUT /api/external/v2/subscription-contracts-update-max-cycles` returns `400` with the title `UserGeneratedError:Gift subscriptions end after their prepaid deliveries, so their maximum can't be changed.` and the `errorKey` `giftMaxCyclesChangeBlocked` when the subscription is a gift subscription. The Shopify Flow action **Update Max Cycles** fails on gift subscriptions for the same reason.

**Who is affected:** clients that map `eventType` to a fixed list of values.

**Action required:** accept values you don't recognize.

Reference: [Get subscription gifts](https://developers.appstle.com/subscription-storefront-api/subscription-gift-resource/get-subscriptionscpapisubscription-gifts.md), [Create a new subscription group](https://developers.appstle.com/subscription-admin-api/subscription-plans/create-a-new-subscription-group-selling-plan-group.md), [Update subscription group details](https://developers.appstle.com/subscription-admin-api/subscription-plans/update-subscription-group-details-and-product-assignments.md), [Update maximum cycles for a subscription contract](https://developers.appstle.com/subscription-admin-api/subscription-management/update-maximum-cycles-for-a-subscription-contract.md).

**October 11, 2026**

Activity logs, gift subscriptions, charge cap

## Gift email event types and gift selling plans

* `eventType` in `GET /api/external/v2/activity-logs` has two new values: `SEND_GIFT_RECEIVED_EMAIL` and `SEND_GIFT_THANK_YOU_EMAIL`.
* Selling plans return `gift` (boolean), and `POST` and `PUT /api/external/v2/subscription-groups` accept it in `subscriptionPlans[]`.
* `GET /subscriptions/cp/api/data/v2/products-selling-plans` accepts a `contractId` query parameter.
* `POST /api/external/v2/subscription-groups`, and `PUT` for new plans, return `400` with the title `UserGeneratedError:Remove rewards before selling this plan as a gift.` and the `errorKey` `giftRewardsNotSupported` when a plan has `gift` `true` and rewards in `appstleCycles[]`.
* The same calls return `400` with the title `UserGeneratedError:Each new gift option needs its own name.` and the `errorKey` `giftFrequencyNameNotUnique` when a new plan with `gift` `true` has the same `frequencyName` as another plan in the request, ignoring case, and with the title `UserGeneratedError:Gift plans must use Prepaid One-Time with one billing cycle.` and the `errorKey` `invalidGiftPlan` when a plan with `gift` `true` has a `planType` other than `PREPAID` or a `maxCycles` other than `1`.

**Who is affected:** clients that map `eventType` to a fixed list of values.

**Action required:** accept values you don't recognize.

## The charge cap's 500 USD limit is converted into the subscription's currency

* The [charge cap](https://developers.appstle.com/subscription/integration-guide.md#product-management) limits a subscription that has no original order amount on file to 500 USD. That limit is now converted into the subscription's currency. Until now, the number 500 was compared with the subscription's amount in whatever currency it was in.
* The same limit applies when you raise the price of a line item that costs 0, with `PUT /api/external/v2/subscription-contracts-update-line-item-price` or `PUT /api/external/v2/subscription-contracts-update-line-item-pricing-policy`. It also applies when you trigger billing with `PUT /api/external/v2/subscription-billing-attempts/attempt-billing/{id}` for a subscription with no original order amount on file.
* The `400` messages for this limit now show it with its currency code, for example `500.00 USD`. Until now they showed only `500`.
* When Appstle has no exchange rate for the subscription's currency, these calls return `400` with the title `UserGeneratedError:Unable to verify this subscription's safety limit. Please try again or contact Appstle support.` and the `errorKey` `charge_cap_currency_unavailable`.

**Who is affected:** Admin API clients that change products, line item prices or billing on subscriptions in a currency other than USD.

**Action required:** after a `charge_cap_currency_unavailable` error, send the request again later. If it keeps failing, contact [support@appstle.com](mailto:support@appstle.com).

Reference: [Trigger immediate billing for an order](https://developers.appstle.com/subscription-admin-api/billing-&-payments/trigger-immediate-billing-for-an-order.md).

**October 10, 2026**

Cancellation, pausing

## Cancellation rules apply during free trials, and pauses have a limit

`DELETE /subscriptions/cp/api/subscription-contracts/{id}` now applies the store's customer cancellation rules to subscriptions in a free trial too. Until now, a subscription in a free trial skipped them.

* If the merchant doesn't let customers cancel, the call returns `400` with the title `UserGeneratedError:Permission denied, Subscription cannot be modified`.
* If the merchant limits cancellation by how close the next billing date is, the call returns `400` with the `errorKey` `timeBasedRestriction`. Its title is `UserGeneratedError:` followed by a message that can be the merchant's own text, so match the `errorKey`, not the title.

`PUT /subscriptions/cp/api/subscription-contracts-update-status` with `status` `PAUSED` and a `pauseDurationCycle` now works out the date the pause ends from the subscription's billing schedule. It returns `400` with the title `Scheduled pauses must end within 364 days. Please choose a shorter pause duration.` and the `errorKey` `pauseDurationTooLong` when that date is more than 364 days away. It returns `400` with the `errorKey` `invalidPauseSchedule` when the date can't be worked out or isn't in the future.

Until now, a `pauseDurationCycle` at least as large as the number of upcoming orders paused the subscription with no end date. A pause offer run through `POST /subscriptions/cp/api/cancellation-retention-offers/execute` follows both rules, and the free trial rule, with the same `400`s.

`PUT /subscriptions/cp/api/subscription-contracts-update-status` with `status` `PAUSED` or `CANCELLED` also returns `400` while the subscription is in a free trial and the customer portal settings have `allowCancelDuringFreeTrial` set to `false`. The title is `UserGeneratedError:Pausing is not allowed during free trial` or `UserGeneratedError:Cancellation is not allowed during free trial`, and the `errorKey` is `pause_not_allowed_during_trial` or `cancel_not_allowed_during_trial`. When the call can't tell whether the subscription is in a free trial, it returns `400` with the title `UserGeneratedError:Unable to verify the subscription's free trial. Please try again.` and the `errorKey` `free_trial_status_unavailable`.

**Who is affected:** storefront code that lets customers cancel or pause a subscription.

**Action required:** handle a `400` from this call for subscriptions in a free trial, as you already do for other subscriptions. Don't offer a pause that ends more than 364 days from now. Read `allowCancelDuringFreeTrial` in the customer portal settings, and don't offer pause or cancel during a free trial when it's `false`. Show the `400` title when a call is refused.

Reference: [Cancel and delete a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/cancel-and-delete-a-subscription-contract.md), [Update subscription contract status](https://developers.appstle.com/subscription-storefront-api/subscription-management/update-subscription-contract-status.md).

**October 9, 2026**

Customer portal settings

## `allowOrderNowOnListPage`

Customer portal settings return `allowOrderNowOnListPage` (boolean).

**October 8, 2026**

Live order status, rescheduling

## Pickup data on live order status, and a billing-date guard

* `GET /subscriptions/cp/api/orders/{orderId}/live-status` accepts `includePickupDeliveryData` (boolean) and then returns `pickupDeliveryData` (string).
* `PUT /subscriptions/cp/api/subscription-billing-attempts/skip-order/{id}` and `PUT /subscriptions/cp/api/subscription-contracts-update-billing-date` accept an optional `expectedBillingDate` query parameter, and `update-billing-date` also accepts `expectedBillingAttemptId`. Send the upcoming order's `billingDate` exactly as the API returned it. For `update-billing-date`, also send that order's billing attempt `id`. If the upcoming order has changed since, the call changes nothing and returns `400` with the title `UserGeneratedError:This upcoming order has changed. Review the latest order before trying again.`

Reference: [Get the live fulfillment status for an order](https://developers.appstle.com/subscription-storefront-api/delivery-&-shipping/get-the-live-fulfillment-status-for-an-order.md).

**October 7, 2026**

Customer portal settings

## `showSwapOnlyForOutOfStockBABProducts`

Customer portal settings return `showSwapOnlyForOutOfStockBABProducts` (boolean).

**October 5, 2026**

Minimum quantity

## Customers can't go below a plan's minimum quantity

When a customer lowers a line's quantity with `PUT /subscriptions/cp/api/subscription-contracts-update-line-item-quantity`, or swaps a line with `POST /subscriptions/cp/api/subscription-contract-details/replace-variants-v2` or `replace-variants-v3`, the call returns `400` with the title `UserGeneratedError:This subscription plan requires at least {minQuantity} of this product.` and the `errorKey` `minQuantityNotReached` if the new quantity is below the selling plan's `minQuantity`. A line already below the minimum can stay at its current quantity. Admin API calls aren't affected.

**Who is affected:** storefront code that lets customers change quantities or swap products.

**Action required:** don't offer quantities below the plan's `minQuantity`, and show the `400` title when a call is refused.

Reference: [Update line item quantity on a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-line-item-quantity-on-a-subscription-contract.md), [Replace contract variants in bulk (v2)](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/replace-contract-variants-in-bulk-v2.md), [Replace product variants in a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-management/replace-product-variants-in-a-subscription-contract.md).

**October 4, 2026**

Delivery address

## Refused delivery addresses say what's wrong

When Shopify refuses the address itself (its message starts with `Delivery method` or `Validation failed`), `PUT /api/external/v2/subscription-contracts-update-shipping-address` and `PUT /subscriptions/cp/api/subscription-contracts-update-shipping-address` now return `400` with the `errorKey` `addressUpdateFailed` and a title that names the problem. For a missing or invalid phone number, ZIP or postal code, city, last name or state, the title is Appstle's wording, for example `UserGeneratedError:Add a phone number to this address. This store requires one for shipping.` or `UserGeneratedError:Enter a ZIP / postal code.` For other address problems, it's `UserGeneratedError:` followed by Shopify's message. Every other refusal keeps the generic title described under September 17.

**Who is affected:** code that shows or matches on the title of a refused address update.

**Action required:** show the title after `UserGeneratedError:` and don't match on its wording.

Reference: [Update subscription delivery address and method](https://developers.appstle.com/subscription-admin-api/subscription-management/update-subscription-delivery-address-and-method.md), [Update subscription delivery address and method (Storefront API)](https://developers.appstle.com/subscription-storefront-api/subscription-management/update-subscription-delivery-address-and-method.md).

**October 3, 2026**

Cancellation offers, order notes, line items, partner approvals

## Skip offers, order notes and line item edits follow the store's settings

* `POST /subscriptions/cp/api/cancellation-retention-offers/execute` skips the number of orders the merchant set on the cancellation reason when it runs a skip offer. `skipCycle` in the request counts only when the shop's cancellation management setting `dynamicallyPauseCycleOnCancellation` is on and the request has no `flowId` or `flowReasonId`. It must then be between `1` and the configured number, or the call returns `400` with the title `UserGeneratedError:Invalid skip duration for this retention offer.` Otherwise the call ignores `skipCycle`. Until now, any `skipCycle` replaced the configured number.
* `PUT /subscriptions/cp/api/subscription-contract-details` returns `400` with the title `UserGeneratedError:Permission denied, Order notes are not allowed` when a customer changes the order note and the merchant has turned order notes off for the customer portal. Until now, the call saved the note.
* `PUT /subscriptions/cp/api/subscription-contracts-update-line-item-attributes` and `PUT /subscriptions/cp/api/subscription-contracts-edit-line-item-attributes` keep the attributes Appstle sets on a line, such as `_appstle-bb-id` and `_min-cycles`. The call ignores those keys in the request, except a Build-a-Box line's product selection, and keeps the line's existing values. Changing any other attribute needs the merchant's permission to edit attributes, or the call returns `400` with the title `UserGeneratedError:Permission denied, Editing attributes is not allowed`. A request that repeats a key returns `400` with the title `UserGeneratedError:Duplicate custom attribute key.`
* `PUT /subscriptions/cp/api/update-classic-bab-subscription/{contractId}` checks each line it changes. It returns `400` with the title `UserGeneratedError:This line is not a Build-A-Box product.` for a line outside the box, `UserGeneratedError:Permission denied, Editing or removing this product is not allowed` for a product the merchant has locked, and `UserGeneratedError:Permission denied, Removing products from subscription is not allowed` for a quantity of `0` when customers can't remove products. A line that must stay for a minimum number of orders returns `400` until it has, with a title that is `UserGeneratedError:` followed by the merchant's message.
* When a customer swaps the product on a Build-a-Box line, the new line keeps all of the old line's attributes, so it stays in the box. Until now it kept only Appstle's own attributes unless the store keeps line attributes on swaps.

**Who is affected:** clients that send `skipCycle` to a skip offer, and customer portals that let customers edit the order note, line item attributes or Build-a-Box quantities.

**Action required:** send a `skipCycle` no larger than the number the offer shows, send only your own attribute keys, and show the `400` title to the customer when a call is refused.

Reference: [Execute a configured cancellation retention offer and record the retention activity](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/execute-a-configured-cancellation-retention-offer-and-record-the-retention-activity.md), [Update subscription contract details](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-subscription-contract-details.md), [Edit custom attributes on a single line item](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/edit-custom-attributes-on-a-single-line-item.md), [Update line item quantities for a classic Build-a-Box subscription](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-line-item-quantities-for-a-classic-build-a-box-subscription.md).

## Partner approval callbacks and nonce expiry

* When a merchant approves a connection and Appstle sends the partner an `apst_` access token, Appstle now saves the token before it posts it to the partner's approval callback, `/appstle/approved` by default. A call made with the token as soon as it arrives no longer returns `401`.
* Any `2xx` reply from the callback now counts as received, whatever its body. Until now, a `2xx` reply whose body wasn't a JSON object failed the approval.
* When the callback doesn't answer with a `2xx` status within 5 seconds, the approval fails: Appstle deletes the token it created, leaves the connection pending, and returns `400` with the `errorKey` `PARTNER_UNREACHABLE` to the merchant. The partner gets no usable token from that attempt. See [What if the callback fails?](https://developers.appstle.com/subscription/partner-integration.md#what-if-the-callback-fails).
* `POST /api/partner/{partnerId}/verify` now returns `{ "verified": false }` for a `callback_nonce` that is more than 5 minutes old, as the partner guide already said. Until now, an expired nonce could still be verified.

**Who is affected:** partner apps that receive the approval callback or call the verify endpoint.

**Action required:** make the approval callback answer with a `2xx` status within 5 seconds, call verify within 5 minutes of receiving the nonce, and ask the merchant to approve again after a failed approval.

**October 2, 2026**

Cancellation offers, merging subscriptions

## Offer tier on cancellation discounts, and no merging of cancelled subscriptions

* `PUT /subscriptions/cp/api/subscription-contracts-cancellation-discount` accepts an optional `offerTier` query parameter, `PRIMARY` or `FALLBACK`, that records which cancellation offer the customer accepted. It's saved in the retention activity's `additionalInfo.offerTier`.
* `POST /subscriptions/cp/api/subscription-contract-details/merge-contract` returns `400` with the title `UserGeneratedError:Cancelled subscriptions cannot be merged. Please refresh the page and try again.` when either subscription is cancelled. Until now the call went ahead.

Reference: [Merge an existing contract into another](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/merge-an-existing-contract-into-another.md).

**October 1, 2026**

Authentication, free products

## Legacy API key revocation, and free products kept at quantity 1

* Under **More → API Access** in the Appstle admin, the **Legacy API Key** now has a **Revoke** button. After a merchant revokes it, every request that sends that key returns `401`, with the same body as any other wrong key (see [Authentication errors](https://developers.appstle.com/subscription/authentication.md#authentication-errors)). Appstle doesn't issue a replacement. Keys that start with `apst_` aren't affected.
* Each `appstleCycles[]` entry returns `restrictFreeProductQuantity` (boolean). When `true`, each free product stays at quantity 1 regardless of the parent line item's quantity. It's returned and accepted wherever `repeatEvery` is (see October 26).

**Who is affected:** integrations that still send the key shown on the **Legacy API Key** card.

**Action required:** create an `apst_` key under **More → API Access** and move your integration to it before the merchant revokes the legacy key.

Reference: [Managing keys](https://developers.appstle.com/subscription/authentication.md#managing-keys).

**September 28, 2026**

Subscription groups

## Plans need their own names, and rewards sent with new plans are saved

* `POST /api/external/v2/subscription-groups` and `PUT /api/external/v2/subscription-groups` return `400` with the title `UserGeneratedError:Each subscription option needs its own name. Rename one of the duplicate options.` when two plans in the request have the same `frequencyName`. The comparison ignores spaces and letter case. On `PUT`, the call refuses a duplicate only when at least one of the two plans is new, meaning it has no `idNew`. Until now, both calls saved duplicate names.
* `appstleCycles[]` sent with a plan in `POST /api/external/v2/subscription-groups`, or with a new plan in `PUT /api/external/v2/subscription-groups`, is checked and saved as that plan's rewards. Until now both calls dropped it. `PUT` still ignores `appstleCycles[]` on plans that already exist. A reward the store can't use returns `400`, for example `UserGeneratedError:Your current plan doesn't include subscription rewards. Upgrade your plan to add or change rewards.` or `UserGeneratedError:Select the free product for every free-gift reward before saving.`

**Who is affected:** integrations that create subscription groups or add plans with names that repeat, or with `appstleCycles[]` in the body, for example by sending back a group they read.

**Action required:** give every plan in a group its own `frequencyName`, and send `appstleCycles[]` only with the rewards you want a new plan to have. Show the `400` title when a call is refused.

Reference: [Create a new subscription group (selling plan group)](https://developers.appstle.com/subscription-admin-api/subscription-plans/create-a-new-subscription-group-selling-plan-group.md), [Update subscription group details and product assignments](https://developers.appstle.com/subscription-admin-api/subscription-plans/update-subscription-group-details-and-product-assignments.md).

**September 26, 2026**

Immediate billing, locations

## Trigger immediate billing returns an acknowledgement

* `PUT /api/external/v2/subscription-billing-attempts/attempt-billing/{id}` used to return `200` with an empty body. It now returns `200` with `billingAttemptId`, `requestedBillingAttemptId`, `contractId`, `billingDate`, `status` and `message`. This confirms the attempt was queued, not that the payment succeeded: the charge is sent to Shopify right after the call, and the result comes later.
* `billingAttemptId` is the attempt that was queued, and it can differ from the `id` you sent (returned as `requestedBillingAttemptId`). `status` is the attempt's status when it was queued, for example `IMMEDIATE_TRIGGERED`.
* When no billing attempt could be triggered, the call returns `400` with the title `UserGeneratedError:Billing attempt could not be triggered.`
* `GET /subscriptions/cp/api/delivery-profiles/get-locations` returns `fulfillsOnlineOrders` (boolean) for each location.

**Who is affected:** integrations that treat a `200` from `attempt-billing` as a successful payment, or that expect an empty body.

**Action required:** get the payment result from the `subscription.billing-success` and `subscription.billing-failure` [webhooks](https://developers.appstle.com/subscription/webhooks.md). Or poll `GET /api/external/v2/subscription-billing-attempts/past-orders?contractId={contractId}` until the attempt with the returned `billingAttemptId` has `status` `SUCCESS` or `FAILURE`. `SECURITY_CHALLENGE` means the card issuer asked for 3D Secure, and no webhook is sent for it.

Reference: [Trigger immediate billing for an order](https://developers.appstle.com/subscription-admin-api/billing-&-payments/trigger-immediate-billing-for-an-order.md), [Get past orders](https://developers.appstle.com/subscription-admin-api/billing-&-payments/get-past-orders.md).

**September 24, 2026**

Shipping

## Delivery type, shipping model and Markets shipping options

* `GET /subscriptions/cp/api/data/contract-live-shipping-rates` returns `deliveryType` for each rate.
* `GET /subscriptions/cp/api/v2/data/countries` accepts an `includeShippingModel` query parameter.
* `deliveryMethodId` is optional on `PUT /api/external/v2/bulk-automations/delivery-method`.
* On a store that uses Shopify Markets shipping, `GET /subscriptions/cp/api/data/shop-delivery-options` first returns one item per market that has active shipping options, as `{ name, marketOptions }`, where `marketOptions` lists each option as `{ id, name }`. The app's delivery profiles follow, in the same shape as before.
* When the delivery options can't be loaded, `shop-delivery-options` now fails with a server error instead of returning an empty list.
* On a store that uses Shopify Markets shipping, `PUT /api/external/v2/subscription-contracts-update-delivery-method` and `PUT /subscriptions/cp/api/subscription-contracts-update-delivery-method` return `400` with the title `UserGeneratedError:The selected shipping option is no longer available.` and the `errorKey` `shippingOptionUnavailable` when `delivery-method-id` names a shipping option that no longer exists.

**Who is affected:** storefront code that reads every `shop-delivery-options` item as a delivery profile.

**Action required:** check each item for `marketOptions` before you read it as a delivery profile, and handle an error response from `shop-delivery-options`.

Reference: [Bulk update subscription delivery method](https://developers.appstle.com/subscription-admin-api/operations-&-settings/bulk-update-subscription-delivery-method.md), [List delivery options available for the shop](https://developers.appstle.com/subscription-storefront-api/subscription-data/list-delivery-options-available-for-the-shop.md), [Update subscription delivery method](https://developers.appstle.com/subscription-admin-api/subscription-management/update-subscription-delivery-method.md), [Update delivery method on a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-delivery-method-on-a-subscription-contract.md).

**September 22, 2026**

Bundle settings

## `stickyCheckoutButton`

Bundle settings return `stickyCheckoutButton` (boolean).

**September 21, 2026**

Unskipping an order, splitting

## A clear error when an order can't be unskipped yet, and one-time products in splits

`PUT /api/external/v2/subscription-billing-attempts/unskip-order/{id}` and `PUT /subscriptions/cp/api/subscription-billing-attempts/unskip-order/{id}` return `400` with the title `UserGeneratedError:This order is currently being processed by a billing job. Please wait a few minutes and try again.` and the `errorKey` `concurrencyFailure` while a billing job is working on the subscription. Until now the call failed with a `409` or `500` and no usable message. Wait a few minutes and send the request again.

The split calls described under September 19 no longer refuse one-time products. The `invalidSplitProducts` refusal now has the title `UserGeneratedError:Select products to move. Free gifts cannot be selected.`

Reference: [Unskip a previously skipped order](https://developers.appstle.com/subscription-admin-api/billing-&-payments/unskip-a-previously-skipped-order.md), [Split or duplicate an existing subscription contract](https://developers.appstle.com/subscription-admin-api/subscription-management/split-or-duplicate-an-existing-subscription-contract.md).

**September 19, 2026**

Splitting a subscription

## Splits check the products you choose

`POST /api/external/v2/subscription-contract-details/split-existing-contract` and `POST /subscriptions/cp/api/subscription-contract-details/split-existing-contract` check the line IDs in the request body before they split or duplicate a subscription. Each refusal is a `400`:

* The title is `UserGeneratedError:Select subscription products to move. Free gifts and one-time products cannot be selected.` and the `errorKey` is `invalidSplitProducts` when the list is empty, names a line that isn't in the subscription, or names a free gift or a one-time product.
* With `attemptBilling` `true`, the title is `UserGeneratedError:The selected products are skipped for the next order. Unskip a product before ordering it now, or split without placing an order.` and the `errorKey` is `splitProductsSkipped` when every selected product is skipped for the next order.
* With `isSplitContract` `true`, the title is `UserGeneratedError:At least one subscription product must remain in the current subscription. Free gifts and one-time products do not count.` when the split would leave no subscription product in the original subscription. Until now the call refused only when the list named every line, with the title `UserGeneratedError:All products cannot be selected, at least one product is required in the current contract.`
* With `isSplitContract` `true`, the rules for removing a product, such as the store's minimum number of products and a product's minimum number of orders, are checked before the new subscription is created. Until now a refusal came after it had been created. If the products then can't be removed from the original subscription, the call returns `400` with the title `UserGeneratedError:The products could not be removed from the original subscription. No immediate order was requested. Contact support before trying again.` and the `errorKey` `splitRemovalFailed`. With `attemptBilling` `true`, the new subscription's order is placed only after the products are removed.

**Who is affected:** code that splits or duplicates subscriptions through either API.

**Action required:** offer only subscription products in a split, and show the `400` title when a call is refused.

Reference: [Split or duplicate an existing subscription contract](https://developers.appstle.com/subscription-admin-api/subscription-management/split-or-duplicate-an-existing-subscription-contract.md), [Split or duplicate an existing subscription contract (Storefront API)](https://developers.appstle.com/subscription-storefront-api/subscription-management/split-or-duplicate-an-existing-subscription-contract.md).

**September 17, 2026**

Error responses

## Error responses use `appstle.com` problem types and hide internal messages

The `application/problem+json` error responses changed in two ways:

* The `type` of an error now starts with `https://appstle.com/problem/`, for example `https://appstle.com/problem/problem-with-message`. Before, it started with a different base URL.
* `detail` no longer carries internal error text. A server error (`5xx`) has the `detail` `An unexpected error occurred. Please try again later.` A `400` for a path or query parameter of the wrong type, such as a customer portal settings ID that isn't a number, has the `detail` `Unexpected runtime exception`. A `400` for a request body that isn't valid JSON has the `detail` `Unable to convert http message`.

**Who is affected:** clients that match on the `type` URL, or on the text of `detail` in these errors.

**Action required:** stop matching on `type`. Branch on `status`, and on `errorKey` or `title` where the body has them. Treat `detail` in these errors as a generic message.

## Delivery address updates return specific errors

`PUT /api/external/v2/subscription-contracts-update-shipping-address` and `PUT /subscriptions/cp/api/subscription-contracts-update-shipping-address` now return `400` with these titles and `errorKey`s:

* `invalidCountry`: `UserGeneratedError:Choose a valid country for this address.` when `countryCode` isn't a valid country code.
* `addressRequired`: `UserGeneratedError:Enter a street address.` when `address1` is blank.
* `locationRequired`: `UserGeneratedError:Choose a pickup location.` when the delivery method is local pickup and `locationId` is blank.
* `addressUpdateFailed`: `UserGeneratedError:We couldn't update this delivery address. Check the address details and try again.` when Shopify refuses the change. Until now, the title was Shopify's own message. From October 4, a refusal of the address itself names the problem again.

**Who is affected:** code that updates a delivery address and shows or matches on the error message.

**Action required:** check the country, street address and pickup location before you call, and show the title after `UserGeneratedError:` when a call is refused.

Reference: [Update subscription delivery address and method](https://developers.appstle.com/subscription-admin-api/subscription-management/update-subscription-delivery-address-and-method.md), [Update subscription delivery address and method (Storefront API)](https://developers.appstle.com/subscription-storefront-api/subscription-management/update-subscription-delivery-address-and-method.md).

**September 16, 2026**

Activity logs, live order status

## New activity log entity type and line item selling plan name

* `entityType` in `GET /api/external/v2/activity-logs` has a new value, `QUICK_ACTION_LINK`.
* `GET /subscriptions/cp/api/orders/{orderId}/live-status` returns `lineItems[].sellingPlanName`.

**Who is affected:** clients that map `entityType` to a fixed list of values.

**Action required:** accept values you don't recognize.

**September 13, 2026**

Request parameters and bodies

## Requests can't name another customer or store

* A Storefront API request that names a `customerId` other than the signed-in customer's returns `403` with the `detail` `Customer does not match the authenticated session`. This covers `customerId` and `customer_id` in the query string (for example `GET /subscriptions/cp/api/loyalty-integration/earn-options?customerId=...`) and in the request body (for example `PUT /subscriptions/cp/api/subscription-contract-details`).
* A request body whose `shop`, `shopName`, `shopDomain` or `shop_domain` field names another store returns `403` with the `detail` `Shop does not match the authenticated session`. This covers Storefront API request bodies, and Admin API request bodies that have one of these fields, such as `PUT /api/external/v2/product-swaps`. The same check on query parameters started on September 6.

**Who is affected:** storefront code that sends a `customerId` that isn't the signed-in customer's, or a shop field that isn't the store's own, and Admin API clients that send another store's domain in one of these body fields.

**Action required:** leave `customerId` and the shop fields out of requests, or send the signed-in customer's and the store's own values. The session and the API key already identify both.

Reference: [Get available ways to earn loyalty points](https://developers.appstle.com/subscription-storefront-api/loyalty-integration/get-available-ways-to-earn-loyalty-points.md), [Update subscription contract details](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-subscription-contract-details.md), [Update an existing product swap automation](https://developers.appstle.com/subscription-admin-api/product-catalog/update-an-existing-product-swap-automation.md).

**September 11, 2026**

Partner Framework, Build-a-Box pages

## One error for every failed partner authentication

The Partner Framework endpoints that partners call under `/api/partner/{partnerId}/` now return one error when authentication fails. A Partner ID that is wrong or belongs to a deactivated partner returns `400` with the `errorKey` `TOKEN_INVALID` and the title `UserGeneratedError:Invalid partner credentials.`, the same as a wrong secret, an invalid HMAC signature or an old timestamp. Until now, an unknown or deactivated Partner ID returned `PARTNER_NOT_FOUND`.

**Who is affected:** partners whose code branches on `PARTNER_NOT_FOUND`.

**Action required:** stop handling `PARTNER_NOT_FOUND`. On `TOKEN_INVALID`, check the Partner ID first, then the secret or HMAC key.

Reference: [Partner integration](https://developers.appstle.com/subscription/partner-integration.md#error-codes).

## Build-a-Box pages clean the top and bottom HTML

Build-a-Box pages now clean the `bundleTopHtml` and `bundleBottomHtml` fields of `POST` and `PUT /api/external/v2/build-a-box` before they show them. The API still stores what you send. On the page, Appstle keeps standard formatting tags, links, images and tables with a fixed list of attributes, and `style` declarations from a fixed list of properties. It removes everything else, such as `<script>`, `<style>`, `<iframe>` and `<form>` tags and event handler attributes like `onclick`. It also drops any link or image URL whose scheme isn't `http` or `https`, such as `javascript:`, except `mailto:` and `tel:` links. Relative URLs are kept.

**Who is affected:** developers who set these fields with scripts, styles or other interactive HTML.

**Action required:** put scripts and styles in your theme and use plain HTML in these fields.

Reference: [Create a new Build-a-Box subscription bundle](https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/create-a-new-build-a-box-subscription-bundle.md).

**September 10, 2026**

Subscription products, selling plans

## `removeDiscount` is honored, and fixed date schedules allow 24 dates

* `PUT /api/external/v2/subscription-contracts-remove-line-item` now honors `removeDiscount=false` and keeps the subscription's discounts where it can, as the reference describes. Until now the endpoint ignored the parameter and always removed the discounts that applied only to the removed line. Without `removeDiscount`, or with `true`, nothing changes.
* `POST` and `PUT /api/external/v2/subscription-groups` accept up to 24 entries in a plan's `fixedBillingDates`, up from 12. A request with more than 24 returns `400` with the title `Fixed date schedules support up to 24 billing dates a year`. The title used to say `up to 12`.

Reference: [Remove product from subscription](https://developers.appstle.com/subscription-admin-api/subscription-products/remove-product-from-subscription.md), [Create a new subscription group (selling plan group)](https://developers.appstle.com/subscription-admin-api/subscription-plans/create-a-new-subscription-group-selling-plan-group.md).

**September 9, 2026**

Activity logs

## New activity log event type `SUPPORT_APD_DATA_REMOVED`

`eventType` in `GET /api/external/v2/activity-logs` has a new value, `SUPPORT_APD_DATA_REMOVED`.

**Who is affected:** clients that map `eventType` to a fixed list of values.

**Action required:** accept values you don't recognize.

**September 8, 2026**

Activity logs

## New activity log event type `SUPPORT_APD_DATA_UPDATED`

`eventType` in `GET /api/external/v2/activity-logs` has a new value, `SUPPORT_APD_DATA_UPDATED`.

**Who is affected:** clients that map `eventType` to a fixed list of values.

**Action required:** accept values you don't recognize.

**September 6, 2026**

Request parameters

## Query parameters can't name another store

A request whose `shop`, `shopName`, `shopDomain` or `shop_domain` query parameter names a store other than the one its API key or customer session belongs to returns `403` with the `detail` `Shop does not match the authenticated session`. This applies to the Admin API and the Storefront API.

**Who is affected:** clients that send one of these parameters with another store's domain.

**Action required:** leave these parameters out, or send the store's own domain.

Reference: [Get all subscription groups](https://developers.appstle.com/subscription-admin-api/subscription-plans/get-all-subscription-groups.md).

**September 4, 2026**

Subscription groups

## Add products to a subscription group in the background

These endpoints are for large batches (hundreds to thousands of IDs) that could time out on the synchronous endpoints.

* New `POST /api/external/v2/subscription-groups/v3/{id}/add-products` takes the same body as `POST /api/external/v2/subscription-groups/{id}/bulk-add-products` (`productIds` and `variantIds`, at least one of them). It returns `202` right away with `status` `processing` and `subscriptionGroupId`, and adds the products in the background.
* One job runs per store at a time. A second `POST` while a job is running returns `400` with the title `UserGeneratedError: A bulk add-products job for this shop is already in progress`. A `POST` for an unknown group ID returns `400` with the title `Invalid id`.
* New `GET /api/external/v2/subscription-groups/v3/{id}/add-products/status` reports the store's latest job: `found`, `running`, `executionStatus` (for example `RUNNING`, `SUCCEEDED` or `FAILED`), `errorInfo`, `subscriptionGroupId` and `matchesRequestedGroup`. Poll it until `running` is `false`. If `matchesRequestedGroup` is `false`, a newer job for another group has replaced yours, and the status isn't about your request. A store that has never run a job gets only `found: false`, `running: false` and `executionStatus: null`, whatever the group ID.

Reference: [Add products or variants to an existing subscription group (async)](https://developers.appstle.com/subscription-admin-api/subscription-plans/add-products-or-variants-to-an-existing-subscription-group-async.md), [Check status of an async add-products job](https://developers.appstle.com/subscription-admin-api/subscription-plans/check-status-of-an-async-add-products-job.md).

**September 2, 2026**

Subscription contracts

## Contract `revisionId`

Contract responses, for example `GET /api/external/v2/subscription-contracts/contract-external/{contractId}`, return `revisionId`.

**August 31, 2026**

Shipping rates

## Live carrier-quoted shipping rates

* New `GET /subscriptions/cp/api/data/contract-live-shipping-rates` lists live carrier-quoted shipping rates for a subscription contract.
* `PUT /subscriptions/cp/api/subscription-contracts-update-delivery-method` accepts a `carrier-rate` (boolean) query parameter, and `GET /subscriptions/cp/api/shop-infos-by-current-login` returns `disableShippingPricingAutoCalculation` and `recalculateShippingPriceBeforeBilling`.

Reference: [List live carrier-quoted shipping rates for a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-data/list-live-carrier-quoted-shipping-rates-for-a-subscription-contract.md).

**August 29, 2026**

Freeze status, fulfillment

## Read the freeze status of a contract, and reschedule a fulfillment shared by several subscriptions

New `GET /subscriptions/cp/api/subscription-contracts-freeze-status-detail` reads the freeze (skip-billing) status of a contract.

When several subscriptions came from the same order, `PUT /subscriptions/cp/api/subscription-contract-details/subscription-fulfillment/reschedule` needs the `contractId` query parameter to tell which one to reschedule. Without it, or with a contract that isn't one of them, the call returns `400` with the title `UserGeneratedError:Multiple subscriptions share this order, please specify the contract to reschedule` and the `errorKey` `ambiguousFulfillmentContract`.

Reference: [Read freeze (skip-billing) status of a contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/read-freeze-skip-billing-status-of-a-contract.md), [Reschedule a planned subscription fulfillment](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/reschedule-a-planned-subscription-fulfillment.md).

**August 26, 2026**

Retention

## Latest discount usage

New `GET /subscriptions/cp/api/customer-retention-activities/latest-discount-usage?contractId=…` returns the contract's latest retention activity in which a cancellation-flow discount was applied, or `404` when there's none. Use it to tell a customer they've already had the discount. Customer portal settings return the text for that warning, `discountAlreadyUsedWarningText`.

**August 25, 2026**

Bulk reads, Retry Failed Billing action

## Bulk reads for several contracts, and a Retry Failed Billing action

* New `GET /subscriptions/cp/api/campaigns/applicable-bulk` and `GET /subscriptions/cp/api/subscription-contracts/contract-external-bulk` return the applicable campaigns and the raw Shopify contracts for several contracts in one call.
* The new Shopify Flow action **Retry Failed Billing** retries a subscription's most recent failed billing attempt and reuses the failed order instead of creating a new one. It takes `contract_id` and returns `contract_id`, `billing_attempt_id`, `success` and `success_message`. `success` is `true` once billing has started, not when the payment goes through: the result fires **Subscription Billing Success** or **Subscription Billing Failure**. Like **Attempt Billing**, it needs an Appstle plan that includes immediate order placement.

Reference: [List applicable customer portal campaigns for multiple contracts in one call](https://developers.appstle.com/subscription-storefront-api/campaigns/list-applicable-customer-portal-campaigns-for-multiple-contracts-in-one-call.md), [Shopify Flow actions](https://developers.appstle.com/subscription/shopify-flow.md#actions).

**August 24, 2026**

Billing attempts, Build-a-Box, bundle discount rules

## New values `ORPHANED_FREE_PRODUCT` and `CART_DRAWER`

* `partialLinesSkipped` has a new value, `ORPHANED_FREE_PRODUCT`, in `PUT /api/external/v2/subscription-billing-attempts` and in billing attempt responses.
* `bundleRedirect` has a new value, `CART_DRAWER`, next to `CART`, `CHECKOUT`, `CUSTOM` and `NONE`, in Build-a-Box responses and in `POST` and `PUT /api/external/v2/build-a-box` bodies.

**Who is affected:** clients that map `partialLinesSkipped` or `bundleRedirect` to a fixed list of values.

**Action required:** accept values you don't recognize.

## Bundle discount rules return `400` instead of a server error

`GET /subscriptions/cp/api/bundle-discount-rules` now returns `400` with the title `UserGeneratedError: There is an issue with the Bundle App integration. Please check with the support team.` when the store's Bundles discount rules can't be loaded. Until now the call failed with a server error (`5xx`).

**Who is affected:** storefront code that reads bundle discount rules.

**Action required:** handle this `400` the way you handled the server error. A client that retries only on `5xx` no longer retries this failure.

Reference: [List discount rules applicable to bundles](https://developers.appstle.com/subscription-storefront-api/product-catalog/list-discount-rules-applicable-to-bundles.md).

**August 23, 2026**

Bundles

## Add a fixed pricing bundle line item

New `PUT /subscriptions/cp/api/v2/subscription-contracts-add-bundle-line-item` adds a fixed pricing bundle line item, with its child products, to a subscription contract. It refuses a product the customer isn't allowed to add with the `400` described under August 15.

Reference: [Add a fixed pricing bundle line item to a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/add-a-fixed-pricing-bundle-line-item-with-its-child-products-to-a-subscription-contract.md).

**August 21, 2026**

One-time products

## One-time products for a specific billing attempt are discontinued

`PUT /api/external/v2/subscription-contract-one-offs-by-contractId-and-billing-attempt-id` and its Storefront API twin, `PUT /subscriptions/cp/api/subscription-contract-one-offs-by-contractId-and-billing-attempt-id`, no longer add anything. Every call returns `400` with the title `One-time products for a specific billing attempt are no longer supported.` and saves nothing.

**Who is affected:** integrations that add a one-time product to a chosen upcoming order through either endpoint.

**Action required:** to add a one-time product to the next order, call `PUT /api/external/v2/subscription-contracts-add-line-item` with `isOneTimeProduct=true`, or `PUT /subscriptions/cp/api/v2/subscription-contracts-add-line-item` with `isOneTimeProduct=true` from the storefront. The product is removed after that order. The storefront call works only when the merchant has turned on **Customers can add one-time products**. Otherwise it returns `400` with the title `UserGeneratedError:Permission denied, Subscription cannot be modified`. There's no replacement for adding a one-time product to a later order.

Reference: [Add product to subscription](https://developers.appstle.com/subscription-admin-api/subscription-products/add-product-to-subscription.md).

**August 18, 2026**

Minimum quantity

## Minimum quantity events

* After it raises cart lines to their selling plan's minimum quantity, the storefront script dispatches `AppstleSubscription:Cart:MinimumQuantityApplied` on `document` and `window`. `event.detail` is an array with one `{ key, minQuantity, quantity }` entry per corrected line.
* On a product page, when the subscription widget raises the quantity input to the selected plan's minimum quantity, it dispatches `AppstleSubscription:SubscriptionWidget:MinimumQuantityApplied` on `document` and `window`. `event.detail` is `{ widgetId, minQuantity }`.

Reference: [JavaScript events](https://developers.appstle.com/subscription/javascript-hooks.md).

**August 17, 2026**

Activity logs, adding a product

## New activity log event type, and a `400` when a customer adds a product during an order

* `eventType` in `GET /api/external/v2/activity-logs` has a new value, `BILLING_ATTEMPT_BACKFILLED`.
* `PUT /subscriptions/cp/api/v2/subscription-contracts-add-line-item` now returns `400` with the title `UserGeneratedError:Another billing operation is already in progress. Please wait until all ongoing processes are completed.` while a billing attempt for the subscription is in progress. Until now the call went through while the order was being placed.

**Who is affected:** clients that map `eventType` to a fixed list of values, and storefront code that lets customers add products.

**Action required:** accept `eventType` values you don't recognize. When the storefront add-product call returns this `400`, wait until the order has been placed and send the request again.

Reference: [Search activity logs](https://developers.appstle.com/subscription-admin-api/operations-&-settings/getsearch-activity-logs.md), [Add a line item to a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/add-a-line-item-to-a-subscription-contract.md).

**August 15, 2026**

Customer portal product rules

## One error for products a customer isn't allowed to add or swap to

When the merchant's customer portal settings don't let a customer add a product or swap to it, two Storefront API calls return the same `400`. The title is `UserGeneratedError:This product is not available for this subscription.` and the `errorKey` is `productnotallowedforcustomerportal`.

* `PUT /subscriptions/cp/api/v2/subscription-contracts-add-line-item`
* `POST /subscriptions/cp/api/subscription-contract-details/replace-variants-v3`

Until now `PUT /subscriptions/cp/api/v2/subscription-contracts-add-line-item` didn't check these settings and added any product, and the swap call returned the title `UserGeneratedError:This product is not available for subscription.` with the `errorKey` `productnotallowedforswap`.

From the storefront, `GET /subscriptions/cp/api/data/products1` now lists products by the merchant's customer portal setting instead of the `productSelectionOption` you send, and lists every product when `purchaseOption` is `ONE_TIME`.

**Who is affected:** storefront code that lets customers add or swap products, or that matches on `productnotallowedforswap`.

**Action required:** match on the `errorKey` `productnotallowedforcustomerportal`, and show the `400` title when a product is refused.

Reference: [Add a line item to a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/add-a-line-item-to-a-subscription-contract.md), [Replace product variants in a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-management/replace-product-variants-in-a-subscription-contract.md).

**August 13, 2026**

Selling plans

## `minQuantity` on selling plans

Selling plans return `minQuantity` (integer), and `POST` and `PUT /api/external/v2/subscription-groups` accept it in `subscriptionPlans[]`.

**August 12, 2026**

Activity logs

## New activity log event type `DELIVERY_PRICE_SYNC_SKIPPED`

`eventType` in `GET /api/external/v2/activity-logs` has a new value, `DELIVERY_PRICE_SYNC_SKIPPED`.

**Who is affected:** clients that map `eventType` to a fixed list of values.

**Action required:** accept values you don't recognize.

**August 11, 2026**

Pickup, contract list, delivery price

## Pickup `contractId`, `nextPaidBillingDate` in the contract list, and a delivery price check

* `GET /subscriptions/cp/api/pickup/eligible-dates/first`, `POST /subscriptions/cp/api/pickup/eligible-dates`, and `GET` and `POST /subscriptions/cp/api/pickup/eligible-times` accept an optional `contractId`. With it, eligibility is checked against that subscription's products, starting from its next order date. `locationId` is now optional on `eligible-dates/first` and `eligible-dates`.
* `GET /api/external/v2/subscription-contract-details` now fills `nextPaidBillingDate` for each contract: the next date the customer is actually charged, which skips upcoming orders that a reward makes fully free. Until now it was `null` in this list.
* `PUT /api/external/v2/subscription-contracts-update-delivery-price` now checks that Shopify applied the new price. When Shopify didn't, usually because the subscription has no delivery method, the call returns `400` with the title `UserGeneratedError:Shopify did not apply the new delivery price. This subscription has no delivery method set, so a custom shipping price can't be saved yet. Please set a delivery method first and try again.` Until now it returned `200` and the price stayed the same.

**Who is affected:** integrations that set a delivery price on subscriptions that have no delivery method.

**Action required:** set a delivery method on the subscription before you set its delivery price, and handle this `400`.

Reference: [Retrieve subscription contracts](https://developers.appstle.com/subscription-admin-api/subscription-contracts/retrieve-subscription-contracts.md), [Update delivery price for a subscription contract](https://developers.appstle.com/subscription-admin-api/subscription-management/update-delivery-price-for-a-subscription-contract.md).

**August 10, 2026**

Free products, contract delivery method

## Multiple free products, delivery method type on new contracts

* Each `appstleCycles[]` entry returns `freeProducts[]` (`freeVariantId`, `freeProductHandle`, `freeProductTitle`, `freeProductImage`) for rewards of discount type `FREE_PRODUCT`. It holds up to 5 free products. Entry 0 is always copied to the older single `freeVariantId`, `freeProductHandle`, `freeProductTitle` and `freeProductImage` fields, so clients that read only those keys keep working. When `freeProducts` is absent, those single fields describe the one free product.
* `POST /api/external/v2/subscription-contract-details/create-subscription-contract` accepts `methodType` (`SHIPPING`, `LOCAL` or `PICK_UP`). `LOCAL` requires `deliveryPhone`, and `PICK_UP` requires `locationId`, a Shopify location ID.

**August 7, 2026**

Retention

## Customer retention activities

New `GET /api/external/v2/customer-retention-activities` lists customer retention activities, filtered by `contractId`, `eventSource`, `status`, `retentionAction`, `retentionReason`, `fromDay` and `toDay`, and paged.

Reference: [Get customer retention activities](https://developers.appstle.com/subscription-admin-api/customer-retention/get-customer-retention-activities.md).

**August 6, 2026**

Discount codes

## A clear error for a discount code on a \$0 subscription

`PUT /api/external/v2/subscription-contracts-apply-discount` and `PUT /subscriptions/cp/api/subscription-contracts-apply-discount` return `400` with the title `UserGeneratedError:This subscription already has a total price of $0. An additional discount cannot be applied.` and the `errorKey` `subscriptionTotalAlreadyZero` when the subscription's line items already total 0 (the delivery price isn't counted). The Shopify Flow action that applies a discount code fails the same way. The call also writes a failed discount entry to the subscription's activity log.

Reference: [Apply discount code to subscription](https://developers.appstle.com/subscription-admin-api/subscription-contracts/apply-discount-code-to-subscription.md), [Apply a discount code to a subscription contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/apply-a-discount-code-to-a-subscription-contract.md).

**August 5, 2026**

Cancellation offers, Build-a-Box

## `reasonId` on cancellation offers, inventory tracking mode

* `POST /subscriptions/cp/api/cancellation-retention-offers/execute` and `PUT /subscriptions/cp/api/subscription-contracts-cancellation-discount` accept `reasonId`.
* Build-a-Box bundles return and accept `inventoryTrackingMode` (`ADD_CHILD_PRODUCT_ON_ORDER` or `ADD_CHILD_PRODUCT_ON_CHECKOUT`).

**August 4, 2026**

Cancellation reasons, live order status, pickup selection

## Translated cancellation reasons, custom attributes on live order status

* `GET /api/external/v2/cancellation-managements/{id}` and its Storefront API twin accept `languageCode` and return `reasonMessageTranslationsJSON`.
* `GET /subscriptions/cp/api/orders/{orderId}/live-status` returns `lineItems[].customAttributes`.

## Pickup and delivery selections update the delivery price

`POST /subscriptions/cp/api/pickup/subscription-selection` now sets the subscription's delivery method and delivery price in the same request when the new selection changes the delivery type, the location or the rate. A change to `date`, `time` or `note` alone doesn't. The call does this only when Appstle calculates the subscription's delivery price: the subscription has no custom delivery price, and the store calculates shipping prices automatically or recalculates them when the delivery method changes. Until now, the call saved only the selection, and the delivery price was recalculated in the background afterwards.

When the new rate can't be verified, the call returns `400` and changes nothing. The title ends with `The delivery method was not changed.` The `errorKey` is one of:

* `pickupRatesUnavailable`: for a `shipping` selection, the store's Pickup & Delivery rates couldn't be loaded.
* `shippingRateUnavailable`: for a `shipping` selection, no eligible Shopify shipping rate is available.
* `pickupRateUnavailable`: for a `pickup` or `delivery` selection, the selected rate couldn't be verified.

**Who is affected:** custom pickup and delivery pickers in a customer portal you build.

**Action required:** handle this `400` and show the customer the message after `UserGeneratedError:`. After a successful call, read the subscription again to show its new delivery price.

Reference: [Update pickup and delivery details on a subscription contract](https://developers.appstle.com/subscription-storefront-api/store-pickup/update-pickup-and-delivery-details-on-a-subscription-contract.md).

**August 3, 2026**

Pickup

## Pickup times

New `POST /subscriptions/cp/api/pickup/eligible-times` lists eligible pickup times for a date. `GET /subscriptions/cp/api/pickup/eligible-times` and `POST /subscriptions/cp/api/pickup/eligible-dates` accept a `timeZone` query parameter, and `POST /subscriptions/cp/api/pickup/subscription-selection` accepts `timeZone` in the body.

Reference: [List eligible pickup times for a date](https://developers.appstle.com/subscription-storefront-api/store-pickup/list-eligible-pickup-times-for-a-date.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.