How to add products with add-on selections to the WooCommerce cart via GraphQL.
This guide covers adding products with add-on selections to the cart.
When adding items to cart, use ProductAddonInput for each addon selection:
input ProductAddonInput { fieldName: String! value: [String]! }
| Field | Type | Required | Description |
|---|---|---|---|
fieldName | String! | Yes | Addon field identifier from query |
value | [String]! | Yes | Selected value(s) |
Add-ons can be passed to the following mutations:
addToCart - Simple and variable productsaddCompositeToCart - Composite productsmutation AddToCartWithAddons( $productId: Int! $quantity: Int! $addons: [ProductAddonInput!] ) { addToCart( input: { productId: $productId quantity: $quantity addons: $addons } ) { cart { contents { itemCount } total } cartItem { key quantity total extraData { key value } } } }
Pass the text value directly:
{ "addons": [ { "fieldName": "addon-123_engraving-0", "value": "Custom Text" } ] }
Pass an array of selected option labels (exact match):
{ "addons": [ { "fieldName": "addon-123_extras-2", "value": ["Gift Wrap", "Express Shipping"] } ] }
Pass the option label suffixed with -{1-based-index}:
{ "addons": [ { "fieldName": "addon-123_color-3", "value": "Blue-2" } ] }
Where Blue is the label and -2 indicates it's the second option.
Pass a numeric string:
{ "addons": [ { "fieldName": "addon-123_pages-6", "value": "3" } ] }
Pass the price as a string:
{ "addons": [ { "fieldName": "addon-123_tip-7", "value": "25.00" } ] }
Pass the file path after handling upload:
{ "addons": [ { "fieldName": "addon-123_image-4", "value": "/uploads/customer-file.jpg" } ] }
query GetProductAddons($id: ID!) { product(id: $id, idType: DATABASE_ID) { databaseId name price addons { fieldName name type required price priceType ... on AddonMultipleChoice { choiceType options { label price priceType } } ... on AddonCheckbox { options { label price priceType } } ... on AddonShortText { restrictions characterLimit { min max } } ... on AddonQuantity { quantityLimit { min max } } ... on AddonCustomerDefinedPrice { priceRangeLimit { min max } } } } }
interface AddonSelection { fieldName: string; value: string | string[]; } interface ProductAddon { fieldName: string; name: string; type: string; required: boolean; options?: { label: string; price: number }[]; } function buildAddonSelections( addons: ProductAddon[], formData: Record<string, any> ): AddonSelection[] { return addons .filter(addon => addon.type !== 'HEADING') .map(addon => { const value = formData[addon.fieldName]; // Skip empty optional fields if (!addon.required && !value) { return null; } // Handle multiple choice - append option index if (addon.type === 'MULTIPLE_CHOICE' && addon.options) { const selectedLabel = value; const optionIndex = addon.options.findIndex( opt => opt.label === selectedLabel ); if (optionIndex >= 0) { return { fieldName: addon.fieldName, value: `${selectedLabel}-${optionIndex + 1}`, }; } } // Handle checkbox - pass array of labels if (addon.type === 'CHECKBOX') { const selectedLabels = Array.isArray(value) ? value : [value]; return { fieldName: addon.fieldName, value: selectedLabels, }; } // All other types - pass value directly return { fieldName: addon.fieldName, value: String(value), }; }) .filter(Boolean) as AddonSelection[]; }
const ADD_TO_CART = gql` mutation AddToCart( $productId: Int! $quantity: Int! $addons: [ProductAddonInput!] ) { addToCart( input: { productId: $productId quantity: $quantity addons: $addons } ) { cart { total } cartItem { key total extraData { key value } } } } `; async function addProductWithAddons( productId: number, quantity: number, addonSelections: AddonSelection[] ) { const { data } = await client.mutate({ mutation: ADD_TO_CART, variables: { productId, quantity, addons: addonSelections, }, }); return data.addToCart; }
After adding to cart, addon data is stored in extraData:
| Key | Description |
|---|---|
addons | JSON array of processed addon data |
addons_price_before_calc | Product price before addon calculations |
addons_regular_price_before_calc | Regular price before calculations |
addons_sale_price_before_calc | Sale price before calculations (nullable) |
Each addon entry in the addons array:
interface CartItemAddon { name: string; // Addon display name value: string; // Selected value (without index suffix) price: number; // Price for this addon field_name: string; // Field identifier field_type: string; // Addon type id: string; // Addon ID price_type: string; // Pricing method display?: string; // Optional display value }
function parseCartItemAddons(extraData: { key: string; value: string }[]): CartItemAddon[] { const addonsData = extraData.find(d => d.key === 'addons'); if (!addonsData) return []; return JSON.parse(addonsData.value); }
{ "cartItem": { "key": "abc123def456", "extraData": [ { "key": "addons", "value": "[{\"name\":\"Engraving\",\"value\":\"Custom Text\",\"price\":7.99,\"field_name\":\"addon-123_engraving-0\",\"field_type\":\"custom_text\",\"id\":\"1234567890\",\"price_type\":\"flat_fee\"}]" }, { "key": "addons_price_before_calc", "value": "29.99" }, { "key": "addons_regular_price_before_calc", "value": "29.99" }, { "key": "addons_sale_price_before_calc", "value": null } ] } }
Add-ons work with composite products via addCompositeToCart:
mutation AddCompositeWithAddons( $productId: Int! $configuration: [CompositeProductConfigurationInput!]! $addons: [ProductAddonInput!] ) { addCompositeToCart( input: { productId: $productId quantity: 1 configuration: $configuration addons: $addons } ) { cart { total } cartItem { key total extraData { key value } } } }
Variables:
{ "productId": 123, "configuration": [ { "componentId": "1", "productId": 456, "quantity": 1 }, { "componentId": "2", "productId": 789, "quantity": 1 } ], "addons": [ { "fieldName": "addon-123_engraving-0", "value": "Custom Text" } ] }
Add-ons work with variable products:
mutation AddVariableWithAddons( $productId: Int! $variationId: Int! $addons: [ProductAddonInput!] ) { addToCart( input: { productId: $productId variationId: $variationId quantity: 1 addons: $addons } ) { cartItem { key variation { node { name } } extraData { key value } } } }
Addon prices are calculated based on priceType:
| Type | Calculation |
|---|---|
FLAT_FEE | addonPrice added to total |
QUANTITY_BASED | addonPrice * cartQuantity |
PERCENTAGE_BASED | productPrice * (addonPrice / 100) |
The addons_price_before_calc in extraData contains the original product price before addon calculations.
function validateAddonInput( addon: ProductAddon, value: any ): string | null { // Required validation if (addon.required && (!value || (Array.isArray(value) && value.length === 0))) { return `${addon.name} is required`; } // Skip further validation for optional empty fields if (!value) return null; switch (addon.type) { case 'SHORT_TEXT': case 'LONG_TEXT': const text = String(value); const { min, max } = addon.characterLimit || {}; if (min && text.length < min) { return `${addon.name} must be at least ${min} characters`; } if (max && text.length > max) { return `${addon.name} must be at most ${max} characters`; } break; case 'CUSTOMER_DEFINED_PRICE': const price = parseFloat(value); const { min: minPrice, max: maxPrice } = addon.priceRangeLimit || {}; if (isNaN(price) || price < 0) { return `${addon.name} must be a valid price`; } if (minPrice && price < minPrice) { return `${addon.name} must be at least ${minPrice}`; } if (maxPrice && price > maxPrice) { return `${addon.name} must be at most ${maxPrice}`; } break; case 'QUANTITY': const qty = parseInt(value, 10); const { min: minQty, max: maxQty } = addon.quantityLimit || {}; if (isNaN(qty) || qty < 0) { return `${addon.name} must be a valid quantity`; } if (minQty && qty < minQty) { return `${addon.name} must be at least ${minQty}`; } if (maxQty && qty > maxQty) { return `${addon.name} must be at most ${maxQty}`; } break; } return null; }
Common errors returned from mutations:
| Error | Cause |
|---|---|
Invalid addon field | fieldName doesn't exist on product |
Required addon missing | Required addon not provided |
Invalid addon value | Value doesn't match expected format |
Value out of range | Numeric value outside limits |
function CartItemAddons({ extraData }: { extraData: { key: string; value: string }[] }) { const addons = parseCartItemAddons(extraData); if (addons.length === 0) return null; return ( <ul className="cart-item-addons"> {addons.map((addon, index) => ( <li key={index}> <span className="addon-name">{addon.name}:</span> <span className="addon-value">{addon.value}</span> {addon.price > 0 && ( <span className="addon-price"> +${addon.price.toFixed(2)} </span> )} </li> ))} </ul> ); }