> ## Documentation Index
> Fetch the complete documentation index at: https://docs.publica.la/llms.txt
> Use this file to discover all available pages before exploring further.

# Update a Bundle

Change a bundle's details, replace its products, swap or drop its cover, and publish or unpublish it.

---

## Endpoint

```
PUT /api/v3/bundles/{bundle}
```

---

## Path Parameters

| Parameter | Type   | Description                                                            |
| --------- | ------ | ---------------------------------------------------------------------- |
| `bundle`  | string | The bundle's `id`, or its `external_id` when `id_type=external` is set |

## Query Parameters

| Parameter | Type   | Default    | Description                                                                                                                       |
| --------- | ------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `id_type` | string | `internal` | `internal` (bundle `id`) or `external` (`external_id`)                                                                            |
| `include` | string | -          | Comma-separated: `products`, `prices`, `availability`, `detailed_stock`. See [Included blocks](./bundles-v3-overview.md#includes) |
| `fields`  | string | -          | Comma-separated list of fields to return, e.g. `id,published`. Applied after `include`                                            |

---

## Request Body

Updates are **partial**: only the fields present in the payload are validated and written. A field you do not send keeps its value, so renaming a bundle never touches its products, cover, state or `external_id`.

:::note
The verb is `PUT`, but the semantics are a merge, not a replacement: fields absent from the payload are left untouched rather than cleared.
:::

| Field              | Type    | Notes                                                                                                                                              |
| ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`             | string  | Max 255 characters. Cannot be `null`                                                                                                               |
| `products`         | array   | Replaces the whole composition, in the order sent. From 2 to 100, each once. See [Replacing the products](#replacing-the-products)                 |
| `products_id_type` | string  | `internal` (default) or `external`, how `products` names the products. See [Products by identifier](./bundles-v3-create.md#products-by-identifier) |
| `description`      | string  | Max 2,000 characters                                                                                                                               |
| `slug`             | string  | Max 255 characters                                                                                                                                 |
| `external_id`      | string  | Max 64 characters, unique in your store. Send `null` to clear it. Resending the bundle's own `external_id`, in any case, is accepted               |
| `published`        | boolean | `true` publishes, `false` unpublishes. See [Publishing](#publishing)                                                                               |
| `private`          | boolean | See [States](./bundles-v3-overview.md#states)                                                                                                      |
| `reserved_stock`   | integer | Units of each physical product held for the bundle, min `0`. See [Reserved stock](#reserved-stock)                                                 |
| `cover_url`        | string  | Downloads a new cover. `null` drops the bundle's own cover. Not accepted together with `cover`. See [Cover](#cover)                                |
| `cover`            | string  | Storage path of a new uploaded cover. `null` drops the bundle's own cover. Not accepted together with `cover_url`. See [Cover](#cover)             |

`published_at` is not accepted, as on [create](./bundles-v3-create.md#request-body): the platform sets it.

An `external_id` already used by another bundle of your store answers `422` with `A bundle with this external_id already exists.`

---

## Replacing the Products {#replacing-the-products}

Send `products` to replace the composition. The rules are the same as on [create](./bundles-v3-create.md#products): **from 2 to 100 products**, each once, named by position when one fails. Leave `products` out to keep the current composition as it is.

```bash
curl -X PUT "https://yourstore.publica.la/api/v3/bundles/318" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "products": [1207, 1201] }'
```

Send `products_id_type: external` to list them by identifier instead, as described under [Products by identifier](./bundles-v3-create.md#products-by-identifier).

Products the bundle **already has** are kept as they are, whether you send them by ID or by identifier. A product that stopped being sellable after it was added (it was withdrawn from sale, for example) does not block an update that sends it again: the bundle keeps it, and with `?include=products` the response shows its `rejection_reason`. A product **new** to the bundle must be sellable, so that same product is refused when you add it to another bundle.

A bundle that keeps an unsellable product stays unavailable. While it is unpublished, it cannot be published until you replace that product.

---

## Publishing {#publishing}

| You send                             | On a bundle that is | Result                                                                       |
| ------------------------------------ | ------------------- | ---------------------------------------------------------------------------- |
| `published: true`                    | unpublished         | Publishes it, when it can be sold with the products it has after the request |
| `published: true` without `products` | already published   | Keeps it published as it is, without checking whether it can be sold         |
| `published: true` with `products`    | already published   | Keeps it published, when it can be sold with the new products                |
| `published: false`                   | any                 | Unpublishes it. Always allowed, also on a bundle that cannot be sold         |
| no `published`                       | any                 | Leaves the state as it is                                                    |

Unpublishing a published bundle sets its `reserved_stock` to `0`, and publishing it again does not bring it back. A request that unpublishes and sends `reserved_stock` keeps the value it sends: the bundle becomes a draft with that reserved stock. See [When reserved stock goes to 0](./bundles-v3-overview.md#when-the-reserve-goes-to-0).

Publishing a bundle that is already published keeps its original `published_at`; the date does not move. So an integration that sends `published: true` again without `products` does not fail when one of the products has stopped being sellable since. When the request also carries `products`, the bundle is checked again with those products. The update is saved, and the response reports that the bundle cannot be sold in `availability` when the request asks for `?include=availability`.

When the bundle cannot be sold, a `published: true` that checks it answers `422` under `published`, naming each reason with its [availability code](./bundles-v3-overview.md#availability), and **nothing in the request is saved**, including the other fields it carried:

```json
{
  "message": "The bundle cannot be published: some of its products cannot be sold in a bundle (ineligible_products).",
  "errors": {
    "published": [
      "The bundle cannot be published: some of its products cannot be sold in a bundle (ineligible_products)."
    ]
  }
}
```

A bundle saved as a single-product draft from the dashboard answers the same way:

```json
{
  "message": "The bundle cannot be published: it has fewer than 2 products (below_minimum_products).",
  "errors": {
    "published": [
      "The bundle cannot be published: it has fewer than 2 products (below_minimum_products)."
    ]
  }
}
```

To publish such a draft, send its complete `products` together with `published: true`.

### Example: Publish

```bash
curl -X PUT "https://yourstore.publica.la/api/v3/bundles/318" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "published": true, "private": false }'
```

---

## Reserved Stock {#reserved-stock}

Send `reserved_stock` to change the units the bundle holds for its buyers. How reserved stock works is described under [Reserved stock](./bundles-v3-overview.md#reserved-stock).

```bash
curl -X PUT "https://yourstore.publica.la/api/v3/bundles/318?include=products,detailed_stock" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "reserved_stock": 20 }'
```

Sending `0` is always allowed. Lowering `reserved_stock` to another value, or sending the same value again, is allowed while the bundle has a physical product. Raising it checks each physical product the bundle keeps. A rejected `reserved_stock` answers `422` under `reserved_stock`, with its code between parentheses, and nothing is saved:

| Code                   | When                                                                            | Error message                                                                                              |
| ---------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `no_physical_products` | The bundle, with the products it has after the request, has no physical product | `The bundle has no physical product with tracked stock to reserve stock from (no_physical_products).`      |
| `untracked_stock`      | A physical product the bundle keeps does not track its stock                    | `Rock Icons, Volume 1 does not track its stock, so it cannot hold reserved stock (untracked_stock).`       |
| `below_reserved_stock` | A physical product the bundle keeps has less available stock than the new value | `Rock Icons, Volume 1 can hold at most 30 units of reserved stock for this bundle (below_reserved_stock).` |

The `below_reserved_stock` message names the most the product can hold for this bundle: its total stock less the reserved stock of the other bundles. When several products fail, the error names one: a product that does not track its stock first, else the one with the least available stock.

```json
{
  "message": "Rock Icons, Volume 1 can hold at most 30 units of reserved stock for this bundle (below_reserved_stock).",
  "errors": {
    "reserved_stock": [
      "Rock Icons, Volume 1 can hold at most 30 units of reserved stock for this bundle (below_reserved_stock)."
    ]
  }
}
```

A physical product the same request adds to the bundle answers under `products.{position}` instead, with the [rejection reasons](./bundles-v3-overview.md#rejection-reasons) `untracked_stock` or `below_reserved_stock`. That also applies to a product added to a bundle that already reserves stock, with no `reserved_stock` in the request. A create checks every product this way, since all of them are new.

An update of `products` that leaves the bundle without physical products sets `reserved_stock` to `0`.

---

## Cover {#cover}

| You send                           | Result                                                                    |
| ---------------------------------- | ------------------------------------------------------------------------- |
| `cover_url` with a URL             | Downloads the image and replaces the bundle's cover                       |
| `cover` with a storage path        | Replaces the bundle's cover with that uploaded image                      |
| `cover_url: null` or `cover: null` | Drops the bundle's own cover: it goes back to the collage of its products |
| neither                            | Keeps the current cover                                                   |

The image rules are listed under [Covers](./bundles-v3-overview.md#covers).

```bash
curl -X PUT "https://yourstore.publica.la/api/v3/bundles/318" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "cover_url": null }'
```

---

## Request Examples

### Rename, Found by `external_id`

```bash
curl -X PUT "https://yourstore.publica.la/api/v3/bundles/rock-icons?id_type=external" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "name": "Rock Icons: The Complete Set" }'
```

### Change or Clear the `external_id`

```bash
curl -X PUT "https://yourstore.publica.la/api/v3/bundles/318" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "external_id": "rock-icons-2026" }'
```

Send `"external_id": null` to clear it.

### Unpublish

```bash
curl -X PUT "https://yourstore.publica.la/api/v3/bundles/318" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "published": false }'
```

---

## Response

`200 OK` with the updated [bundle object](./bundles-v3-overview.md#the-bundle-object) under `data`. It carries `products`, `prices`, `availability` and `detailed_stock` only when the request names them in `include`, computed after the update.

---

## Errors

| Status | Condition                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing or invalid `X-User-Token`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `403`  | Selling is disabled for the store (`Selling is disabled for this store.`), or the token does not belong to an administrator. A token without access gets `403` even for a bundle that does not exist                                                                                                                                                                                                                                                                                                                                  |
| `404`  | The bundle does not exist in your store, checked before the payload is validated                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `422`  | Validation failure: `name` sent as `null`, fewer than 2 or more than 100 products, a product repeated, not found, ambiguous or not sellable, `published: true` on a bundle that cannot be sold, a `reserved_stock` the products cannot hold, `published_at` sent, an `external_id` used by another bundle, `cover` and `cover_url` together, a cover that cannot be downloaded or is not a supported image, or an `include` other than `products`, `prices`, `availability` or `detailed_stock` or sent as an array. Nothing is saved |
| `429`  | Rate limit exceeded (20 requests per minute per token, shared with create and with the create and update endpoints of the [Add-ons API](../add-ons/add-ons-v3-overview.md))                                                                                                                                                                                                                                                                                                                                                           |

---

## See Also

- [Bundles API Overview](./bundles-v3-overview.md): the bundle object, states, covers and retries
- [Create a Bundle](./bundles-v3-create.md): the product rules and their errors
- [Get a Bundle](./bundles-v3-get.md): read a bundle back
