Documentation

Bundles Admin API

The Bundles Admin API lets you manage bundles programmatically from your own scripts and systems: sync bundles from a PIM, build seasonal bundles in bulk, or wire bundle creation into your existing catalog tooling. It covers the same operations as the app's bundle editor, so anything you create here behaves exactly like a bundle built in the app.

This API manages bundle configuration. To add bundles to the cart on your storefront, see Cart API Integration.

Base URL

https://api.flexbundles.com/v1

Authentication

Every request needs an API key sent as a bearer token:

Authorization: Bearer fxb_your_api_key

Generate your key from the Settings page in the Flex Bundles app. The key is shown once at generation time and only a hash is stored, so copy it somewhere safe. Regenerating or revoking a key stops the old key working immediately.

Keep the key server-side. It grants full bundle management for your store, so never ship it in theme code or client-side JavaScript.

Making Requests

The examples in this guide show cURL, Node.js, and Ruby. The Node.js and Ruby examples use these small helpers, which need nothing beyond the standard runtime (Node.js 18+, any recent Ruby):

const API = "https://api.flexbundles.com/v1";
const KEY = process.env.FLEX_BUNDLES_API_KEY;

async function flexBundles(path, method = "GET", body) {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: {
      "Authorization": `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${res.status}: ${json.error}`);
  return json;
}
require "net/http"
require "json"

API = "https://api.flexbundles.com/v1"
KEY = ENV.fetch("FLEX_BUNDLES_API_KEY")

def flex_bundles(path, method: :get, body: nil)
  uri = URI("#{API}#{path}")
  request = Net::HTTP.const_get(method.capitalize).new(uri)
  request["Authorization"] = "Bearer #{KEY}"
  request["Content-Type"] = "application/json"
  request.body = body.to_json if body

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  json = JSON.parse(response.body)
  raise "#{response.code}: #{json["error"]}" unless response.is_a?(Net::HTTPSuccess)
  json
end

Endpoints

Method Path Description
GET /v1/flex-bundles List flex bundles
POST /v1/flex-bundles Create a flex bundle
GET /v1/flex-bundles/{id} Get one flex bundle
PUT or PATCH /v1/flex-bundles/{id} Update a flex bundle
DELETE /v1/flex-bundles/{id} Delete a flex bundle
GET /v1/fixed-bundles List fixed bundles
POST /v1/fixed-bundles Create a fixed bundle
GET /v1/fixed-bundles/{id} Get one fixed bundle
PUT or PATCH /v1/fixed-bundles/{id} Update a fixed bundle
DELETE /v1/fixed-bundles/{id} Delete a fixed bundle

All request and response bodies are JSON. IDs (bundle, product, variant) are numeric Shopify IDs without the gid:// prefix.


Flex Bundles

Flex bundles attach to a product and let customers build their own bundle on your storefront. See Bundle Types for how they work.

Create a flex bundle

curl -X POST https://api.flexbundles.com/v1/flex-bundles \
  -H "Authorization: Bearer fxb_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "10309675680045",
    "security": { "minimum_price": 25 },
    "settings": { "title": "Build Your Own Kit" }
  }'
const { bundle } = await flexBundles("/flex-bundles", "POST", {
  product_id: "10309675680045",
  security: { minimum_price: 25 },
  settings: { title: "Build Your Own Kit" },
});
result = flex_bundles("/flex-bundles", method: :post, body: {
  product_id: "10309675680045",
  security: { minimum_price: 25 },
  settings: { title: "Build Your Own Kit" },
})
bundle = result["bundle"]
Field Type Required Description
product_id string or number Yes The parent product. One flex bundle per product.
active boolean No Defaults to true.
security.minimum_price number No Reject carts where the bundle price is below this. Defaults to 0 (off).
security.minimum_component_price number No Reject carts where any component price is below this. Defaults to 0 (off).
settings object No Optional presentation settings (title, image, walkthrough_url).

Response is 201 Created with the full bundle. The id is generated by the API:

{
  "bundle": {
    "id": 1783440131288415,
    "active": true,
    "bundle_type": "flex",
    "parent": { "product_id": 10309675680045 },
    "security": { "minimum_price": 25, "minimum_component_price": 0 },
    "settings": { "title": "Build Your Own Kit" }
  }
}

Update a flex bundle

PUT and PATCH behave identically: send only the fields you want to change.

curl -X PATCH https://api.flexbundles.com/v1/flex-bundles/1783440131288415 \
  -H "Authorization: Bearer fxb_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'
await flexBundles("/flex-bundles/1783440131288415", "PATCH", { active: false });
flex_bundles("/flex-bundles/1783440131288415", method: :patch, body: { active: false })

product_id cannot be changed on a flex bundle. Delete the bundle and create a new one on the other product instead.


Fixed Bundles

Fixed bundles attach to a specific variant with a pre-set component list. Adding the parent variant to the cart automatically includes the components.

Create a fixed bundle

curl -X POST https://api.flexbundles.com/v1/fixed-bundles \
  -H "Authorization: Bearer fxb_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "10313045967149",
    "variant_id": "52212525334829",
    "components": [
      { "product_id": "10309675221293", "variant_id": "52199500808493", "quantity": 2 },
      { "product_id": "10309675286829", "variant_id": "52199500972333", "quantity": 1 }
    ]
  }'
const { bundle } = await flexBundles("/fixed-bundles", "POST", {
  product_id: "10313045967149",
  variant_id: "52212525334829",
  components: [
    { product_id: "10309675221293", variant_id: "52199500808493", quantity: 2 },
    { product_id: "10309675286829", variant_id: "52199500972333", quantity: 1 },
  ],
});
result = flex_bundles("/fixed-bundles", method: :post, body: {
  product_id: "10313045967149",
  variant_id: "52212525334829",
  components: [
    { product_id: "10309675221293", variant_id: "52199500808493", quantity: 2 },
    { product_id: "10309675286829", variant_id: "52199500972333", quantity: 1 },
  ],
})
bundle = result["bundle"]
Field Type Required Description
product_id string or number Yes The parent product.
variant_id string or number Yes The parent variant. Must belong to product_id. One fixed bundle per variant.
components array Yes At least one component (see below).
active boolean No Defaults to true.
settings object No Optional presentation settings (title, image, walkthrough_url).

Each component:

Field Type Required Description
variant_id string or number Yes The component variant. Use 0 to match any variant of product_id.
product_id string or number No The component's product. Recommended.
quantity integer No Defaults to 1. Must be at least 1.
attributes object No Extra line item properties stamped on this component in the cart.

Response is 201 Created with the full bundle.

Note: saving a fixed bundle also syncs inventory behavior on the parent variant. If every component is in stock the parent variant's inventory tracking is turned off (it is a virtual item holding no stock of its own); if any component is out of stock the parent is set out of stock so the bundle cannot be oversold.

Update a fixed bundle

Send only what changes. components, when provided, replaces the whole array.

curl -X PATCH https://api.flexbundles.com/v1/fixed-bundles/1783369970901365 \
  -H "Authorization: Bearer fxb_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "components": [
      { "product_id": "10309675221293", "variant_id": "52199500808493", "quantity": 3 }
    ]
  }'
await flexBundles("/fixed-bundles/1783369970901365", "PATCH", {
  components: [
    { product_id: "10309675221293", variant_id: "52199500808493", quantity: 3 },
  ],
});
flex_bundles("/fixed-bundles/1783369970901365", method: :patch, body: {
  components: [
    { product_id: "10309675221293", variant_id: "52199500808493", quantity: 3 },
  ],
})

A fixed bundle's parent cannot be changed: product_id and variant_id are fixed at creation. To move a bundle to a different variant, delete it and create a new one.


Listing and Reading

curl https://api.flexbundles.com/v1/flex-bundles \
  -H "Authorization: Bearer fxb_your_api_key"
const { bundles } = await flexBundles("/flex-bundles");
bundles = flex_bundles("/flex-bundles")["bundles"]

GET /v1/flex-bundles and GET /v1/fixed-bundles return lightweight summaries:

{
  "bundles": [
    {
      "id": 1783369970901365,
      "product_id": 10309675286829,
      "variant_id": 52199500972333,
      "active": true,
      "created_at": "2026-07-02T18:20:11.000Z",
      "updated_at": "2026-07-08T09:14:32.000Z"
    }
  ]
}

variant_id appears on fixed bundles only. Fetch GET /v1/{type}-bundles/{id} for the full configuration, which returns the same shape as create and update responses plus created_at and updated_at.

Deleting

Delete a flex bundle:

curl -X DELETE https://api.flexbundles.com/v1/flex-bundles/1783440131288415 \
  -H "Authorization: Bearer fxb_your_api_key"
await flexBundles("/flex-bundles/1783440131288415", "DELETE");
flex_bundles("/flex-bundles/1783440131288415", method: :delete)

Delete a fixed bundle:

curl -X DELETE https://api.flexbundles.com/v1/fixed-bundles/1783369970901365 \
  -H "Authorization: Bearer fxb_your_api_key"
await flexBundles("/fixed-bundles/1783369970901365", "DELETE");
flex_bundles("/fixed-bundles/1783369970901365", method: :delete)

Returns { "deleted": true, "id": 1783369970901365 }. Deleting removes the bundle configuration and, for fixed bundles, restores the parent variant's inventory tracking and cleans up component references. Orders already placed are unaffected.

Errors

Errors return a JSON body with a single error message:

{ "error": "This product already has a flex bundle. Edit the existing bundle instead of creating a new one." }
Status Meaning
400 Invalid input: malformed JSON, non-numeric ID, bad field type or value.
401 Missing or invalid API key, or the app's access to your store needs re-authorizing (open the app in Shopify admin, then retry).
402 An active Flex Bundles subscription is required.
404 No bundle with that ID (or it is a different bundle type).
405 HTTP method not supported on that path.
409 The product or variant already has a bundle of that type.
422 The product or variant does not exist, the variant does not belong to the product, or you tried to change a bundle's parent (product_id or variant_id).

Good to Know

  • Bundles created through the API appear in the Flex Bundles app immediately, and vice versa. There is one source of truth.
  • Send write requests sequentially rather than in parallel. Concurrent writes from multiple sources can race on the bundle registry.
  • There is one API key per store. Rotate it any time from the app; the old key stops working the moment a new one is generated.