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

# REST API

> Use the Appfox REST API to list, create, update, and delete Shopify bundles, and to read bundle orders and analytics. FBT bundles are not supported yet.

The REST API gives you the same control over your bundles as the dashboard. [Create an API key](/product-bundles/docs/product-bundles/docs/settings/api-and-mcp) first, then send it with every request:

```bash theme={null}
curl "<your-app-url>/api/v1/bundles" \
  -H "Authorization: Bearer <your-api-key>"
```

`<your-app-url>` is shown on **Settings** → **API & MCP**. All requests and responses are JSON. Errors look like this:

```json theme={null}
{ "error": { "message": "Bundle not found" } }
```

## Endpoints

| Method and path | Access | Description |
| - | - | - |
| `GET /api/v1/bundles` | Read | List bundles. Filter with `status` (`ACTIVE` or `PAUSED`), page with `page` and `limit` (max 100). |
| `GET /api/v1/bundles/{id}` | Read | Get one bundle with its products and offers. |
| `POST /api/v1/bundles` | Write | Create a bundle. |
| `PATCH /api/v1/bundles/{id}` | Write | Update the fields you send. `products` and `offers` replace the whole list when included. |
| `DELETE /api/v1/bundles/{id}` | Write | Delete a bundle. Safe to repeat. |
| `GET /api/v1/orders` | Read | List orders that contained a bundle. Filter with `bundleId`. Customer details aren't included. |
| `GET /api/v1/analytics` | Read | Use `range=7d`, `30d` or `90d`, or `range=custom&start=YYYY-MM-DD&end=YYYY-MM-DD`. |

## List bundles

```bash theme={null}
curl "<your-app-url>/api/v1/bundles?status=ACTIVE&limit=2" \
  -H "Authorization: Bearer <your-api-key>"
```

```json theme={null}
{
  "bundles": [
    {
      "id": 42,
      "name": "Skincare duo",
      "title": "Buy both and save",
      "bundleType": "FIXED",
      "status": "ACTIVE",
      "products": [
        { "productId": "gid://shopify/Product/1234567890", "title": "Cleanser", "handle": "cleanser", "quantity": 1, "isPrimary": true, "defaultVariantId": null },
        { "productId": "gid://shopify/Product/2345678901", "title": "Moisturizer", "handle": "moisturizer", "quantity": 1, "isPrimary": false, "defaultVariantId": null }
      ],
      "offers": [],
      "startsAt": null,
      "endsAt": null,
      "createdAt": "2026-10-01T09:30:00.000Z",
      "updatedAt": "2026-10-08T14:12:00.000Z"
    }
  ],
  "page": 1,
  "limit": 2,
  "total": 1
}
```

Each bundle also includes its display and targeting settings (`buttonText`, `badgeText`, `minProducts`, `maxProducts`, `targetType`, `combinesWith`, `customerAudience`, `excludeB2b`, `customerTagsInclude`, `customerTagsExclude`, `widgetOnlyDiscount` and more). Bundles created from the dashboard may include more settings than the API accepts.

## Create a bundle

You supply only the Shopify product IDs. The product title, handle, image and variants are loaded from your store.

```bash theme={null}
curl -X POST "<your-app-url>/api/v1/bundles" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Skincare duo",
    "title": "Buy both and save",
    "bundleType": "FIXED",
    "status": "PAUSED",
    "products": [
      { "productId": "gid://shopify/Product/1234567890", "quantity": 1, "isPrimary": true },
      { "productId": "gid://shopify/Product/2345678901", "quantity": 1 }
    ],
    "offers": [
      { "title": "Both", "subTitle": "", "tag": "", "label": "", "selected": true, "order": 0, "discountType": "PERCENTAGE", "quantity": 1, "value": 10 }
    ]
  }'
```

The response is `201` with the created bundle:

```json theme={null}
{
  "bundle": {
    "id": 43,
    "name": "Skincare duo",
    "title": "Buy both and save",
    "bundleType": "FIXED",
    "status": "PAUSED",
    "products": [
      { "productId": "gid://shopify/Product/1234567890", "title": "Cleanser", "handle": "cleanser", "quantity": 1, "isPrimary": true, "defaultVariantId": null },
      { "productId": "gid://shopify/Product/2345678901", "title": "Moisturizer", "handle": "moisturizer", "quantity": 1, "isPrimary": false, "defaultVariantId": null }
    ],
    "offers": [
      { "title": "Both", "subTitle": "", "tag": "", "label": "", "selected": true, "order": 0, "discountType": "PERCENTAGE", "quantity": 1, "value": 10, "imageUrl": null }
    ],
    "createdAt": "2026-10-09T10:00:00.000Z",
    "updatedAt": "2026-10-09T10:00:00.000Z"
  }
}
```

Bundle rules match the dashboard:

* **Fixed** bundles need at least two products.
* **BOGO** bundles need at least one primary product (`isPrimary: true`) and one other product.
* **Volume** bundles need a title on every offer, and either products or `targetType` of `ALL_PRODUCTS` or `COLLECTIONS` with `targetCollectionIds`.
* **Mix & Match** bundles need at least one product or `collectionIds`.
* `bundleType` can't be changed after creation. Frequently bought together (FBT) bundles can't be created or changed through the API yet.

`discountType` is one of `PERCENTAGE`, `FLAT`, `FIXED_PER_ITEM`, `FIXED_PRICE` or `FREE_GIFT`.

## Update a bundle

Send only what should change. This publishes a bundle:

```bash theme={null}
curl -X PATCH "<your-app-url>/api/v1/bundles/43" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{ "status": "ACTIVE" }'
```

The response is `200` with the updated bundle in the same shape as above.

## Delete a bundle

```bash theme={null}
curl -X DELETE "<your-app-url>/api/v1/bundles/43" \
  -H "Authorization: Bearer <your-api-key>"
```

```json theme={null}
{ "id": 43, "deleted": true }
```

If the request returns `502`, the bundle was deleted but Shopify wasn't updated. Send the same request again to finish the sync.

## List orders

```bash theme={null}
curl "<your-app-url>/api/v1/orders?bundleId=42&limit=2" \
  -H "Authorization: Bearer <your-api-key>"
```

```json theme={null}
{
  "orders": [
    {
      "orderId": "gid://shopify/Order/9001",
      "orderNumber": "#1001",
      "bundleId": 42,
      "bundleName": "Skincare duo",
      "orderTotal": "126.00",
      "bundleTotal": "126.00",
      "bundleDiscount": "14.00",
      "purchasedAt": "2026-10-07T18:20:00.000Z",
      "createdAt": "2026-10-07T18:20:05.000Z"
    }
  ],
  "page": 1,
  "limit": 2,
  "total": 1
}
```

## Analytics

```bash theme={null}
curl "<your-app-url>/api/v1/analytics?range=7d" \
  -H "Authorization: Bearer <your-api-key>"
```

The response has a `range` (`start` and `end` as ISO timestamps), a `summary` for the whole shop, a `bundles` array with views, add-to-carts, orders, revenue, discount, conversion rate and ROI per bundle, and a daily `series` of revenue and orders.

## Error codes

| Status | Meaning |
| - | - |
| `400` | The request is invalid. The message says what to fix, for example a missing field or a product that doesn't exist in your store. |
| `401` | The key is missing, wrong or revoked. |
| `403` | The key is read only. Create a key with write access. |
| `404` | The bundle doesn't exist in your shop. |
| `405` | The method isn't supported on this path. |
| `409` | The bundle changed while saving (retry), or the app isn't installed or needs to be reopened in the Shopify admin. |
| `422` | Publishing would exceed Shopify's 10KB bundle data limit. Target collections, use fewer products, or create the bundle as `PAUSED`. |
| `502` | The change was saved but syncing with Shopify failed. Repeat the request, or open the app and save the bundle again. |

<Tip>Need a field or endpoint that isn't here? Contact us at [support@getappfox.com](mailto:support@getappfox.com).</Tip>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.