View Categories

REST API: Bundles & Composites

16 min read

The REST API for Bundles & Composites: create and change bundle products with their items, choices and rules; read a bundle line’s items on an order, or add a bundle line to an order with a configuration; add a bundle to a Store API cart with the shopper’s picks; and page through the options of a choice. The bundle fields ride on WooCommerce’s own wc/v3 products and orders endpoints, so a bundle is created, read and updated with the calls you already use for products and orders. The setup and the conventions shared by every endpoint (keys, errors, pagination) are in REST API overview and authentication.

Words you need #

  • A bundle is a product of type si_bundle.
  • A bundle has items. An item is a fixed product (kind = product) or a choice (kind = choice: the customer picks one of its options).
  • A configuration is what a customer picked: one row per item.
  • The bundle line in a cart or an order is the container. Its item lines are its children.

Base URL and sign-in #

NamespaceWhat lives thereNeeds
/wp-json/wc/v3/productsBundle products: the bundle field, read and writeA WooCommerce REST API key
/wp-json/wc/v3/ordersOrder lines: bundle_parent, bundle_children, bundle_config; adding a bundle lineA WooCommerce REST API key
/wp-json/wc/store/v1/The Store API: bundle data in extensions.sibundles, and the sibundles rows on cart/add-itemNo account. A Nonce or Cart-Token header, as for every Store API cart call
/wp-json/sibundles/v1/optionsThe options of a choice, page by pageNothing: public
  • wc/v3 takes a WooCommerce REST API key (WooCommerce › Settings › Advanced › REST API) sent as HTTP Basic. The key needs Read/Write access, and its user decides what it may do (below).
  • wc/store/v1 is WooCommerce’s public cart API. The first call to GET /wc/store/v1/cart returns a Nonce header and a Cart-Token header; send them back on every later call (a call that changes the cart without them answers 401 woocommerce_rest_missing_nonce). The cart belongs to whoever holds the token.
  • sibundles/v1/options is public product data and takes no sign-in. It answers for published bundles only, unless the visitor may read the bundle (below).
  • Send bodies as JSON (Content-Type: application/json).
  • Errors use the WordPress shape {"code", "message", "data": {"status"}}. The bundle codes start with sibundles_.
  • These fields and routes are in every release of the plugin (1.0.0 and later).
STORE="https://example.com/wp-json"
curl -u "ck_your_key:cs_your_secret" "$STORE/wc/v3/products?type=si_bundle"

Who may do what #

The routes use WooCommerce’s own permission checks. The plugin adds no capability of its own.

AbilityWho has itOpens
A key with Read access, for a user who may read productsShop managers, administratorsGET /wc/v3/products, including private and draft bundles
A key with Write access, for a user who may publish and edit productsShop managers, administratorsCreating and changing bundle products
A key with Read or Write access, for a user who may read or edit orders (edit_shop_orders)Shop managers, administratorsReading orders, and adding or changing bundle lines
NothingEveryoneThe Store API cart calls (with the Nonce or Cart-Token header) and sibundles/v1/options

A request without a valid key gets 401; a user without the ability gets 403.

Bundle products #

A product of type si_bundle carries a bundle field on every wc/v3 product response. It is read and write.

"bundle": {
  "settings": { "layout": "list", "shipping": "separately", "min_size": 2, "...": "..." },
  "items": [
    { "id": 11, "kind": "product", "product_id": 345, "position": 0, "quantity_min": 1, "quantity_default": 1, "quantity_max": 1, "priced_individually": true, "...": "..." },
    { "id": 13, "kind": "choice", "product_id": 0, "position": 2, "sources": { "product": [401, 402], "product_cat": [], "product_tag": [], "exclude": [] }, "default_pick": 401, "...": "..." }
  ],
  "rules": [],
  "stock_status": "instock",
  "stock_quantity": 10,
  "price_range": { "min": "41.99", "max": "119.96" }
}
FieldNotes
settingsRead and write. Writing merges into the saved settings. Keys: layout, form_location, group_mode, min_size, max_size, shipping, weight, sold_individually, edit_in_cart, catalog_price, qty_tiers, qty_tiers_base. A value that is not allowed falls back to its default.
itemsRead and write. Writing replaces the list (see below).
rulesRead and write. Writing replaces the rules. A list of rule objects: hide rules ({ when, then }) and only_these rules ({ combinations }).
stock_statusRead only. The stock status shown to customers.
stock_quantityRead only. How many bundles the stock allows, or null for no limit.
price_rangeRead only. min and max, the cheapest and dearest bundle price.

Sending bundle: null, or leaving bundle out, changes nothing.

Items #

Writing items replaces the whole list, in the order you send it:

  • An entry with the id of one of the bundle’s items keeps that item. The keys you send are merged into its settings.
  • An entry without an id is a new item.
  • An item you leave out is removed.
  • position is read only: the list order is the order.
  • A new item may carry a ref (write only, letters, digits, - and _) that rules can use to name it before it has an id.
Item keyNotes
kindproduct or choice. Defaults to product for a new item, and to the saved kind for an existing one.
product_idA fixed item’s product (simple or variable). Required for a new fixed item. 0 on a choice.
sourcesA choice’s options: an object of id lists. product (product ids), product_cat (category ids, with their sub-categories), product_tag (tag ids), exclude (product ids left out).
quantity_min, quantity_default, quantity_maxQuantities per bundle. quantity_max "" means no upper limit.
optional, optional_checkedThe customer may leave the item out; whether it starts ticked.
allowed_variations, default_attributesFor a variable product: the variation ids allowed, and the attributes picked first.
priced_individually, discount, discount_baseWhether the item adds its own price; a percent off (discount_base is current or regular).
shipped_individuallyShip the item as its own package.
visible_product, visible_cart, visible_orderWhere the item is listed. A hidden item is never shown to customers.
price_visible_product, price_visible_cart, price_visible_orderWhere the item’s price is shown.
title, description, hide_thumbnailOverrides for how the item is shown.

A choice also takes: default_pick, options_style, per_page, pagination, sort, filters, filter_attributes, select_action, details_title, details_description, details_image, details_price, option_price_display, image_id. Item rows come back with every setting of their kind.

Errors #

ErrorWhen
400 sibundles_rest_invalid_itemAn item has a product_id that is not an existing product, or is a grouped, external or bundle product. The message names the item number.
400 sibundles_rest_not_a_bundlebundle was sent for a product that is not of type si_bundle.
400 rest_invalid_paramWooCommerce’s schema check refused the body before the plugin saw it: bundle is not an object, rules is not a list of objects, or kind is not product or choice.
401 / 403No key, or the user may not read or edit products.

GET /wc/v3/products #

Lists products. Add type=si_bundle to list only bundles, and status=any to include private and draft ones. GET /wc/v3/products/{id} reads one. Both carry bundle for bundle products.

POST /wc/v3/products #

Creates a bundle. Send type = si_bundle, the usual product fields and bundle. A bundle’s price is its own base price (regular_price, optional) plus the items that are priced individually. This creates a private bundle with two fixed items and one choice:

curl -X POST "$STORE/wc/v3/products" \
  -u "ck_your_key:cs_your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Starter kit",
    "type": "si_bundle",
    "status": "private",
    "bundle": {
      "settings": { "layout": "list", "shipping": "separately" },
      "items": [
        { "ref": "mug", "kind": "product", "product_id": 345, "priced_individually": true },
        { "ref": "tape", "kind": "product", "product_id": 346, "quantity_default": 2, "quantity_max": 4, "priced_individually": true },
        { "ref": "pick", "kind": "choice", "sources": { "product": [401, 402] }, "default_pick": 401, "priced_individually": true }
      ]
    }
  }'

Answers 201 with the product, including bundle (item ids are assigned on save).

GET /wc/v3/products/{id} #

curl -u "ck_your_key:cs_your_secret" "$STORE/wc/v3/products/123?_fields=id,name,status,type,bundle"

PUT /wc/v3/products/{id} #

Changes a bundle. Send only what changes. This keeps the first item and the choice, changes the second item’s maximum, publishes the bundle and sets a minimum size of 2:

curl -X PUT "$STORE/wc/v3/products/123" \
  -u "ck_your_key:cs_your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "publish",
    "bundle": {
      "settings": { "min_size": 2 },
      "items": [ { "id": 11 }, { "id": 12, "quantity_max": 6 }, { "id": 13 } ]
    }
  }'

Items you leave out of the list are removed, so list every item you want to keep.

Orders #

Each line item of a wc/v3 order response carries three fields:

FieldNotes
bundle_parentThe id of the bundle line this line is an item of, or null.
bundle_childrenIds of a bundle line’s item lines (empty on other lines).
bundle_configA bundle line’s configuration, or null on other lines. Read as an object keyed by item id: { "11": { "kind", "product_id", "variation_id", "attributes", "qty", "selected", "title" } }.

bundle_parent and bundle_children are read only. bundle_config is what you send to add or change a bundle line.

Adding a bundle line #

POST /wc/v3/orders (or a PUT that adds a line) with a line item whose product_id is a bundle builds its item lines on the server: you do not post the items. Send bundle_config as rows, or leave it out for the defaults.

Row keyNotes
itemThe bundle item’s id (required in each row).
product_idFor a choice: the option picked. Items you leave out take their defaults.
quantityThe item’s quantity. Clamped into the item’s minimum and maximum.
selectedFor an optional item or a choice: false leaves it out.
variation_id, attributesFor a variable product. attributes is { "attribute_size": "Large" } or [ { "name": "size", "option": "Large" } ].

The stored shape (the object bundle_config returns) is accepted too. A row for a hidden item is ignored.

  • The item lines are created after the order is saved, then totals are worked out again.
  • Stock is not checked when you add the line. It is reduced when a paid order reduces stock, as for any order.
  • A bundle line’s quantity change (PUT with quantity) scales its totals, and its item lines follow. An item line that the same request gives its own quantity keeps it.
  • Sending bundle_config for a bundle line that is already on the order (a line item with its id) reconfigures it: the item lines are replaced to match.
  • Removing a bundle line the WooCommerce way (a line item sent with its id and product_id set to null) removes its item lines in the same save.
curl -X POST "$STORE/wc/v3/orders" \
  -u "ck_your_key:cs_your_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "pending",
    "billing": { "first_name": "Sarah", "last_name": "Miller", "email": "[email protected]" },
    "line_items": [
      {
        "product_id": 123,
        "quantity": 1,
        "bundle_config": [
          { "item": 12, "quantity": 3 },
          { "item": 13, "product_id": 402 }
        ]
      }
    ]
  }'

Answers 201. The response lists the bundle line (bundle_children has the ids of the item lines) followed by its item lines, each with bundle_parent set:

"line_items": [
  { "id": 501, "name": "Starter kit", "product_id": 123, "quantity": 1, "total": "0.00",
    "bundle_parent": null, "bundle_children": [502, 503, 504], "bundle_config": { "11": { "...": "..." } } },
  { "id": 502, "name": "Blue Mug", "product_id": 345, "quantity": 1, "total": "10.00",
    "bundle_parent": 501, "bundle_children": [], "bundle_config": null },
  { "...": "..." }
]

In this example every item is priced individually, so the prices sit on the item lines and the bundle line is 0.00.

To change the picks of a line already on the order:

curl -X PUT "$STORE/wc/v3/orders/456" \
  -u "ck_your_key:cs_your_secret" \
  -H "Content-Type: application/json" \
  -d '{ "line_items": [ { "id": 501, "bundle_config": [ { "item": 12, "quantity": 2 }, { "item": 13, "product_id": 401 } ] } ] }'

Errors #

ErrorWhen
400 sibundles_rest_invalid_configThe configuration does not work (stock aside): a pick that is not an option of the choice, a quantity or size limit that cannot be met, an unavailable variation. The message lists the problems.
400 sibundles_rest_not_a_bundlebundle_config was given on a new line whose product is not a bundle.
400 sibundles_rest_not_a_bundle_linebundle_config was given for an existing line that is not a bundle line (for example an item line).

A refused request answers 400 before the order is saved.

Read aliases for the previous bundle plugins #

For stores that moved from WooCommerce Product Bundles or Composite Products, read-only copies of the old field names are served so that integrations keep working. New integrations should use bundle, bundle_parent and bundle_children.

  • Products. On every product: bundled_by (ids of the bundles that hold the product; an empty list on a bundle), bundle_stock_status, bundle_stock_quantity. On bundles also:
    • Product Bundles names: bundle_virtual, bundle_layout, bundle_add_to_cart_form_location, bundle_editable_in_cart, bundle_sold_individually_context, bundle_item_grouping, bundle_min_size, bundle_max_size, bundle_price, bundled_items (the fixed items).
    • Composite Products names: composite_virtual, composite_layout, composite_add_to_cart_form_location, composite_editable_in_cart, composite_sold_individually_context, composite_shop_price_calc, composite_components (every item), composite_scenarios (the rules, as far as their shape can say them).
  • Order lines. bundled_by and composite_parent (the bundle line’s id as a string, "" on other lines), bundled_items and composite_children (a bundle line’s item line ids), bundled_item_title.
  • They are read only, and only keys the response does not already have are added: when the old plugin is still active, its own values stay.
  • An imported item keeps the id it had in the old plugin in these fields (bundled_item_id and the component id).

Setting: WooCommerce › Bundles › Settings › Previous REST field names: Automatic (on once bundles were imported to this store; the default), On, or Off.

curl -u "ck_your_key:cs_your_secret" "$STORE/wc/v3/products/123?_fields=id,bundled_items,composite_components"

Store API cart #

The plugin adds data to WooCommerce’s Store API; it adds no routes there.

POST /wc/store/v1/cart/add-item #

Adds a bundle with its items. Besides the usual id (the bundle’s product id) and quantity (how many bundles), it takes:

ParameterNotes
sibundlesA list of rows, or a JSON string of the list. Optional: without it the bundle is added with its defaults.

Each row is { item, product_id, quantity, selected, variation_id, attributes }, the same rows as an order’s bundle_config (see above): item is required, a choice’s pick goes in product_id, a row for a hidden item is ignored, and a quantity outside the item’s limits is clamped into them.

curl -i "$STORE/wc/store/v1/cart"
# note the Nonce and Cart-Token response headers

curl -X POST "$STORE/wc/store/v1/cart/add-item" \
  -H "Nonce: NONCE_FROM_THE_HEADER" \
  -H "Cart-Token: CART_TOKEN_FROM_THE_HEADER" \
  -H "Content-Type: application/json" \
  -d '{
    "id": 123,
    "quantity": 1,
    "sibundles": [
      { "item": 12, "quantity": 2 },
      { "item": 13, "product_id": 402 }
    ]
  }'

Answers 201 with the cart.

ErrorWhen
400 woocommerce_rest_add_to_cart_errorThe configuration does not pass the product page’s checks: a pick that is not an option, a size or quantity limit, not enough stock. The message says what to fix (for example “Choose one of the options of A choice.”).
400 sibundles_add_to_cartThe bundle passed the checks but one of its item lines could not be added to the cart.
400 woocommerce_rest_product_not_purchasableThe bundle is not published (private or draft).
401 woocommerce_rest_missing_nonceNo Nonce or Cart-Token header.

The cart response #

items in a cart response lists the bundle line (the container) and then its item lines. Every one carries extensions.sibundles:

Lineextensions.sibundles
Bundle linerole = container; mode (grouped, flat or items_only); children (cart keys of its shown item lines); editable (can be edited from the cart); saveable (can be saved for later).
Item linerole = child; mode; parent (the bundle line’s cart key); item (the bundle item id); removable; price_hidden (the price is shown on the bundle line); last (the last shown item); choice (the choice’s name, "" for a fixed item); depth (1, or 2 for an item of a bundle picked as a choice option); saveable. A bundle picked as a choice option also has its own children.
"extensions": {
  "sibundles": { "role": "child", "mode": "grouped", "parent": "349c7de3...", "item": 12,
                 "removable": false, "price_hidden": true, "last": false, "choice": "", "depth": 1, "saveable": false }
}
  • Hidden items are left out of the cart response. In items_only mode the bundle line is left out too.
  • In grouped mode the bundle line carries the price and totals of the whole bundle. Item lines carry their own quantities, which are read only: quantity_limits.editable is false.
  • Prices are in the Store API’s minor units, as strings ("6198" is 61.98 in a two-decimal currency).

GET /wc/store/v1/products and /products/{id} #

For bundles, prices is the shop price, not the bundle’s own base price: price is the cheapest configuration, regular_price its regular total, sale_price equals price, and price_range (min_amount, max_amount) is set when the dearest configuration costs more. All are null when the store hides the catalog price or this visitor may not see prices.

extensions.sibundles on a bundle:

FieldNotes
itemsThe items shown on the product page (hidden items are never listed). Each: id, kind, product_id, name, quantity_min, quantity_max, quantity_default, optional, optional_checked, priced_individually, discount (null when prices are hidden), allowed_variations. A choice adds options_count, option_ids (the first 100; use sibundles/v1/options for more), default_pick, options_style, option_price_display.
prices_shownfalse when the visitor may not see the bundle’s prices: item discounts are then left out.
layoutlist, table, grid, stacked, accordion, steps or summary_first.
group_modegrouped, flat or items_only.
min_size, max_sizeFewest and most items in total, or null.
edit_in_cartWhether the bundle can be edited from the cart.
needs_configurationWhether the shopper has something to pick before adding it.
stock_quantityHow many bundles the stock allows, or null.
rulesThe bundle’s rules.
rule_parentsVariation id to product id, for the variations the rules name.

Other products return an empty extensions.sibundles.

Plugin route: options of a choice #

GET /wp-json/sibundles/v1/options #

Public. Returns the options of one choice, for the product page: a page at a time, all of them for a dropdown, or the details of one picked option. It takes no sign-in and changes nothing. The bundle must be published; a bundle that is private, draft or password-protected answers only a visitor who may read it.

ParameterNotes
bundleInteger, required. The bundle’s product id.
itemInteger, required. The id of one of its choices. The choice must be shown on the product page.
modepage (default), all or details.
pageInteger, 1 or more. Default 1. The page size is the choice’s own (per_page, 30 for a dropdown) and cannot be set here.
searchOnly options whose name or SKU holds this text. Up to 200 characters.
orderbydefault, popularity, rating, date, price, price-desc or title.
filtersJSON (or an array): attribute taxonomy to term slugs, for example {"pa_color":["red"]}. Up to 50 slugs per taxonomy.
picksJSON: the picks so far, so the bundle’s rules apply: item id to [product id, variation id], for example {"11":[345,0]}. Up to 200 items.
pickWith mode=details: the option (product id) to describe.

Response by mode:

ModeResponse
pagehtml (the option list markup), pages_html (the paging markup), options (object keyed by product id), order (product ids in display order), total, pages, page.
alloptions (a list, up to 1,000 a page), total, page, pages, more (true when another page exists).
detailshtml, pick (the product id, or 0 when it is not an option), variable, sources, option. An unknown pick answers 200 with an empty html and pick 0.

Each option has name, label (the price text), variable, price, regular, in_stock, max and image. In all mode each also has id and text (name and price, for a dropdown).

curl "$STORE/sibundles/v1/options?bundle=123&item=13&mode=all&search=shade&orderby=title"
{
  "options": [
    { "id": 402, "name": "Table shade", "label": "$12.00", "variable": false, "price": 12, "regular": 12,
      "in_stock": true, "max": 10, "image": "", "text": "Table shade – $12.00" }
  ],
  "total": 1, "page": 1, "pages": 1, "more": false
}
ErrorWhen
400 rest_missing_callback_parambundle or item is missing.
400 rest_invalid_paramA parameter is invalid, for example a mode that is not page, all or details.
404 sibundles_not_found“No such bundle”: the id is not a bundle, or it is not published (and the visitor may not read it). “No such choice”: the item is missing, is a fixed item, or is hidden on the product page.

Hooks #

Saving a bundle through the API runs the same actions as saving it in the editor: sibundles_items_saved when its items are saved and sibundles_bundle_synced when its price and stock are counted again. When item lines follow a bundle line’s new quantity, sibundles_order_items_followed fires with the order id. The plugin has no hooks of its own for REST requests.