# Carts and Orders ## Quantity limits for Line Items Previously, commercetools allowed merchants to track total stock but didn't provide a native way to restrict the specific quantity of a product a single customer could add to their cart. This release introduces `minCartQuantity` and `maxCartQuantity` fields, allowing for more granular control over purchase behavior directly at the inventory level. ### Why use this? - Wholesale/B2B: enforce minimum order quantities for specific items. - Inventory protection: prevent "denial of inventory" attacks where one user locks up all available stock in their cart. - Limited releases: limit high-demand items (for example, "Max two per customer") to ensure fair distribution. **Example:** Let’s do a quick test and set a quantity limit for the Sweet Pea Candle (SKU: SPC-01) where each customer has to buy at least four of them but not more than 20 and see what happens when we try to only add two of them to our Cart. Of course, to update an inventory entry, we have to find it first. To keep things simple, let’s pick the inventory entry that is not bound to any specific supply channel: ```ts // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. /** * Updates the min and max quantity for a specific SKU. */ async function setMinMaxQuantityForSku( sku: string, min: number, max: number ): Promise { const response = await apiRoot .inventory() .get({ queryArgs: { where: `sku="${sku}" and supplyChannel is not defined` } }) .execute(); const entry = response.body.results[0]; return apiRoot .inventory() .withId({ ID: entry.id }) .post({ body: { version: entry.version, actions: [ { action: "setInventoryLimits", minCartQuantity: min, maxCartQuantity: max, }, ], }, }) .execute(); } /** * Creates a Cart with the specified SKU. */ async function createCart(sku: string, quantity: number): Promise { return apiRoot .carts() .post({ body: { currency: "EUR", country: "DE", lineItems: [{ sku, quantity }], }, }) .execute(); } // Execute the logic async function run() { const targetSku = "SPC-01"; try { // 1. Set Limits await setMinMaxQuantityForSku(targetSku, 4, 20); console.log(`Limits updated! Min: 4, Max: 20`); // 2. Create Cart with quantity (this will fail as 2 < 4) const cartResponse = await createCart(targetSku, 2); console.log( "Cart created successfully!", JSON.stringify(cartResponse.body.lineItems, null, 2) ); } catch (error: any) { // --- READABLE ERROR HANDLING --- console.error("\nREQUEST FAILED"); if (error.body && error.body.errors) { console.error(`Status Code: ${error.statusCode}`); error.body.errors.forEach((err: any) => { console.error(`Field: ${err.code} | Message: ${err.message}`); }); } else { console.error("System Error:", error.message); } } } run(); ``` ```java // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. /** * Updates the min and max quantity for a specific SKU. */ public CompletableFuture setMinMaxQuantityForSku(String sku, Long min, Long max) { return apiRoot.inventory().get().withWhere("sku = :sku and supplyChannel is not defined", "sku", sku).execute() .thenCompose(response -> { InventoryEntry entry = response.getBody().getResults().get(0); return apiRoot.inventory().withId(entry.getId()).post( InventoryEntryUpdateBuilder.of() .version(entry.getVersion()) .actions(InventoryEntryUpdateActionBuilder.of() .setInventoryLimitsBuilder().minCartQuantity(min).maxCartQuantity(max).build()) .build() ).execute().thenApply(res -> res.getBody()); }); } /** * Creates a Cart with the specified SKU. */ public CompletableFuture createCart(String sku, Long quantity) { return apiRoot.carts().post( CartDraftBuilder.of() .currency("EUR").country("DE") .lineItems(LineItemDraftBuilder.of().sku(sku).quantity(quantity).build()) .build() ).execute().thenApply(response -> response.getBody()); } public static void main(String[] args) { ProjectApiRoot apiRoot = HTTPApiClient.createApiClient(); InventoryService service = new InventoryService(apiRoot); String targetSku = "SPC-01"; // Execute the logic service.setMinMaxQuantityForSku(targetSku, 4L, 20L) .thenCompose(inventory -> { System.out.println("Limits updated! Min: 4, Max: 20"); // 2. Create Cart with quantity (this will fail as 2 < 4) return service.createCart(targetSku, 2L); }) .thenAccept(cart -> { System.out.println("Cart created successfully! ID: " + cart.getId()); cart.getLineItems().forEach(item -> System.out.println(" Item: " + item.getVariant().getSku() + " | Qty: " + item.getQuantity())); }) .exceptionally(ex -> { // --- READABLE ERROR HANDLING --- Throwable cause = (ex instanceof CompletionException) ? ex.getCause() : ex; System.err.println("\nREQUEST FAILED"); if (cause instanceof BadRequestException) { BadRequestException bre = (BadRequestException) cause; System.err.println("Status Code: " + bre.getStatusCode()); bre.getErrorResponse().getErrors().forEach(err -> System.err.println("Field: " + err.getCode() + " | Message: " + err.getMessage())); } else { System.err.println("System Error: " + cause.getMessage()); } return null; }).join(); } } ``` ```bash title="Result" REQUEST FAILED Status Code: 400 Field: LineItemQuantityBelowLimit | Message: Quantity '2' less than minimum '4'. ``` Looks like everything worked as expected and we couldn't add the Sweet Pea Candle to our Cart in a quantity that doesn't fall within the allowed range. Speaking of Line Item quantity, it's worth noting that commercetools now requires a minimum quantity of 1 for the `removeLineItem` update action. Specifying 0 will now throw a “400 Bad Request error”. If you omit the quantity field entirely, it still removes the whole line item, which brings this validation in line with the Shopping Lists API. ## Configurable price rounding modes for Carts If you've ever battled your Finance team or an ERP integration partner over a €0.01 discrepancy in a Cart total, this section is for you. In 2025 we have added a new `roundingMode` field, available as a Project setting and on the following resources: Cart, CartDraft, Order, OrderImportDraft, Quote, and QuoteRequest. This feature allows you to precisely define how rounding is applied to line item totals, tax calculations, and discount applications, ensuring consistency with your accounting requirements. The supported modes are: - `HalfEven`: the classic commercetools behavior (Default). - `HalfUp`: rounds to the "nearest neighbor," rounding up if both neighbors are equidistant. - `HalfDown`: rounds to the "nearest neighbor," rounding down if both neighbors are equidistant. **Example:** Let’s create three Carts containing the same items: - Evergreen Candle (SKU: EC-0993) - Wine Bottle Opener (SKU: WOP-09) - Ice Bucket (SKU: BUCK-023) - Willow Teapot (SKU: WTP-09) Based on the items selected, it appears our Cart will qualify for one of the previously configured Cart Discounts. The resulting carts will be identical, with the only difference being the `priceRoundingMode` set for each: ```ts // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. const SKUS = ["EC-0993", "WOP-09", "WTP-09", "BUCK-023"]; const CURRENCY = "EUR"; const COUNTRY = "DE"; type RoundingMode = "HalfEven" | "HalfUp" | "HalfDown"; /** * Creates a Cart with the given rounding mode and predefined SKUs. */ async function createCart(roundingMode: RoundingMode): Promise { return apiRoot .carts() .post({ body: { currency: CURRENCY, country: COUNTRY, priceRoundingMode: roundingMode, lineItems: SKUS.map((sku) => ({ sku })), }, }) .execute(); } function formatMoney(centAmount: number): string { return `${(centAmount / 100).toFixed(2)} ${CURRENCY}`; } // Execute the demo async function run(): Promise { const modes: RoundingMode[] = ["HalfEven", "HalfUp", "HalfDown"]; console.log("=== Price Rounding Modes Demo ===\n"); try { for (const mode of modes) { console.log(`Creating cart with rounding mode: ${mode} ...`); const response = await createCart(mode); const cart = response.body; console.log( `✓ Cart created | Mode: ${mode} | ID: ${cart.id} | Total: ${formatMoney( cart.totalPrice.centAmount )}` ); } } catch (error: any) { console.error("\nCART CREATION FAILED"); if (error.body && error.body.errors) { console.error(`Status Code: ${error.statusCode}`); error.body.errors.forEach((err: any) => { console.error(`Field: ${err.code} | Message: ${err.message}`); }); } else { console.error("System Error:", error.message); } } } run(); ``` ```java // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. public class RoundingDemo { // This should be initialized using your ClientCredentials setup static ProjectApiRoot apiRoot = HTTPApiClient.createApiClient(); private static final List SKUS = Arrays.asList("EC-0993", "WOP-09", "WTP-09", "BUCK-023"); private static final String CURRENCY = "EUR"; private static final String COUNTRY = "DE"; public static void main(String[] args) { System.out.println("=== Price Rounding Modes Demo ===\n"); RoundingMode[] modes = { RoundingMode.HALF_EVEN, RoundingMode.HALF_UP, RoundingMode.HALF_DOWN }; for (RoundingMode mode : modes) { try { System.out.printf("Creating cart with rounding mode: %s ...%n", mode.getJsonName()); // .join() blocks the thread to simulate the 'await' behavior in the TS script Cart cart = createCart(mode).join().getBody(); System.out.printf("✓ Cart created | Mode: %-10s | ID: %s | Total: %s%n", mode.getJsonName(), cart.getId(), formatMoney(cart.getTotalPrice().getCentAmount())); } catch (Exception e) { System.err.println("\nCART CREATION FAILED"); // In production, catch ErrorResponseException specifically e.printStackTrace(); } } } /** * Creates a Cart with the given rounding mode and predefined SKUs. */ private static java.util.concurrent.CompletableFuture> createCart(RoundingMode roundingMode) { List lineItems = SKUS.stream() .map(sku -> LineItemDraft.builder().sku(sku).build()) .collect(Collectors.toList()); CartDraft cartDraft = CartDraftBuilder.of() .currency(CURRENCY) .country(COUNTRY) .priceRoundingMode(roundingMode) .lineItems(lineItems) .build(); return apiRoot.carts() .post(cartDraft) .execute(); } private static String formatMoney(long centAmount) { return String.format("%.2f %s", centAmount / 100.0, CURRENCY); } } ``` ```bash title="Result" === Price Rounding Modes Demo === Creating cart with rounding mode: HalfEven ... ✓ Cart created | Mode: HalfEven | ID: 837e6b2b-4a71-426b-b620-2c71bfb79d8c | Total: 15.76 EUR Creating cart with rounding mode: HalfUp ... ✓ Cart created | Mode: HalfUp | ID: d6f8dcc7-8bc9-40fb-8746-74ff13558e3e | Total: 15.76 EUR Creating cart with rounding mode: HalfDown ... ✓ Cart created | Mode: HalfDown | ID: d9e1cca8-7d70-462e-b031-eba493903987 | Total: 15.75 EUR ``` The result clearly shows that the chosen rounding mode can lead to minor variations in the final results. Ultimately, choosing the right price rounding mode is key to ensuring that your price calculations are consistent and reliable across all your integrated systems. ## Cart merging for externally authenticated Customers We've all run into this: a customer shops on their phone as a "Guest," adds a few things to their cart, and then finally logs in. Poof! The items vanish, or even worse, they wipe out the stuff the customer had saved in their account last week. This "Cart Merging" scenario is a classic in commerce. Previously, the automatic "Merge Cart" logic was tightly coupled with the commercetools native password flow. If you used the `/login` endpoint (our native auth), you could just pass the `anonymousCartId`, and the platform would smoothly merge it with the customer's existing Cart. But if you were using an External OAuth/OIDC flow, you had to authenticate the user, grab the token, and then manually move items from the guest Cart to the customer Cart with some custom code. That was super error-prone and could really slow things down. Good news: We've now rolled out a dedicated [Merge Cart](/api/projects/carts.md#mergecartdraft) feature specifically for customers who log in externally. You can now use the same reliable merging logic (Guest ➜ Customer) even if they didn't come through the commercetools password endpoint. **Example:** Let's walk through a straightforward example: we will create a customer Cart and an anonymous Cart, then use the new [Merge Cart](/api/projects/carts.md#merge-cart) feature to combine them. ```ts // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. // ID of a customer belonging to our project const CUSTOMER_ID = "46fbc1a6-fd3c-424c-bbf5-096ab5da0f21"; /** * Creates a customer Cart with a single Line Item. */ async function createCustomerCart(sku: string, quantity: number): Promise { return apiRoot .carts() .post({ body: { currency: "EUR", country: "DE", customerId: CUSTOMER_ID, lineItems: [{ sku, quantity }], }, }) .execute(); } /** * Creates an anonymous Cart with a single Line Item. */ async function createAnonymousCart( sku: string, quantity: number ): Promise { return apiRoot .carts() .post({ body: { currency: "EUR", country: "DE", lineItems: [{ sku, quantity }], }, }) .execute(); } /** * Merges an anonymous Cart into a customer Cart. */ async function mergeCartsForCustomer(anonymousCartId: string): Promise { return apiRoot .carts() .customerIdWithCustomerIdValueMerge({ customerId: CUSTOMER_ID }) .post({ body: { anonymousCart: { typeId: "cart", id: anonymousCartId }, mergeMode: "MergeWithExistingCustomerCart", updateProductData: false, }, }) .execute(); } /** * Prints a formatted summary of the Cart merge operation. */ function printMergeSummary( customerCartId: string, anonymousCartId: string, mergedCart: any ): void { console.log("=== Final Result ==="); console.log(`Customer Cart ID: ${customerCartId}`); console.log(`Anonymous Cart ID: ${anonymousCartId}`); console.log(`\nMerged Cart ID: ${mergedCart.id}`); console.log(`Total Quantity: ${mergedCart.totalLineItemQuantity}`); console.log("\nLine Items in Merged Cart:"); mergedCart.lineItems.forEach((item: any, index: number) => { console.log( ` ${index + 1}. Product: ${item.productKey}, Quantity: ${ item.quantity }, SKU: ${item.variant.sku || "N/A"}` ); }); } // Execute the logic async function run() { try { console.log("=== Cart Merge Demo ===\n"); // Step 1: Create customer Cart console.log("Step 1: Creating customer cart..."); const customerCartResponse = await createCustomerCart("CST-01", 2); const customerCartId = customerCartResponse.body.id; console.log(`✓ Customer cart created with ID: ${customerCartId}\n`); // Step 2: Create anonymous Cart console.log("Step 2: Creating anonymous cart..."); const anonymousCartResponse = await createAnonymousCart("CDG-09", 1); const anonymousCartId = anonymousCartResponse.body.id; console.log(`✓ Anonymous cart created with ID: ${anonymousCartId}\n`); // Step 3: Merge Carts console.log("Step 3: Merging anonymous cart into customer cart..."); const mergedCartResponse = await mergeCartsForCustomer(anonymousCartId); console.log(`✓ Carts merged successfully!\n`); // Step 4: Print summary printMergeSummary(customerCartId, anonymousCartId, mergedCartResponse.body); } catch (error: any) { // --- READABLE ERROR HANDLING --- console.error("\nCART MERGE FAILED"); if (error.body && error.body.errors) { console.error(`Status Code: ${error.statusCode}`); error.body.errors.forEach((err: any) => { console.error(`Field: ${err.code} | Message: ${err.message}`); }); } else { console.error("System Error:", error.message); } } } run(); ``` ```java // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. public class CartMergeDemo { // Replace with your actual ProjectApiRoot initialization logic static ProjectApiRoot apiRoot = HTTPApiClient.createApiClient(); // ID of a customer belonging to our project private static final String CUSTOMER_ID = "46fbc1a6-fd3c-424c-bbf5-096ab5da0f21"; public static void main(String[] args) { try { System.out.println("=== Cart Merge Demo ===\n"); // Step 1: Create customer Cart System.out.println("Step 1: Creating customer cart..."); Cart customerCart = createCustomerCart("CST-01", 2L).join(); String customerCartId = customerCart.getId(); System.out.println("✓ Customer cart created with ID: " + customerCartId + "\n"); // Step 2: Create anonymous Cart System.out.println("Step 2: Creating anonymous cart..."); Cart anonymousCart = createAnonymousCart("CDG-09", 1L).join(); String anonymousCartId = anonymousCart.getId(); System.out.println("✓ Anonymous cart created with ID: " + anonymousCartId + "\n"); // Step 3: Merge Carts System.out.println("Step 3: Merging anonymous cart into customer cart..."); Cart mergedCart = mergeCartsForCustomer(anonymousCartId).join(); System.out.println("✓ Carts merged successfully!\n"); // Step 4: Print summary printMergeSummary(customerCartId, anonymousCartId, mergedCart); } catch (Exception e) { System.err.println("\nCART MERGE FAILED"); e.printStackTrace(); } } /** * Creates a customer Cart with a single Line Item. */ public static CompletableFuture createCustomerCart(String sku, Long quantity) { CartDraft cartDraft = CartDraftBuilder.of() .currency("EUR") .country("DE") .customerId(CUSTOMER_ID) .lineItems(Collections.singletonList( LineItemDraftBuilder.of().sku(sku).quantity(quantity).build() )) .build(); return apiRoot.carts().post(cartDraft).execute().thenApply(response -> response.getBody()); } /** * Creates an anonymous Cart with a single Line Item. */ public static CompletableFuture createAnonymousCart(String sku, Long quantity) { CartDraft cartDraft = CartDraftBuilder.of() .currency("EUR") .country("DE") .lineItems(Collections.singletonList( LineItemDraftBuilder.of().sku(sku).quantity(quantity).build() )) .build(); return apiRoot.carts().post(cartDraft).execute().thenApply(response -> response.getBody()); } /** * Merges an anonymous Cart into a customer Cart. */ public static CompletableFuture mergeCartsForCustomer(String anonymousCartId) { return apiRoot.carts() .customerIdWithCustomerIdValueMerge(CUSTOMER_ID) .post(MergeCartDraftBuilder.of() .anonymousCart(CartResourceIdentifierBuilder.of().id(anonymousCartId).build()) .build()) .execute() .thenApply(response -> response.getBody()); } /** * Prints a formatted summary of the Cart merge operation. */ private static void printMergeSummary(String customerCartId, String anonymousCartId, Cart mergedCart) { System.out.println("=== Final Result ==="); System.out.println("Customer Cart ID: " + customerCartId); System.out.println("Anonymous Cart ID: " + anonymousCartId); System.out.println("\nMerged Cart ID: " + mergedCart.getId()); System.out.println("Total Quantity: " + mergedCart.getTotalLineItemQuantity()); System.out.println("\nLine Items in Merged Cart:"); for (int i = 0; i < mergedCart.getLineItems().size(); i++) { LineItem item = mergedCart.getLineItems().get(i); System.out.printf(" %d. Product: %s, Quantity: %d, SKU: %s%n", i + 1, item.getProductKey(), item.getQuantity(), item.getVariant().getSku() != null ? item.getVariant().getSku() : "N/A" ); } } } ``` ```bash title="Result" ... === Final Result === Customer Cart ID: 121aab26-9ad6-4a95-bf86-2f3f1c139e19 Anonymous Cart ID: 22f58524-45c3-49c1-b89c-21ad5654b9c4 Merged Cart ID: 121aab26-9ad6-4a95-bf86-2f3f1c139e19 Total Quantity: 3 Line Items in Merged Cart: 1. Product: classic-serving-tray, Quantity: 2, SKU: CST-01 2. Product: crystal-drinking-glass, Quantity: 1, SKU: CDG-09 ``` The results clearly show that the two Carts were merged, exactly like if we would be using the `/login` endpoint. Since we did not specify a Cart merging strategy, `MergeWithExistingCustomerCart` was used as the default. ## Recurring Orders For a long time, implementing subscription models in commercetools required external "crons" or middleware to clone Carts and trigger order creation at specific intervals. With the 2025 release of Recurring Orders, commercetools moved this logic natively into the platform, allowing developers to define automated order schedules directly within the API. The Recurring Orders framework relies on three primary components working in tandem: ### The Recurrence Policy The Recurrence Policy defines the "when." It is a standalone resource that dictates the frequency of the subscription (for example, every 30 days, every first of the month). - Schedule Type: currently supports standard intervals (Days, Weeks, Months). - Reusability: a single policy (like "Standard Monthly") can be referenced by thousands of different recurring orders. ### The right price After establishing at least one Recurrence Policy, we must define recurring prices for the products intended for subscription sales. These prices can be either Embedded or Standalone, provided they reference a previously defined Recurrence Policy. ### The blueprint Cart Unlike standard checkouts, a Recurring Order is initialized from a Cart that acts as a template. This Cart contains the shipping address, payment info, and the specific Line Items intended for the subscription. - Recurrence Info: Line Items in these Carts must include `recurrenceInfo`, which links the item to a specific Recurrence Policy. ### The Recurring Order resource The Recurring Order is the "engine." When you create this resource, you point it to your blueprint Cart. commercetools then uses this blueprint to automatically generate new Orders at the intervals defined by the policy. **Example:** To properly set up and use this exciting new functionality, you have to follow these steps: - Define a [Recurrence Policy/interval](/api/projects/recurrence-policies.md#recurrencepolicy) for the subscription schedule (the "heartbeat"—daily, weekly, monthly). - Add at least one price to your Product Variant that is tied to the Recurrence Policy (a "subscription price" if you will). - Add the Product Variant to a customer Cart, specifying that the Line Item should use the price tied to a Recurrence Policy. - [Create a Recurring Order](/search.md?urn=ctp:api:endpoint:/{projectKey}/recurring-orders:POST) from that Cart using the `/recurring-orders` endpoint. Let’s go ahead and give it a try by coding the steps described above ```ts // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. const CUSTOMER_ID = "46fbc1a6-fd3c-424c-bbf5-096ab5da0f21"; const SKU = "WTP-09"; const CURRENCY = "EUR"; const COUNTRY = "DE"; const PRICE_AMOUNT = 599; // in cents const RECURRENCE_POLICY_KEY = "daily-subscription"; const expiryInDays = (days: number): string => new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString(); const formatMoney = (centAmount: number): string => `${(centAmount / 100).toFixed(2)} ${CURRENCY}`; /** * Creates a daily Recurrence Policy. */ async function createRecurrencePolicy(): Promise { return apiRoot .recurrencePolicies() .post({ body: { key: RECURRENCE_POLICY_KEY, schedule: { type: "standard", intervalUnit: "Days", value: 1, }, }, }) .execute(); } /** * Adds an embedded price to a Product Variant and ties it to the Recurrence Policy. */ async function addPriceToVariant( productId: string, productVersion: number, recurrencePolicyId: string ): Promise { return apiRoot .products() .withId({ ID: productId }) .post({ body: { version: productVersion, actions: [ { action: "addPrice", sku: SKU, price: { value: { type: "centPrecision", currencyCode: CURRENCY, centAmount: PRICE_AMOUNT, }, country: COUNTRY, recurrencePolicy: { typeId: "recurrence-policy", id: recurrencePolicyId, }, }, }, { action: "publish" }, ], }, }) .execute(); } /** * Fetches a product by SKU to get its ID and variant details. */ async function getProductBySku(sku: string): Promise { const response = await apiRoot .productProjections() .get({ queryArgs: { where: `masterVariant(sku="${sku}")`, }, }) .execute(); if (response.body.results.length === 0) { throw new Error(`Product with SKU "${sku}" not found`); } return response.body.results[0]; } /** * Creates a customer Cart with a specific SKU and selects "subscription price". */ async function createCustomerCart( sku: string, recurrencePolicyId: string ): Promise { return apiRoot .carts() .post({ body: { currency: CURRENCY, country: COUNTRY, customerId: CUSTOMER_ID, lineItems: [ { sku, recurrenceInfo: { recurrencePolicy: { typeId: "recurrence-policy", id: recurrencePolicyId, }, priceSelectionMode: "Fixed", // or "Dynamic" }, }, ], shippingAddress: { country: COUNTRY, }, }, }) .execute(); } /** * Creates a Recurring Order from a Cart. */ async function createRecurringOrder( cartId: string, cartVersion: number ): Promise { return apiRoot .recurringOrders() .post({ body: { cart: { typeId: "cart", id: cartId, }, cartVersion: cartVersion, expiresAt: expiryInDays(5), }, }) .execute(); } // Execute the demo async function run(): Promise { console.log("=== Recurring Orders Demo ===\n"); try { // Step 1: Create Recurrence Policy console.log("Step 1: Creating daily recurrence policy..."); const recurrencePolicyResponse = await createRecurrencePolicy(); const recurrencePolicy = recurrencePolicyResponse.body; const recurrencePolicyId = recurrencePolicy.id; console.log(`✓ Recurrence Policy created | ID: ${recurrencePolicyId}\n`); // Step 2: Get Product by SKU and add price console.log("Step 2: Fetching product and adding price..."); const product = await getProductBySku(SKU); const { id: productId, version: productVersion } = product; await addPriceToVariant(productId, productVersion, recurrencePolicyId); console.log( `✓ Price added to variant | Product ID: ${productId} | Variant SKU: ${SKU}\n` ); // Step 3: Create customer Cart console.log("Step 3: Creating customer cart..."); const cartResponse = await createCustomerCart(SKU, recurrencePolicyId); const cart = cartResponse.body; const cartId = cart.id; const cartVersion = cart.version; console.log( `✓ Customer cart created | ID: ${cartId} | Total: ${formatMoney( cart.totalPrice.centAmount )}\n` ); // Step 4: Create Recurring Order console.log("Step 4: Creating recurring order..."); const recurringOrderResponse = await createRecurringOrder( cartId, cartVersion ); const recurringOrder = recurringOrderResponse.body; console.log(`✓ Recurring order created | ID: ${recurringOrder.id}\n`); } catch (error: any) { console.error("\nRECURRING ORDER CREATION FAILED"); if (error.body && error.body.errors) { console.error(`Status Code: ${error.statusCode}`); error.body.errors.forEach((err: any) => { console.error(`Field: ${err.code} | Message: ${err.message}`); }); } else { console.error("System Error:", error.message); } } } run(); ``` ```java // Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example. public class RecurringOrdersDemo { static ProjectApiRoot apiRoot = HTTPApiClient.createApiClient(); private static final String CUSTOMER_ID = "46fbc1a6-fd3c-424c-bbf5-096ab5da0f21"; private static final String SKU = "WTP-09"; private static final String CURRENCY = "EUR"; private static final String COUNTRY = "DE"; private static final long PRICE_AMOUNT = 599L; private static final String RECURRENCE_POLICY_KEY = "daily-subscription-11"; public static void main(String[] args) { System.out.println("=== Recurring Orders Demo ===\n"); try { // Step 1: Create Recurrence Policy System.out.println("Step 1: Creating daily recurrence policy..."); RecurrencePolicy recurrencePolicy = createRecurrencePolicy().join(); String recurrencePolicyId = recurrencePolicy.getId(); System.out.println("✓ Recurrence Policy created | ID: " + recurrencePolicyId + "\n"); // Step 2: Fetch product and add price System.out.println("Step 2: Fetching product and adding price..."); ProductProjection product = getProductBySku(SKU).join(); addPriceToVariant(product.getId(), product.getVersion(), recurrencePolicyId).join(); System.out.println("✓ Price added to variant | SKU: " + SKU + "\n"); // Step 3: Create customer Cart System.out.println("Step 3: Creating customer cart..."); Cart cart = createCustomerCart(SKU, recurrencePolicyId).join(); System.out.println("✓ Cart created | ID: " + cart.getId() + " | Total: " + formatMoney(cart.getTotalPrice().getCentAmount()) + "\n"); // Step 4: Create Recurring Order System.out.println("Step 4: Creating recurring order..."); RecurringOrder recurringOrder = createRecurringOrder(cart.getId(), cart.getVersion()).join(); System.out.println("✓ Recurring order created | ID: " + recurringOrder.getId() + "\n"); } catch (Exception e) { System.err.println("\nRECURRING ORDER CREATION FAILED"); e.printStackTrace(); } } private static CompletableFuture createRecurrencePolicy() { RecurrencePolicyDraft draft = RecurrencePolicyDraftBuilder.of() .key(RECURRENCE_POLICY_KEY) .schedule(RecurrencePolicyScheduleDraftBuilder.of().standardBuilder().intervalUnit(IntervalUnit.DAYS).value(1L).build()) .build(); return apiRoot.recurrencePolicies().post(draft).execute().thenApply(r -> r.getBody()); } private static CompletableFuture addPriceToVariant(String productId, Long version, String policyId) { return apiRoot.products().withId(productId).post(ProductUpdateBuilder.of() .version(version) .actions( ProductAddPriceActionBuilder.of() .sku(SKU) .price(PriceDraftBuilder.of() .value(CentPrecisionMoneyDraftBuilder.of().currencyCode(CURRENCY).centAmount(PRICE_AMOUNT).build()) .country(COUNTRY) .recurrencePolicy(RecurrencePolicyResourceIdentifierBuilder.of().id(policyId).build()) .build()) .build(), ProductPublishActionBuilder.of().build() ).build()) .execute().thenApply(r -> r.getBody()); } private static CompletableFuture getProductBySku(String sku) { return apiRoot.productProjections().get() .withWhere("masterVariant(sku=\"" + sku + "\")") .execute() .thenApply(r -> { if (r.getBody().getResults().isEmpty()) throw new RuntimeException("Product not found"); return r.getBody().getResults().get(0); }); } private static CompletableFuture createCustomerCart(String sku, String policyId) { CartDraft draft = CartDraftBuilder.of() .currency(CURRENCY) .country(COUNTRY) .customerId(CUSTOMER_ID) .lineItems(Collections.singletonList( LineItemDraftBuilder.of() .sku(sku) .recurrenceInfo(LineItemRecurrenceInfoDraftBuilder.of() .recurrencePolicy(RecurrencePolicyResourceIdentifierBuilder.of().id(policyId).build()) .priceSelectionMode(PriceSelectionMode.FIXED) // or PriceSelectionMode.DYNAMIC .build()) .build() )) .shippingAddress(com.commercetools.api.models.common.BaseAddressBuilder.of().country(COUNTRY).build()) .build(); return apiRoot.carts().post(draft).execute().thenApply(r -> r.getBody()); } private static CompletableFuture createRecurringOrder(String cartId, Long version) { RecurringOrderDraft draft = RecurringOrderDraftBuilder.of() .cart(CartResourceIdentifierBuilder.of().id(cartId).build()) .cartVersion(version) .expiresAt(ZonedDateTime.now().plusDays(5)) .build(); return apiRoot.recurringOrders().post(draft).execute().thenApply(r -> r.getBody()); } private static String formatMoney(long centAmount) { return String.format("%.2f %s", centAmount / 100.0, CURRENCY); } } ``` ```bash title="Result" === Recurring Orders Demo === Step 1: Creating daily recurrence policy... ✓ Recurrence Policy created | ID: a713e79b-9c76-4b26-bdf3-f1365c7bf7ae Step 2: Fetching product and adding price... ✓ Price added to variant | SKU: WTP-09 Step 3: Creating customer cart... ✓ Cart created | ID: de80c89b-34cb-4405-a442-e9dcca1cf6a9 | Total: 5.99 EUR Step 4: Creating recurring order... ✓ Recurring order created | ID: 31cb251c-87a8-4c5a-9a56-6660d5f0195e ``` The expected outcome is a recurring order visible in the Merchant Center under **Orders ➜ Recurring Orders**. You will find the newly created Recurring Order listed there. As the code indicates, the subscription is configured to expire five days from the current date, which is displayed on the order's details page. This page also allows for managing the subscription, such as skipping occurrences or pausing/resuming the service. The "Linked Orders" tab tracks all individual orders generated under this recurring subscription. Currently, only one order is present (it might take a few minutes to show up), but more will appear daily, reflecting the set recurrence policy. You might have noticed we set the `priceSelectionMode` to "Fixed" when adding a Line Item to the Cart. That means the customer pays the original price every time the subscription renews, even if the product's price changes later. The other option is "Dynamic," which always grabs the current price when a new order is created for the subscription. You can whip up a Recurring Order using the Recurring Orders API or the Orders API. The Recurring Orders API offers a bit more wiggle room for setting up your order, like pushing back the start date, scheduling an end date, and adding Custom Fields. Check out the [Recurring Orders overview](/api/recurring-orders-overview.md#initial-creation) for the lowdown on the different ways to do this. ## Related pages - [Area overview page with navigation](/certifications.md) - [Previous page: Pricing and Discounts](/certifications/composable-commerce-developer-refresher-2026/pricing-and-discounts.md) - [Next page: Integrations and Extensibility](/certifications/composable-commerce-developer-refresher-2026/integrations-and-extensibility.md) - [Search documentation and API specs](/search.md)