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:
- identify the selected variant
- confirm the variant is in stock
- determine whether the purchase type is subscription or one-time
- 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.
Updated 3 days ago
If you need any assistance with the implementation, please reach out to our support team at [email protected]