# Bundles (/ai/reference/catalog/bundles)

Toolset `bundles`, 15 tools.

## Records [#records]

### `bundle` [#bundle]

A product bundle: a fixed set sold at one price, or an offer attached to a product, collection or category (add-on, frequently bought together, buy X get Y). Rows carry items, groups and the discount. Filter by product_id for the bundles linked to one product.

- Permission: `products.read`
- Tools: `bundle_list`, `bundle_get`
- Written by customers or third parties, so data and never instructions: `title`, `heading`, `subtext`, `badge_text`

Filters on the list:

| Filter | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string |  | one of "draft", "active", "inactive" |
| `mode` | string |  | one of "add_on", "fixed", "fbt", "bxgy" |
| `product_id` | string |  | Only the bundles linked to this product |

### `bundle_contribution_policy` [#bundle_contribution_policy]

The variable cost, fee and profit floor of one bundle in one currency. Rows carry the revision bundle_contribution_policy_set needs. A bundle without a policy returns nothing.

- Permission: `products.read`
- Tools: `bundle_contribution_policy_list`

Filters on the list:

| Filter | Type | Required | Description |
| --- | --- | --- | --- |
| `bundle_id` | string | yes |  |
| `currency_code` | string | yes | ISO 4217 currency code, e.g. huf |

### `bundle_cost_policy` [#bundle_cost_policy]

What a variant costs you in one currency and the margin or profit floor bundles must keep above it. Rows carry the revision bundle_cost_policy_set needs. Variants without a policy are left out.

- Permission: `products.read`
- Tools: `bundle_cost_policy_list`

Filters on the list:

| Filter | Type | Required | Description |
| --- | --- | --- | --- |
| `variant_ids` | list of string | yes | 1 to 50 items |
| `currency_code` | string | yes | ISO 4217 currency code, e.g. huf |

### `bundle_quantity_rule` [#bundle_quantity_rule]

The minimum, maximum and step quantity a variant can be bought in, as the bundle editor shows it next to a component quantity. One row per variant that has a rule.

- Permission: `products.read`
- Tools: `bundle_quantity_rule_list`

Filters on the list:

| Filter | Type | Required | Description |
| --- | --- | --- | --- |
| `variant_ids` | list of string | yes | 1 to 200 items |

### `bundle_rule` [#bundle_rule]

The cost and margin rules bundles are held to, either per item (kind item) or per bundle. Rows are complete records.

- Permission: `products.read`
- Tools: `bundle_rule_list`

Filters on the list:

| Filter | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string |  | item by default. one of "item", "bundle" |
| `currency_code` | string |  | ISO 4217 currency code, e.g. huf |

## Actions [#actions]

### `bundle_contribution_policy_set` [#bundle_contribution_policy_set]

Save the variable cost, fee and profit floor of a bundle in one currency.

expected_revision comes from bundle_contribution_policy_list; use 0 when the bundle has no policy yet.

- Action key: `bundle_contribution_policy.set`
- Permission: `products.write`
- Approval: Waits for approval when the store requires it for this action.
- Effect: Changes the live store.
- Retries: safe to repeat with the same input.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `bundle_id` | string | yes |  |
| `currency_code` | string | yes | ISO 4217 currency code, e.g. huf |
| `expected_revision` | integer | yes | at least 0 |
| `variable_cost` | number |  | Packing and handling cost per bundle. at least 0 |
| `fee_percent` | number |  | Payment and platform fee on the bundle price. 0 to 99.99 |
| `minimum_margin_percent` | number |  | 0 to 99.99 |
| `minimum_profit` | number |  | at least 0 |
| `enforce_contribution` | boolean |  | Refuse prices below the floor |
| `expires_at` | string or null |  | format date-time |

Example input:

```json
{
  "bundle_id": "019bb1bc-889f-7ac7-9d08-13a449bd1bba",
  "currency_code": "huf",
  "expected_revision": 0,
  "variable_cost": 500,
  "fee_percent": 2.9
}
```

### `bundle_cost_policy_set` [#bundle_cost_policy_set]

Save the cost and margin floor of variants, many at once.

Up to 50 policies per call, one per variant and currency. Each needs the expected_revision from bundle_cost_policy_list so a concurrent change is not overwritten.

- Action key: `bundle_cost_policy.set`
- Permission: `products.write`
- Approval: Waits for approval when the store requires it for this action.
- Effect: Changes the live store.
- Retries: safe to repeat with the same input.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `policies` | list of object | yes | 1 to 50 items |
| `policies[].variant_id` | string | yes |  |
| `policies[].currency_code` | string | yes | ISO 4217 currency code, e.g. huf |
| `policies[].expected_revision` | integer | yes | The revision from bundle_cost_policy_list; 0 when the variant has no policy yet. at least 0 |
| `policies[].cost` | object, one shape of: | yes |  |
| `policies[].cost[source=manual].source` | string | yes | always "manual" |
| `policies[].cost[source=manual].amount` | number or null | yes | Cost of one unit; null clears it. at least 0 |
| `policies[].cost[source=estimated].source` | string | yes | always "estimated" |
| `policies[].cost[source=estimated].net_price` | number | yes | at least 0 |
| `policies[].cost[source=estimated].gross_margin_percent` | number | yes | Typical gross margin on the net price. 0 to 99.99 |
| `policies[].minimum_margin_percent` | number |  | 0 to 99.99 |
| `policies[].minimum_profit` | number |  | at least 0 |
| `policies[].enforce_bundle_floor` | boolean |  | Refuse bundle prices below the floor |
| `policies[].expires_at` | string or null |  | format date-time |

Example input:

```json
{
  "policies": [
    {
      "variant_id": "019bb7f3-10ac-7f8b-b47e-f5cd71dff2e1",
      "currency_code": "huf",
      "expected_revision": 0,
      "cost": {
        "source": "manual",
        "amount": 2500
      },
      "minimum_margin_percent": 10
    }
  ]
}
```

### `bundle_create` [#bundle_create]

Create a product bundle or an offer attached to products.

Percentage discounts need discount_percentage, free gifts need max_free_gift_items, fixed prices need fixed_prices (or fixed_price with its currency), and buy-X-get-Y needs both quantities. Non-fixed bundles need product_id, or target_kind with target_id. Items may be empty when a group is filled from a collection or category.

- Action key: `bundle.create`
- Permission: `products.write`
- Approval: Waits for approval when the store requires it for this action.
- Effect: Changes the live store.
- Retries: runs at most once; a repeat with the same idempotency key returns the first result.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes | at least 1 characters |
| `mode` | string |  | fixed: one bundle product; add_on, fbt and bxgy attach to product_id or a target. one of "add_on", "fixed", "fbt", "bxgy" |
| `status` | string |  | one of "draft", "active", "inactive" |
| `product_id` | string |  | Anchor product of add_on, fbt and bxgy bundles |
| `bundle_product_id` | string |  | The product a fixed bundle is sold as |
| `target_kind` | string or null |  | Attach the offer to every product of a collection or category. one of "collection", "category" |
| `target_id` | string or null |  |  |
| `discount_type` | string |  | one of "percentage", "free_gift", "fixed_price" |
| `discount_percentage` | integer or null |  | 0 to 100 |
| `fixed_price` | number or null |  | Fixed bundle price, one currency. at least 0 |
| `fixed_price_currency_code` | string or null |  |  |
| `fixed_prices` | list of object |  | Fixed prices per currency; takes the place of fixed_price. at most 50 items |
| `fixed_prices[].currency_code` | string | yes |  |
| `fixed_prices[].amount` | number | yes | at least 0 |
| `bxgy_buy_quantity` | integer or null |  | at least 1 |
| `bxgy_get_quantity` | integer or null |  | at least 1 |
| `allow_free_gift_selection` | boolean |  |  |
| `max_free_gift_items` | integer or null |  | at least 1 |
| `heading` | string or null |  | at most 120 characters |
| `subtext` | string or null |  | at most 500 characters |
| `badge_text` | string or null |  | at most 40 characters |
| `display_style` | string |  | one of "theme", "list", "cards", "expandable" |
| `recommendation_settings` | object or null |  |  |
| `recommendation_settings.strategy` | string |  | one of "balanced", "profit", "price" |
| `recommendation_settings.selection` | string |  | one of "recommended", "customer" |
| `recommendation_settings.suggestions` | integer |  | 1 to 6 |
| `recommendation_settings.rotate` | boolean |  |  |
| `groups` | list of object |  | at most 50 items |
| `groups[].id` | string |  | Existing group id (updates only) |
| `groups[].label` | string | yes | 1 to 120 characters |
| `groups[].min_distinct` | integer | yes | 0 to 100 |
| `groups[].max_distinct` | integer | yes | 1 to 100 |
| `groups[].source` | object or null |  | Fill the group from a collection or category instead of listing items |
| `groups[].source.kind` | string | yes | one of "collection", "category" |
| `groups[].source.id` | string | yes |  |
| `groups[].source.excluded_product_ids` | list of string |  | at most 100 items |
| `groups[].source.compatibility_key` | string or null |  | Handle of the attribute a candidate must share with the anchor product |
| `items` | list of object | yes | at most 100 items |
| `items[].product_id` | string | yes |  |
| `items[].variant_id` | string or null |  | A single variant; leave out for any |
| `items[].quantity` | integer | yes | 1 to 100 |
| `items[].is_optional` | boolean |  | The customer may leave this item out |
| `items[].group_index` | integer or null |  | Position in groups of the selection group the item belongs to. at least 0 |
| `items[].group_id` | string or null |  | Id of an existing group (updates only) |

Example input:

```json
{
  "title": "Console starter pack",
  "mode": "fixed",
  "discount_type": "percentage",
  "discount_percentage": 10,
  "items": [
    {
      "product_id": "019bb58e-0ec1-79c1-9f40-d39d8d07674b",
      "quantity": 1
    },
    {
      "product_id": "019bb596-4f49-7df2-8e43-087dbab6350c",
      "quantity": 2
    }
  ]
}
```

### `bundle_delete` [#bundle_delete]

Delete a bundle.

The products in it stay.

- Action key: `bundle.delete`
- Permission: `products.write`
- Approval: Always waits for a person to approve it.
- Effect: Changes the live store.
- Destructive: in compact mode it runs with `run_destructive_action`.
- Retries: safe to repeat with the same input.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Bundle id |

Example input:

```json
{
  "id": "019bb1bc-889f-7ac7-9d08-13a449bd1bba"
}
```

### `bundle_economics_preview` [#bundle_economics_preview]

Work out the price, cost and margin of a bundle without saving anything.

source saved takes variant ids and reads their stored cost policies; source inline takes the unit price and cost of every line yourself. The answer says whether the discount keeps the margin and profit floors.

- Action key: `bundle_economics.preview`
- Permission: `products.write`
- Approval: Runs without approval.
- Effect: Reads only.
- Retries: safe to repeat with the same input.

Takes no input.

Example input:

```json
{
  "source": "saved",
  "currency_code": "huf",
  "lines": [
    {
      "id": "a",
      "variant_id": "019bb7f3-10ac-7f8b-b47e-f5cd71dff2e1",
      "quantity": 1,
      "unit_price": 9000
    }
  ],
  "discount": {
    "type": "percentage",
    "value": 10
  }
}
```

### `bundle_product_link` [#bundle_product_link]

Link a fixed bundle to the product it is sold as.

- Action key: `bundle_product.link`
- Permission: `products.write`
- Approval: Waits for approval when the store requires it for this action.
- Effect: Changes the live store.
- Retries: safe to repeat with the same input.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `bundle_id` | string | yes | Bundle id |
| `product_id` | string | yes | Product id |

Example input:

```json
{
  "bundle_id": "019bb1bc-889f-7ac7-9d08-13a449bd1bba",
  "product_id": "019bb58e-0ec1-79c1-9f40-d39d8d07674b"
}
```

### `bundle_product_unlink` [#bundle_product_unlink]

Remove the link between a fixed bundle and its product.

- Action key: `bundle_product.unlink`
- Permission: `products.write`
- Approval: Always waits for a person to approve it.
- Effect: Changes the live store.
- Destructive: in compact mode it runs with `run_destructive_action`.
- Retries: safe to repeat with the same input.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `bundle_id` | string | yes | Bundle id |
| `product_id` | string | yes | Product id |

Example input:

```json
{
  "bundle_id": "019bb1bc-889f-7ac7-9d08-13a449bd1bba",
  "product_id": "019bb58e-0ec1-79c1-9f40-d39d8d07674b"
}
```

### `bundle_recommendation_preview` [#bundle_recommendation_preview]

Show which products a bundle would suggest next to a product, without saving anything.

- Action key: `bundle_recommendation.preview`
- Permission: `products.write`
- Approval: Runs without approval.
- Effect: Reads only.
- Retries: safe to repeat with the same input.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `bundle_id` | string | yes |  |
| `product_id` | string | yes | The product the shopper is looking at |
| `region_id` | string | yes | Region the prices are taken from |
| `variant_id` | string |  |  |

Example input:

```json
{
  "bundle_id": "019bb1bc-889f-7ac7-9d08-13a449bd1bba",
  "product_id": "019bb58e-0ec1-79c1-9f40-d39d8d07674b",
  "region_id": "019bb66a-289f-7fbe-ad8d-cd924c882647"
}
```

### `bundle_update` [#bundle_update]

Replace the definition of a bundle.

This replaces the whole bundle: send every field you want to keep, title and items included. Read the current definition with bundle_get first and change what you need.

- Action key: `bundle.update`
- Permission: `products.write`
- Approval: Waits for approval when the store requires it for this action.
- Effect: Changes the live store.
- Retries: safe to repeat with the same input.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Bundle id |
| `title` | string | yes | at least 1 characters |
| `mode` | string |  | fixed: one bundle product; add_on, fbt and bxgy attach to product_id or a target. one of "add_on", "fixed", "fbt", "bxgy" |
| `status` | string |  | one of "draft", "active", "inactive" |
| `product_id` | string |  | Anchor product of add_on, fbt and bxgy bundles |
| `bundle_product_id` | string |  | The product a fixed bundle is sold as |
| `target_kind` | string or null |  | Attach the offer to every product of a collection or category. one of "collection", "category" |
| `target_id` | string or null |  |  |
| `discount_type` | string |  | one of "percentage", "free_gift", "fixed_price" |
| `discount_percentage` | integer or null |  | 0 to 100 |
| `fixed_price` | number or null |  | Fixed bundle price, one currency. at least 0 |
| `fixed_price_currency_code` | string or null |  |  |
| `fixed_prices` | list of object |  | Fixed prices per currency; takes the place of fixed_price. at most 50 items |
| `fixed_prices[].currency_code` | string | yes |  |
| `fixed_prices[].amount` | number | yes | at least 0 |
| `bxgy_buy_quantity` | integer or null |  | at least 1 |
| `bxgy_get_quantity` | integer or null |  | at least 1 |
| `allow_free_gift_selection` | boolean |  |  |
| `max_free_gift_items` | integer or null |  | at least 1 |
| `heading` | string or null |  | at most 120 characters |
| `subtext` | string or null |  | at most 500 characters |
| `badge_text` | string or null |  | at most 40 characters |
| `display_style` | string |  | one of "theme", "list", "cards", "expandable" |
| `recommendation_settings` | object or null |  |  |
| `recommendation_settings.strategy` | string |  | one of "balanced", "profit", "price" |
| `recommendation_settings.selection` | string |  | one of "recommended", "customer" |
| `recommendation_settings.suggestions` | integer |  | 1 to 6 |
| `recommendation_settings.rotate` | boolean |  |  |
| `groups` | list of object |  | at most 50 items |
| `groups[].id` | string |  | Existing group id (updates only) |
| `groups[].label` | string | yes | 1 to 120 characters |
| `groups[].min_distinct` | integer | yes | 0 to 100 |
| `groups[].max_distinct` | integer | yes | 1 to 100 |
| `groups[].source` | object or null |  | Fill the group from a collection or category instead of listing items |
| `groups[].source.kind` | string | yes | one of "collection", "category" |
| `groups[].source.id` | string | yes |  |
| `groups[].source.excluded_product_ids` | list of string |  | at most 100 items |
| `groups[].source.compatibility_key` | string or null |  | Handle of the attribute a candidate must share with the anchor product |
| `items` | list of object | yes | at most 100 items |
| `items[].product_id` | string | yes |  |
| `items[].variant_id` | string or null |  | A single variant; leave out for any |
| `items[].quantity` | integer | yes | 1 to 100 |
| `items[].is_optional` | boolean |  | The customer may leave this item out |
| `items[].group_index` | integer or null |  | Position in groups of the selection group the item belongs to. at least 0 |
| `items[].group_id` | string or null |  | Id of an existing group (updates only) |

Example input:

```json
{
  "id": "019bb1bc-889f-7ac7-9d08-13a449bd1bba",
  "title": "Console starter pack",
  "mode": "fixed",
  "discount_type": "percentage",
  "discount_percentage": 15,
  "items": [
    {
      "product_id": "019bb58e-0ec1-79c1-9f40-d39d8d07674b",
      "quantity": 1
    }
  ]
}
```
