Carts and Orders

Discover advanced cart logic - Merging, Limits, and Rounding.

Ask about this Page
Copy for LLM
View as Markdown

After completing this page, you should be able to:

  • Describe the benefits and use cases of the new Recurring Orders feature.

  • Explain the functionality and updated behavior of the Cart merge endpoints for both native and externally authenticated customers.

  • Identify the purpose of configurable price rounding modes for Carts (HalfEven, HalfDown, HalfUp).

  • Summarize how minimum and maximum quantity limits for Line Items are set and what business problems they solve.

Note: replace hardcoded IDs (such as Inventory entry IDs, Customer IDs, and Cart IDs) with your actual values when you test or implement these examples. Retrieve the necessary IDs by using the relevant API endpoints or searching for entries that match your data.

Configurable price rounding modes for Carts

We have covered new ways to discount prices. Now, let’s talk about how we calculate the final penny.

If you have ever had an argument with your Finance team (or your ERP integration partner) about a Cart total being off by €0.01, this chapter is for you.
For years, commercetools utilized "Half-Even" rounding (often called "Banker's Rounding") as the standard. While this is statistically superior for reducing cumulative error, many ERPs and tax jurisdictions legally mandate "Half-Up" (standard commercial rounding). This mismatch often led to reconciliation errors where the Commerce system said €10.50 and the ERP said €10.51.

With the June 2025 release, you can now align your rounding logic perfectly with your backend systems.

The new roundingMode field is now available for Cart, CartDraft, Order, OrderImportDraft, Quote, and QuoteRequest resources, as well as a Project setting. This feature allows you to precisely define how line item totals, tax calculations, and discount applications are rounded, ensuring consistency with your accounting department's requirements.

The supported modes are:

  • HalfEven: the classic commercetools behavior (default).
  • HalfUp: rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
  • HalfDown: rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
Example:
We actually saw small discrepancies in the previous chapter. Even though we were applying the same discount on the same cart, when testing the even discount distribution, our cart total was higher by €0.01 than the other two carts using proportionate or individual discount distributions.
If we were to test that scenario again, but set the Cart’s priceRoundingMode to HalfUp:
Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/carts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
    "currency": "EUR",
    "country": "DE",
    "lineItems": [
      {
        "sku": "EC-0993"
      },
      {
        "sku": "WOP-09"
      },
      {
        "sku": "WTP-09"
      },
      {
        "sku": "BUCK-023"
      }
    ]
  ,
  "priceRoundingMode" : "HalfUp"
  }
'  | jq
Resultbash
...
 "cartState": "Active",
  "totalPrice": {
    "type": "centPrecision",
    "currencyCode": "EUR",
    "centAmount": 1576,
    "fractionDigits": 2
  },
...
  "priceRoundingMode": "HalfUp",
...
}

We can see that the Cart’s total price is now €15.76, just like the other two carts.

Correctly setting the configurable price rounding mode—be it HalfEven, HalfUp, or HalfDown—is paramount for financial integrity. Functional Architects must ensure the priceRoundingMode is aligned at the Project or Cart level to prevent reconciliation mismatches with downstream ERP and accounting systems. This alignment is key to eliminating costly discrepancies, ensuring compliance, and maintaining a trustworthy commerce solution.

Quantity limits for Line Items

We have covered how to price items and how to discount them. Now, let’s talk about control.

In B2B scenarios (and high-demand B2C drops), allowing users to add unlimited quantities of a single product to a cart is often risky. You might want to prevent:

  • Hoarding: a bot buying your entire stock of limited-edition sneakers.
  • Logistics nightmares: a customer accidentally ordering 500 pallets of bricks instead of 500 bricks.
  • Minimum viable orders: ensuring wholesale customers buy at least a "case" (for example, minimum 12 units).

Before July 2025, enforcing these limits required custom API extensions or frontend validation (which could be bypassed). Now, it’s a native feature of the Cart API.

Historically, if you wanted to limit a customer to "Max 5 per order," you had to build an API Extension that intercepted every addLineItem or changeLineItemQuantity request. This added latency and maintenance overhead.

If you relied only on the frontend to hide the "plus" button, savvy users could still hit the API directly and drain your inventory.

You can now define min and max quantity limits directly on each inventory entry for a Product. These limits are enforced by the platform whenever a Line Item is added or updated.
Example:

To quickly test this, let's update the default inventory entry for the Sweet Pea Candle. We want to set the minimum purchase quantity at 5 and the maximum at 50 per order.

Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/inventory/f3119cbb-9742-419a-9324-5f969b33c943' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
  "version": 1,
  "actions": [
    {
      "action": "setInventoryLimits",
      "minCartQuantity": 5,
      "maxCartQuantity": 50
    }
  ]
}' |jq

Once successful, let’s create a new cart with just one Sweet Pea candle in it:

Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/carts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
    "currency": "EUR",
    "country": "DE",
    "lineItems": [
      {
        "sku": "SPC-01"
      }
    ]
  }
' | jq
Resultjson
{
  "statusCode": 400,
  "message": "Quantity '1' less than minimum '5'.",
  "errors": [
    {
      "code": "LineItemQuantityBelowLimit",
      "message": "Quantity '1' less than minimum '5'.",
      "quantity": 1,
      "minCartQuantity": 5
    }
  ]
}

We were prevented from adding fewer candles than the previously established minimum limit.

Introducing or changing quantity limits can impact existing Line Items in Carts. The next time a Cart is updated after a limit change, any Line Item quantity that is now too high or too low will be removed. If a Cart update tries to change a Line Item's quantity or supply channel and that change would violate the new limits, the platform will reject the entire update request.

Cart merging for externally authenticated Customers

We've all been there: a customer shops on their phone as a "Guest", adds three items to their cart, and then decides to log in. Suddenly, the items disappear, or worse, they overwrite the items they had saved in their account from last week.

This "Cart Merging" friction is a classic commerce headache.

Previously, the automatic "Merge Cart" logic was tightly coupled with the commercetools native password flow.

If you used the /login endpoint (native auth), you could pass an anonymousCartId, and the platform would magically merge it with the customer's existing cart.
But if you used an External OAuth/OIDC flow, you authenticated the user first, got a token, and then had to manually move items from the guest cart to the customer cart using a script. This was error-prone and could cause unnecessary slowdowns.
We introduced a dedicated Merge Cart capability for externally authenticated customers. You can now trigger the same robust merging logic (Guest ➜ Customer) even if the customer didn't log in via the commercetools password endpoint.
Example:

Imagine that a customer has created a Cart with a Line Item in it:

Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/carts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
  "customerId" : "abcd0a05-e532-4673-9bd9-dd8e9545c24a",
    "currency": "EUR",
    "country": "DE",
    "lineItems": [
      {
        "sku": "WOP-09"
      }
    ]
  }
' | jq
The created Cart has an ID of “3f626126-2fb4-4c2d-b1af-c6df2d24a7a1”. They did not check out their cart and logged out from the store. Some time later they created a new anonymous Cart:
Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/carts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
    "currency": "EUR",
    "country": "DE",
    "lineItems": [
      {
        "sku": "WTP-09"
      }
    ]
  }
' | jq
The anonymous Cart ID is “31ce7a11-1455-4db0-b5a8-31d6b04a8bf7”.

Now that customer logs in using an external authentication method you can now explicitly tell the API: "Take this anonymous Cart and merge it into this Customer's active Cart."

Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/carts/customer-id=abcd0a05-e532-4673-9bd9-dd8e9545c24a/merge' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
  "anonymousCart" : {
    "typeId" : "cart",
    "id" : "31ce7a11-1455-4db0-b5a8-31d6b04a8bf7"
  },
  "mergeMode" : "MergeWithExistingCustomerCart",
  "updateProductData" : false
}'
The request requires the customerId to retrieve the customer's active Cart and a reference to the anonymous Cart. You also have the option to specify which of the supported Cart merging strategies should be utilized. Furthermore, you can decide whether a full update of product data is necessary. If a full update is not chosen (set to false), only the prices, discounts, and tax rates will be updated.
Since we selected the "MergeWithExistingCustomerCart" strategy, the outcome is the updated customer Cart, which now includes items from both original carts.
Resultjson
{
  "type": "Cart",
  "id": "3f626126-2fb4-4c2d-b1af-c6df2d24a7a1",
...
  },
  "customerId": "abcd0a05-e532-4673-9bd9-dd8e9545c24a",
  "lineItems": [
    {
...
      "productKey": "sweet-pea-candle",
...
    },
    {
...
      "productKey": "evergreen-candle",
...
  "totalLineItemQuantity": 2
}

Recurring Orders

If you’ve been looking for a way to lock in customer loyalty and automate revenue streams without building complex custom engines, 2025 has been your year.

Over the last few months, we’ve rolled out Recurring Orders—a native way to handle subscriptions.

In this section, we will walk through how to set up the "rhythm" of a subscription, manage the lifecycle of an order, and handle the inevitable bumps in the road using the latest API capabilities.

Before you can have a subscription, you need a schedule. The foundation of this feature is the Recurrence Policy/Interval. Think of this as the "heartbeat" of your subscription—defining if it happens daily, weekly, or monthly.
Example:

To demonstrate this, we will set up a daily subscription for a product within our project. Choosing a daily frequency will allow you to see the results of the configuration most quickly.

Let’s start by creating a recurrence policy/interval. This can be done via the API or directly in the Merchant Center by going to Settings ➜ Project Settings ➜ Intervals and selecting Add Interval:
The Create an Interval page inside Project settings in the Merchant Center
Next, let’s find a product in our Product List and create a new embedded recurring price by going to Product list ➜ <any product> ➜ Variants ➜ <any variant> ➜ Prices and select Add embedded price.
The Manage embedded price page of Product list in the Merchant Center

Remember to publish the product after creating the price.

Now that we have a “recurring price” configured we can create a Cart with a Line Item that uses it. This part can currently only be done via the API:

Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/carts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
  "currency": "EUR",
  "customerId": "abcd0a05-e532-4673-9bd9-dd8e9545c24a",
  "customerEmail": "jen@example.de",
  "shippingAddress" : {"country" : "DE"},
  "country": "DE",
  "lineItems": [
    {
      "sku": "VC-01",
      "recurrenceInfo": {
        "recurrencePolicy": {
          "typeId": "recurrence-policy",
          "key": "daily-payment"
        },
        "priceSelectionMode": "Fixed"
      }
    }
  ]
}
' | jq
Resultjson
{
  "type": "Cart",
  "id": "334f0c53-3914-4e8f-8c5a-ddcf770d46db",
  "version": 1,
...
  "lineItems": [
    {
      "id": "591d0db1-765c-4f3c-9caf-2c42cb361a10",
      "productId": "b7a01766-8dce-4b02-b4bc-7216e9032586",
      "productKey": "vanilla-candle",
...
      "price": {
        "id": "277d5f06-8e99-4858-bacc-e86c18278da2",
        "value": {
          "type": "centPrecision",
          "currencyCode": "EUR",
          "centAmount": 499,
          "fractionDigits": 2
        },
        "key": "recurrence-price",
        "country": "DE",
        "recurrencePolicy": {
          "typeId": "recurrence-policy",
          "id": "352e2fc6-e969-4654-8f28-57ae8eff05ff"
        }
      },
      "quantity": 1,
...
  "totalLineItemQuantity": 1
}

The newly created Cart should have one Line Item with a recurring price.

The example demonstrates the use of the priceSelectionMode. Setting it to Fixed ensures that all recurring orders generated from the Cart retain the initial Line Item price (EUR 4.99, as shown in the API response with "centAmount": 499), irrespective of any price changes during the subscription. Alternatively, Dynamic can be chosen, which will result in each recurring order using the Line Item's current price at the time each recurring order instance is created (not just the initial order).
The great thing about this new feature is that, even though we have a dedicated Recurring Orders API for creating recurring orders, you can still just create orders the old-fashioned way using the Orders API. The difference is that when using the Recurring Orders API, you have more flexibility regarding delaying the start until a specific date, setting the end date, and setting Custom Fields.

To prove that the above statement is correct, let’s create a recurring order from our newly created Cart using the Orders API:

Requestbash
curl  -X 'POST' \
  'https://api.{region}.commercetools.com/{projectKey}/orders' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {bearerToken}' \
  -d '{
  "cart" : {
    "id" : "334f0c53-3914-4e8f-8c5a-ddcf770d46db",
    "typeId" : "cart"
  },
  "version" : 1
}' | jq
Looks like an order was created, but is it a recurring order? We can easily check that in the Merchant Center by going to Orders ➜ Recurring Orders. You should see your newly created order there. After selecting it we can view more details. Details of a recurring order in the Recurring orders page in the Merchant Center
Here, you can set when the subscription ends, skip specific occurrences, pause/resume the subscription etc. The Linked Orders tab lists all individual orders made as part of this recurring one. Right now there's just one, but you should see more soon since the recurrence policy is set to daily.
These native subscription are key for automated revenue and loyalty. The process is simple: define a Recurrence Policy (like daily/monthly), add a recurring price to a product, and create a Cart using that price. You can create orders via the standard or the specialized Recurring Orders API, which offers more control (like setting start/end dates). A crucial feature is the priceSelectionMode: set it to Fixed (locks the initial price) or Dynamic (uses the current price at each renewal) via the Line Item's recurrenceInfo.

Test your knowledge