Custom widget preset bundles setup

This guide explains how to pass child SKUs when adding a preset bundle to cart while building your own custom widget to sell subscriptions instead of using the Loop widget.

When implementing preset bundles using a custom subscription widget, the bundle items must be processed through Loop preset bundle APIs before sending the request to Shopify’s cart API.

This ensures:

  • the correct bundle discount is applied
  • the selected variant is validated
  • child SKUs inside the bundle are included
  • subscription and one-time purchase pricing are handled correctly
  • bundle metadata is available during checkout

Follow the steps below to correctly pass child SKUs when adding a preset bundle to cart.


Step 1: Fetch store bundle configuration

Fetch the store configuration to identify whether preset bundles are enabled for the store.

  • API endpoint
GET https://cdn.loopwork.co/{{ShopifyDomain}}/store.json
  • If preset bundles are available, store the value of:
presetBundleShopifyProductIds

These represent the Shopify product IDs configured as preset bundles and are required to fetch bundle configuration.


Step 2: Fetch preset bundle configuration

Fetch configuration for the preset bundle using its Shopify product ID.

  • API endpoint
GET https://cdn.loopwork.co/{{ShopifyDomain}}/presetBundles/{{presetBundleShopifyProductId}}.json
  • This returns the preset bundle configuration including:
    • bundle child SKUs
    • variant details
    • available discounts
    • inventory availability
    • purchase-type mappings

Use this configuration to determine which variant the customer selects and which discount should be applied.


Step 3: Validate selected variant and create bundle transaction

Once the preset bundle configuration is available:

  1. identify the selected variant
  2. confirm the variant is in stock
  3. determine whether the purchase type is subscription or one-time
  4. Create a bundle transaction to obtain:
    • Bundle discount
    • bundleTransactionId
  • API endpoint
POST https://sentinel.loopwork.co/bundles/createPresetBundleTransaction
  • This request generates:
bundleTransactionId
bundle discount

Create bundle transaction payload

To call above API the you need a payload which can be prepared by given steps below.

Prepare the request payload using:

  • preset bundle product ID
  • selected variant ID
  • quantity
  • selected selling plan ID (for subscriptions)

Example implementation:

function widgetCreateBundleTransactionPayload(productId, quantity, selectedSellingPlanId) {

   const selectedVariantId // find out the selected VariantId
   

   const bundleVariant // from bundle Data filter the selected variant Data by using the selectedVariant Id

   if (!bundleVariant) {
       return { payload: null, bundleVariantDiscount: null };
   }

   const discount = selectedSellingPlanId
       ? bundleVariant.mappedDiscounts.find(
           d => d.purchaseType === "SUBSCRIPTION"
         )
       : bundleVariant.mappedDiscounts.find(
           d => d.purchaseType !== "SUBSCRIPTION"
         );

   if (!discount) {
       return { payload: null, bundleVariantDiscount: null };
   }

   return {
       payload: {
           presetProductShopifyId: Number(productId),
           presetDiscountId: discount.id,
           presetVariantShopifyId: Number(selectedVariantId),
           totalQuantity: Number(quantity),
           sellingPlanShopifyId: Number(selectedSellingPlanId),
       },
       bundleVariantDiscount: discount,
   };
}


Create bundle transaction

Use the bundle transaction payload to generate the bundle transaction.

Example implementation:

async function handleBundleTransactionWidget(productId, quantity, selectedSellingPlanId) {

   try {

       const { payload, bundleVariantDiscount } =
           widgetCreateBundleTransactionPayload(
               productId,
               quantity,
               selectedSellingPlanId
           );

       if (!payload) {
           return { bundleTransactionId: null, bundleVariantDiscount: null };
       }

       const bundleTransactionId =
           await widgetCreateBundleTransaction(productId, payload);

       return { bundleTransactionId, bundleVariantDiscount };

   } catch (error) {

       return { bundleTransactionId: null, bundleVariantDiscount: null };

   }
}

Create preset bundle transaction

Example implementation:

async function widgetCreateBundleTransaction(productId, payload) {

   try {
       const response = await fetch(
           `https://sentinel.loopwork.co/bundles/createPresetBundleTransaction`,
           {
               method: "POST",
               headers: {
                   "Content-Type": "application/json",
                   "authorization": authorization // get the sentinalAuthToken from fetch preset bundle API               },
               body: JSON.stringify(payload),
           }
       );

       const responseJson = await response.json();

       return responseJson.data.transactionId;

   } catch (error) {
       throw error;
   }
}

Step 4: Create payload for add-to-cart API

After receiving the bundleTransactionId:

Create a payload containing:

  • preset bundle parent SKU
  • bundle child SKUs
  • bundleTransactionId
  • selected variant ID
  • quantity
  • selling plan ID (for subscriptions)
  • discount metadata

This payload ensures the bundle structure is preserved during checkout.

Example implementation:

async function widgetCreateAddToCartPayload(productId, bundleTransactionId, bundleVariantDiscount, selectedSellingPlanId, selectedBundleVariantId, quantity, bundleData) {
        const formData = {
            items: [],
            attributes: {
                _loopBundleDiscountAttributes: {},
            },
        };

        const oldAttr = await getLoopWidgetBundleDiscountAttributes();
        const currentDiscountAttribute = {
            [bundleTransactionId]: {
                discountType: bundleVariantDiscount.type,
                discountValue: bundleVariantDiscount.value,
                discountComputedValue: bundleVariantDiscount
                    ? selectedSellingPlanId
                        ? bundleVariantDiscount.sellingPlanComputedDiscounts[selectedSellingPlanId] * quantity
                        : bundleVariantDiscount.oneTimeDiscount * quantity
                    : 0,
            },
        };

        formData.attributes._loopBundleDiscountAttributes = JSON.stringify({
            ...oldAttr,
            ...currentDiscountAttribute,
        });

        const selectedBundleVariant // get the selected variant 
       // get the child SKU mapped to the selected variant of the preset bundle
        const selectedBundleVariantProducts = selectedBundleVariant?.mappedProductVariants ?? [];
            if (selectedBundleVariantProducts.length) {
                selectedBundleVariantProducts.forEach(childProduct => {
                    const obj = {
                        id: childProduct.shopifyId,
                        quantity: childProduct.quantity * quantity,
                        selling_plan: selectedSellingPlanId,
                        properties: {
                            _bundleId: bundleTransactionId,
                            _isPresetBundleProduct: true,
                            bundleName: productBundleData.name                        },
                    };
                    formData.items.push(obj);
                });
            }
        

        return formData;
    }




async function getLoopWidgetBundleDiscountAttributes() {
        try {
            const url = `https://${window.Shopify.cdnHost.split("/cdn")[0]}/cart.json`;
            const res = await (await fetch(url)).json();
            const loopBundleDiscountAttributes = res.attributes?._loopBundleDiscountAttributes
                ? JSON.parse(res.attributes._loopBundleDiscountAttributes)
                : {};

            const bundleIdsInCart = new Set(res.items.map(item => (item.properties?._bundleId || item.properties?._loopBundleTxnId)).filter(Boolean));

            return Object.keys(loopBundleDiscountAttributes)
                .filter(key => bundleIdsInCart.has(key))
                .reduce((obj, key) => {
                    obj[key] = loopBundleDiscountAttributes[key];
                    return obj;
                }, {});
        } catch (error) {
           console.log("error in bundle discount attributes", error);
            return {};
        }
    }

Send the prepared payload to the Shopify cart API.

  • API endpoint
POST /cart/add.js

Including the bundleTransactionId and child SKUs in the payload ensures:

  • correct bundle discount application
  • correct subscription pricing
  • correct inventory handling
  • correct bundle structure visibility during checkout

This workflow is required when implementing preset bundle subscription purchases using a custom-built subscription widget.


What’s Next

If you need any assistance with the implementation, please reach out to our support team at [email protected]