Skip to main content

Create a Bundle

Create a bundle from 2 to 100 of your products, in a single request.


Endpoint​

POST /api/v3/bundles

Request Body​

FieldTypeRequiredDescription
namestringYesMax 255 characters
productsarrayYesProduct IDs, in the order the bundle shows them. From 2 to 100, each once. See Products
products_id_typestringNointernal (default): products holds product IDs. external: it holds product identifiers. See Products by identifier
descriptionstringNoMax 2,000 characters
slugstringNoSlug of the bundle page, max 255 characters. Generated from name when omitted
external_idstringNoYour identifier for the bundle, max 64 characters, unique in your store. See Retries
publishedbooleanNotrue publishes the bundle in the same request. See Publishing on create. Defaults to unpublished
privatebooleanNoDefaults to false. See States
cover_urlstringNoPublic URL of the cover image to download. Not accepted together with cover. See Covers
coverstringNoStorage path of an uploaded cover image. Not accepted together with cover_url. See Covers

Add ?fields= to the URL to trim the response to the fields you need, as on get.

published_at is not accepted

The platform sets published_at when the bundle is published. Sending it returns 422, also when its value is null:

{
"message": "The system sets published_at. Send published: true to publish the bundle.",
"errors": {
"published_at": [
"The system sets published_at. Send published: true to publish the bundle."
]
}
}

Products​

Every create carries the whole composition: from 2 to 100 products, each one once, all of them products your store can sell. This holds for an unpublished bundle too. The API takes a bundle complete, in one request, so there is no draft with fewer products to fill in later: a create with a single product answers 422 under products, published or not.

The dashboard is different

The dashboard lets an administrator save a draft bundle with a single product and add the rest later. That draft is a dashboard feature only; the API does not create one.

  • IDs: the product id of the Content API, sent as numbers or as numeric strings. To send identifiers such as ISBNs instead, see Products by identifier.
  • Order: the order of the array is the order the bundle shows its products. The first one gets position 0.
  • Sellable products: each product must be one your store can sell in a bundle: on sale, not free, not for subscribers only, in stock, and with a price of its own in a sale currency of your store. Each rejection reason maps to one of these rules.

Errors by product​

A product that fails is named by its position in the array, under products.{position}, with its ID and its rejection code between parentheses. Every failing product gets its own entry:

{
"message": "Product [9999] was not found among the products this store can sell (unresolved). (and 1 more error)",
"errors": {
"products.1": [
"Product [9999] was not found among the products this store can sell (unresolved)."
],
"products.2": [
"Product [1203] cannot be sold in a bundle: Not on sale (not_on_sale)."
]
}
}

A repeated product is named at every position it holds:

{
"message": "Product [1201] appears more than once (duplicate). (and 1 more error)",
"errors": {
"products.0": ["Product [1201] appears more than once (duplicate)."],
"products.2": ["Product [1201] appears more than once (duplicate)."]
}
}

The codes and their messages are listed under Rejection reasons.

Products by identifier​

Send products_id_type: external to list the products by identifier instead of by ID. Each value is matched against the identifiers of your store's products, such as the ISBN or the external_id. Identifiers are compared ignoring case and any character that is not a letter or a digit, so 978-950-123-456-7 matches 9789501234567.

curl -X POST "https://yourstore.publica.la/api/v3/bundles" \
-H "X-User-Token: your-api-token" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Rock Icons",
"products_id_type": "external",
"products": ["9789501234567", "ROCK-VOL-2"]
}'

In this mode every value of products is read as an identifier, never as a product ID. Responses still list the products by their id, in the order you sent them.

The product rules do not change, and every error names the product by the identifier you sent. Two cases are specific to identifiers:

  • An identifier that matches more than one product of your store answers ambiguous. Send a more specific identifier instead, such as the product's external_id.
  • Two identifiers of the same product, such as its ISBN with and without hyphens, are a duplicate.
{
"message": "Product [EXT-404] was not found among the products this store can sell (unresolved). (and 3 more errors)",
"errors": {
"products.0": [
"Product [EXT-404] was not found among the products this store can sell (unresolved)."
],
"products.1": [
"Product [9789509876543] matches more than one product this store can sell (ambiguous)."
],
"products.2": [
"Product [9789501234567] appears more than once (duplicate)."
],
"products.3": [
"Product [978-950-123-456-7] appears more than once (duplicate)."
]
}
}

ambiguous only appears in these errors. A bundle's products[].rejection_reason never carries it, because a saved bundle always holds product IDs.

Errors come in rounds​

The products are checked in three rounds, and each round runs only when the previous one passed:

  1. Shape: products is a list of 2 to 100 entries with no duplicates, made of integer IDs or, by identifier, of identifiers that each match one product. Errors land under products or products.{position}.
  2. Each product: every product is one your store can sell in a bundle. Errors land under products.{position}.
  3. The bundle: when the request sends published: true, the bundle as a whole can be sold. Errors land under published.

So a 422 that names a product says nothing about the publication yet. Fix what it names and the next attempt can report a later round. Errors on other fields, such as name or external_id, are reported alongside, in the same 422.


Publishing on Create​

published: true creates the bundle already published, as long as it can be sold. When it cannot, the request answers 422 under published, naming each reason with its availability code, and nothing is saved:

{
"message": "The bundle cannot be published: its products share no sale currency (no_shared_currency).",
"errors": {
"published": [
"The bundle cannot be published: its products share no sale currency (no_shared_currency)."
]
}
}

Without published, the same bundle is created unpublished (201), and its availability.reasons says why it cannot be sold yet. Fix its products and publish it later with an update.


Request Examples​

Minimal​

curl -X POST "https://yourstore.publica.la/api/v3/bundles" \
-H "X-User-Token: your-api-token" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Rock Icons",
"products": [1201, 1202]
}'

The bundle is created unpublished, not private, with the slug rock-icons and the collage of its products as cover.

Published, With Your Identifier and a Cover​

curl -X POST "https://yourstore.publica.la/api/v3/bundles" \
-H "X-User-Token: your-api-token" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Rock Icons",
"description": "The classics, together",
"external_id": "rock-icons",
"products": [1201, 1202, 1205],
"cover_url": "https://images.example.com/rock-icons.png",
"published": true
}'

Response​

201 Created with the bundle object under data. A create returns its products, prices and availability with no include needed. With ?fields=, name them in fields to keep them:

{
"data": {
"id": "318",
"external_id": "rock-icons",
"name": "Rock Icons",
"slug": "rock-icons",
"description": "The classics, together",
"cover_url": "https://assets.publica.la/yourstore/bundles/cover_1791295500_k3x9qz2a.png",
"private": false,
"published": true,
"published_at": "2026-10-06T14:05:00.000000Z",
"product_url": "https://yourstore.publica.la/library/bundle/rock-icons/318?signature=9f2c4e7a…",
"buy_button_url": "https://yourstore.publica.la/library/bundle/rock-icons/318?signature=9f2c4e7a…&automatically_open_checkout=1",
"created_at": "2026-10-06T14:05:00.000000Z",
"updated_at": "2026-10-06T14:05:00.000000Z",
"products": [
{
"id": "1201",
"position": 0,
"name": "Rock Icons, Volume 1",
"available": true,
"rejection_reason": null
},
{
"id": "1202",
"position": 1,
"name": "Rock Icons, Volume 2",
"available": true,
"rejection_reason": null
},
{
"id": "1205",
"position": 2,
"name": "Rock Icons, Volume 3",
"available": true,
"rejection_reason": null
}
],
"prices": [{ "currency_id": "USD", "amount": 22.5 }],
"availability": { "available": true, "reasons": [] }
}
}

Errors​

StatusCondition
401Missing or invalid X-User-Token
403Selling is disabled for the store (Selling is disabled for this store.), or the token does not belong to an administrator
422Validation failure: name or products missing, fewer than 2 or more than 100 products, products not a list, a product repeated, not found, ambiguous or not sellable in a bundle, published: true on a bundle that cannot be sold, published_at sent, an external_id already in use, cover and cover_url together, or a cover that cannot be downloaded or is not a supported image
429Rate limit exceeded (20 requests per minute per token for create and update, since each may download a cover)

See Also​

X

Graph View