Documentation
Cart API Integration
Flex bundles use Shopify's Cart API to add bundles to the cart with line item properties. This guide covers both pricing modes: Parent Price (bundle uses the parent variant's price) and Component Sum (bundle price is calculated from individual component prices).
Overview
All Flex bundles are added to the cart using the /cart/add.js endpoint with a POST request. The bundle configuration is passed through line item properties, specifically the _components property which contains a JSON-stringified array of component objects.
Pricing Modes
Parent Price Mode
In this mode, the bundle uses the parent variant's price. You can optionally apply a discount percentage using the _discount property.
Use case: Fixed-price bundles where the components are pre-selected.
Component Sum Mode
In this mode, the bundle price is calculated by summing up the individual component prices. Each component must include a price property.
Use case: Build-your-own bundles, customizable bundles, or bundles where the price varies based on selections.
Parent Price Mode
When using the parent's price, add the bundle with the parent variant ID and include an optional _discount percentage.
Request Structure
let formData = {
"items": [
{
"id": 51618980823341, // Parent variant ID
"quantity": 1,
"properties": {
"_discount": 15, // OPTIONAL: Discount percentage (e.g., 15 = 15% off)
"_components": JSON.stringify([
{
"id": 12345678901, // Component variant ID
"quantity": 2, // Component quantity
"attributes": { // OPTIONAL: Component-specific attributes
"Color": "Blue",
"Size": "Medium",
"Gift Message": "Happy Birthday!"
}
},
{
"id": 12345678902,
"quantity": 1,
"attributes": {
"Style": "Classic",
"Engraving": "Your text here"
}
},
{
"id": 12345678903,
"quantity": 1
// No attributes - component with defaults
}
]),
// Display properties (visible to customer)
"Item 1": "Sample Gift with Gift Message",
"Item 2": "Sample Monogrammed Gift",
"Item 3": "Sample Gift",
// OPTIONAL: Bundle settings
"_settings": JSON.stringify({
"title": "Sample Bundle", // Custom bundle title
"image": "https://cdn.shopify.com/s/files/1/0766/2665/7581/files/theme_cover_image.jpg?v=1684349973"
})
}
}
]
};
Full Example
let formData = {
"items": [
{
"id": 51618980823341,
"quantity": 1,
"properties": {
"_discount": 15,
"_components": JSON.stringify([
{
"id": 12345678901,
"quantity": 2,
"attributes": {
"Color": "Blue",
"Size": "Medium",
"Gift Message": "Happy Birthday!"
}
},
{
"id": 12345678902,
"quantity": 1,
"attributes": {
"Style": "Classic",
"Engraving": "Your text here"
}
},
{
"id": 12345678903,
"quantity": 1
}
]),
"Item 1": "Sample Gift with Gift Message",
"Item 2": "Sample Monogrammed Gift",
"Item 3": "Sample Gift",
"_settings": JSON.stringify({
"title": "Sample Bundle",
"image": "https://cdn.shopify.com/s/files/1/0766/2665/7581/files/theme_cover_image.jpg?v=1684349973"
})
}
}
]
};
fetch(window.Shopify.routes.root + 'cart/add.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
console.log('Bundle added:', data);
})
.catch((error) => {
console.error('Error:', error);
});
Component Sum Mode
When summing component prices, each component must include a price property. The total bundle price is calculated from the sum of all component prices multiplied by their quantities.
Request Structure
let formData = {
"items": [
{
"id": 51618980823341, // Parent variant ID
"quantity": 1,
"properties": {
"_components": JSON.stringify([
{
"id": 12345678901, // Component variant ID
"quantity": 2, // Component quantity
"price": 49.99, // Component price (REQUIRED for sum mode)
"attributes": { // OPTIONAL: Component-specific attributes
"Color": "Blue",
"Size": "Medium",
"Gift Message": "Happy Birthday!"
}
},
{
"id": 12345678902,
"quantity": 1,
"price": 29.99,
"attributes": {
"Style": "Classic",
"Engraving": "Your text here"
}
},
{
"id": 12345678903,
"quantity": 1,
"price": 19.99
// No attributes - component with defaults
}
]),
// RECOMMENDED: currency the price values above were captured in
"_currency": window.Shopify.currency.active,
"_currency_rate": String(window.Shopify.currency.rate),
// Display properties (visible to customer)
"Item 1": "Sample Gift with Gift Message",
"Item 2": "Sample Monogrammed Gift",
"Item 3": "Sample Gift",
// OPTIONAL: Bundle settings
"_settings": JSON.stringify({
"title": "Sample Bundle",
"image": "https://cdn.shopify.com/s/files/1/0766/2665/7581/files/theme_cover_image.jpg?v=1684349973"
})
}
}
]
};
Full Example
let formData = {
"items": [
{
"id": 51618980823341,
"quantity": 1,
"properties": {
"_components": JSON.stringify([
{
"id": 12345678901,
"quantity": 2,
"price": 49.99,
"attributes": {
"Color": "Blue",
"Size": "Medium",
"Gift Message": "Happy Birthday!"
}
},
{
"id": 12345678902,
"quantity": 1,
"price": 29.99,
"attributes": {
"Style": "Classic",
"Engraving": "Your text here"
}
},
{
"id": 12345678903,
"quantity": 1,
"price": 19.99
}
]),
"Item 1": "Sample Gift with Gift Message",
"Item 2": "Sample Monogrammed Gift",
"Item 3": "Sample Gift",
"_settings": JSON.stringify({
"title": "Sample Bundle",
"image": "https://cdn.shopify.com/s/files/1/0766/2665/7581/files/theme_cover_image.jpg?v=1684349973"
})
}
}
]
};
fetch(window.Shopify.routes.root + 'cart/add.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
console.log('Bundle added:', data);
// Total price: (49.99 * 2) + 29.99 + 19.99 = $149.96
})
.catch((error) => {
console.error('Error:', error);
});
Multi-Currency Stores
Component price values are captured in whatever currency the customer is browsing in, but Shopify re-runs bundle pricing at checkout in the shop's base currency. In Component Sum mode, always send _currency and _currency_rate alongside _components:
"_currency": window.Shopify.currency.active, // e.g. "AED"
"_currency_rate": String(window.Shopify.currency.rate) // e.g. "3.6725" ("1.0" in the shop currency)
The app divides each captured price by _currency_rate to normalize it to the shop currency, then re-expresses it in the currency of the current pricing context, so cart display and the charged amount both come out right. If _currency_rate is missing, prices are applied as-is: correct for shop-currency sessions, but customers browsing in another currency will be charged the raw number in the shop currency (e.g. an AED 968 bundle charged as $968).
Note that this conversion applies only to the numeric price values inside _components. Display text in attributes and visible properties is never converted.
Property Reference
Line Item Properties
| Property | Type | Required | Description |
|---|---|---|---|
_components |
String (JSON) | Yes | JSON-stringified array of component objects |
_discount |
Number | No | Discount percentage (Parent Price mode only). E.g., 15 for 15% off |
_currency |
String | No | ISO code of the currency the component price values were captured in (e.g. "AED"). Informational only |
_currency_rate |
String | Recommended (Component Sum) | The presented currency's rate relative to the shop currency at the time the prices were captured, from Shopify.currency.rate. Lets the app convert prices correctly in multi-currency stores. "1" for shop-currency sessions |
_settings |
String (JSON) | No | JSON-stringified settings object for bundle customization |
_attributes |
String (JSON) | No | Fixed bundles only. JSON-stringified object of attributes applied to every component in the bundle. See Fixed Bundle Attributes |
| Custom properties | String | No | Any other properties will be displayed to the customer (e.g., "Item 1": "Gift Box") |
Component Object
| Property | Type | Required | Description |
|---|---|---|---|
id |
Number | Yes | Shopify variant ID of the component product |
quantity |
Number | Yes | Quantity of this component in the bundle |
price |
Number | Component Sum only | Price of the component (required for Component Sum mode) |
attributes |
Object | No | Key-value pairs for component-specific attributes (customizations, options, etc.) |
Settings Object
| Property | Type | Required | Description |
|---|---|---|---|
title |
String | No | Custom title to display for the bundle in cart |
image |
String | No | URL to a custom bundle image. Must be hosted on Shopify CDN |
Understanding Properties
Hidden vs. Visible Properties
Properties that start with an underscore (_) are hidden from the customer in the cart and checkout. These are used for bundle configuration:
_components- Bundle component data_discount- Discount percentage_currency/_currency_rate- Currency the component prices were captured in_settings- Bundle display settings
Properties without an underscore are visible to the customer:
"Item 1": "Blue T-Shirt (Size M)"- Shown in cart"Gift Message": "Happy Birthday!"- Shown in cart
Component Attributes
The attributes object within each component allows you to pass customization data that's specific to that component. This is useful for:
- Product options (Color, Size, Material)
- Personalization (Engraving, Monogram, Gift Message)
- Custom configurations
{
"id": 12345678901,
"quantity": 1,
"attributes": {
"Color": "Navy Blue",
"Size": "Large",
"Monogram": "JKS",
"Gift Wrap": "Yes"
}
}
Fixed Bundle Attributes
Fixed bundles expand from the configuration saved in the app, so there is no _components array to carry per-component attributes. Instead, pass an _attributes property when adding the parent variant to the cart. Every key/value pair is added to each component line in the bundle:
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{
id: 12345678901, // the fixed bundle parent variant
quantity: 1,
properties: {
_attributes: JSON.stringify({
"Gift Message": "Happy Birthday!",
"Gift Wrap": "Yes"
})
}
}]
})
});
Rules:
- Applies to fixed bundles only. Flex bundles ignore
_attributes; use the per-componentattributesobject instead. - Merged with any attributes set for the component in the app. If both define the same key, the
_attributesvalue is used. - Values must be strings. Non-string values, empty keys, and the reserved
_flex_bundlekey are ignored. - If the JSON is malformed, the property is ignored and the bundle still expands normally.
- The money amounts guidance below applies here as well.
Avoid money amounts in attributes
Attribute values (and visible line item properties) are frozen display strings. The app never parses, converts, or re-formats them; whatever you write at add-to-cart time is what appears in the cart, at checkout, on the order confirmation, and in notification emails. That makes absolute money amounts a trap in multi-currency stores: a string like "Original Price": "$185.00" captured in one currency will sit unchanged next to totals rendered in another (see Multi-Currency Stores, which only applies to the _components price values, not to attribute text).
Recommendations:
- Prefer currency-proof text: percentages (
"You Save": "15%") or counts ("Includes": "4 items") instead of amounts. - If you do show amounts, format them in the customer's presented currency (using
Shopify.currencyor your theme's money filter) so they at least match the cart at the moment of capture. Be aware they will not re-convert if the customer switches currency or when checkout settles in the shop currency.
Image Requirements
When using a custom bundle image in _settings, the image must be hosted on Shopify's CDN. You can upload images to your Shopify store via:
- Settings > Files - Upload files and get the CDN URL
- Product images - Use an existing product image URL
- Theme assets - Reference theme asset URLs
Valid CDN URL format:
https://cdn.shopify.com/s/files/1/XXXX/XXXX/XXXX/files/your-image.jpg
Error Handling
Always implement proper error handling when adding bundles to the cart:
fetch(window.Shopify.routes.root + 'cart/add.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.status === 422) {
// Handle validation errors
console.error('Validation error:', data.description);
} else {
// Success
console.log('Bundle added successfully:', data);
// Optionally refresh cart or update UI
}
})
.catch((error) => {
console.error('Error adding bundle to cart:', error);
});
Common Issues
Bundle not processing correctly
- Ensure
_componentsis properly JSON-stringified - Verify all component variant IDs are valid and in stock
- Check that the parent variant ID exists and is set up as a Flex Bundle
Discount not applying
- The
_discountproperty only works in Parent Price mode - Ensure the value is a number (not a string)
- Discount is a percentage, not a fixed amount
Custom image not showing
- Image must be hosted on Shopify's CDN
- Verify the URL is publicly accessible
- Check for typos in the URL