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

# Products

> Manage products and SKUs

## List All Products

Retrieve all products for a project with pagination.

<ParamField path="brand" type="string" required>
  The brand/project ID
</ParamField>

<ParamField query="page" type="number">
  Page number for pagination (default: 1)
</ParamField>

<ParamField query="per_page" type="number">
  Number of results per page (default: 25)
</ParamField>

```bash Request theme={null}
GET /api/brands/{brand}/products
```

```bash cURL theme={null}
curl https://app.elasticfunnels.io/api/brands/{brand_id}/products \
  -H "EF-Access-Key: your_api_key_here"
```

<ResponseExample>
  ```json Response theme={null}
  {
    "current_page": 1,
    "data": [
      {
        "id": 2,
        "brand_id": 1,
        "code": "glucotil",
        "type": "physical",
        "title": "1 Bottle",
        "checkout_title": null,
        "price": 69,
        "retail_price": 99,
        "image": "https://storage.googleapis.com/asper-digital/1/1-bottle.png",
        "image_link": "https://storage.googleapis.com/asper-digital/1/1-bottle.png",
        "units": 1,
        "sku": "prod1",
        "classification": "main",
        "product_files": [],
        "unit_cost": null,
        "unit_weight_oz": null,
        "fulfillment_cost": null,
        "packaging_cost": null,
        "fuel_surcharge_cost": null,
        "cogs": null,
        "custom_attributes": null,
        "merchant_product_ids": [],
        "physical_units_per_product": null,
        "fulfillment_multiply_sku": 0,
        "sku_category": null,
        "sku_subcategory": null,
        "product_specifications": null,
        "shipping_profile_id": null,
        "is_subscription": false,
        "subscription_frequency": null,
        "subscription_frequency_unit": null,
        "subscription_trial_days": null,
        "subscription_first_charge_free": false,
        "average_shipping_cost": null,
        "bonuses": []
      }
    ],
    "first_page_url": "https://app.elasticfunnels.io/api/brands/1/products?page=1",
    "from": 1,
    "last_page": 1,
    "last_page_url": "https://app.elasticfunnels.io/api/brands/1/products?page=1",
    "links": [
      {
        "url": null,
        "label": "&laquo; Previous",
        "active": false
      },
      {
        "url": "https://app.elasticfunnels.io/api/brands/1/products?page=1",
        "label": "1",
        "active": true
      },
      {
        "url": null,
        "label": "Next &raquo;",
        "active": false
      }
    ],
    "next_page_url": null,
    "path": "https://app.elasticfunnels.io/api/brands/1/products",
    "per_page": 25,
    "prev_page_url": null,
    "to": 8,
    "total": 8
  }
  ```
</ResponseExample>

***

## List All Products (Unpaginated)

Get all products without pagination for dropdown lists.

<ParamField path="brand" type="string" required>
  The brand/project ID
</ParamField>

```bash Request theme={null}
GET /api/brands/{brand}/products/all
```

```bash cURL theme={null}
curl https://app.elasticfunnels.io/api/brands/{brand_id}/products/all \
  -H "EF-Access-Key: your_api_key_here"
```

***

## Get Product Details

Retrieve details about a specific product.

<ParamField path="brand" type="string" required>
  The brand/project ID
</ParamField>

<ParamField path="product" type="string" required>
  The product ID
</ParamField>

```bash Request theme={null}
GET /api/brands/{brand}/products/{product}
```

```bash cURL theme={null}
curl https://app.elasticfunnels.io/api/brands/{brand_id}/products/2 \
  -H "EF-Access-Key: your_api_key_here"
```

<ResponseExample>
  ```json Response theme={null}
  {
    "id": 2,
    "brand_id": 1,
    "code": "glucotil",
    "type": "physical",
    "title": "1 Bottle",
    "checkout_title": null,
    "price": 69,
    "retail_price": 99,
    "image": "https://storage.googleapis.com/asper-digital/1/1-bottle.png",
    "image_link": "https://storage.googleapis.com/asper-digital/1/1-bottle.png",
    "units": 1,
    "sku": "prod1",
    "classification": "main",
    "product_files": [],
    "unit_cost": null,
    "unit_weight_oz": null,
    "fulfillment_cost": null,
    "packaging_cost": null,
    "fuel_surcharge_cost": null,
    "cogs": null,
    "custom_attributes": null,
    "merchant_product_ids": {},
    "physical_units_per_product": null,
    "fulfillment_multiply_sku": 0,
    "sku_category": null,
    "sku_subcategory": null,
    "product_specifications": null,
    "shipping_profile_id": null,
    "is_subscription": false,
    "subscription_frequency": null,
    "subscription_frequency_unit": null,
    "subscription_trial_days": null,
    "subscription_first_charge_free": false,
    "average_shipping_cost": null,
    "bonuses": []
  }
  ```
</ResponseExample>

***

## Product Fields Reference

<AccordionGroup>
  <Accordion title="Basic Information">
    * `code` - Unique product code/identifier
    * `title` - Product display name
    * `checkout_title` - Alternative title for checkout (optional)
    * `type` - Product type: `physical`, `digital`
    * `classification` - Product classification: `main`, `upsell`, `downsell`, `bump`, `bonus`
  </Accordion>

  <Accordion title="Pricing">
    * `price` - Selling price (in currency units)
    * `retail_price` - Suggested retail/original price
    * `units` - Number of units included
  </Accordion>

  <Accordion title="Inventory & Fulfillment">
    * `sku` - Stock Keeping Unit identifier
    * `unit_cost` - Cost per unit
    * `unit_weight_oz` - Weight per unit in ounces
    * `fulfillment_cost` - Cost to fulfill the order
    * `packaging_cost` - Packaging cost
    * `fuel_surcharge_cost` - Fuel surcharge
    * `cogs` - Cost of Goods Sold
    * `physical_units_per_product` - Physical units per product
    * `fulfillment_multiply_sku` - Multiply fulfillment by SKU quantity
    * `average_shipping_cost` - Average shipping cost
  </Accordion>

  <Accordion title="Media">
    * `image` - Product image URL
    * `image_link` - Full URL to product image
    * `product_files` - Array of downloadable files attached to the product
  </Accordion>

  <Accordion title="Subscription Products">
    * `is_subscription` - Whether this is a subscription product
    * `subscription_frequency` - Billing frequency (e.g., 1, 2, 3)
    * `subscription_frequency_unit` - Unit: `day`, `week`, `month`, `year`
    * `subscription_trial_days` - Number of trial days
    * `subscription_first_charge_free` - Whether first charge is free
  </Accordion>

  <Accordion title="Additional">
    * `custom_attributes` - Array of custom key-value pairs
    * `merchant_product_ids` - Object mapping merchant names to product IDs
    * `product_specifications` - Array of product specifications
    * `shipping_profile_id` - Associated shipping profile
    * `sku_category` - SKU category
    * `sku_subcategory` - SKU subcategory
    * `bonuses` - Array of bonus products associated with this product
  </Accordion>
</AccordionGroup>

***

## Create Product

Create a new product.

<ParamField path="brand" type="string" required>
  The brand/project ID
</ParamField>

<ParamField body="code" type="string" required>
  Unique product code
</ParamField>

<ParamField body="title" type="string" required>
  Product title
</ParamField>

<ParamField body="price" type="number" required>
  Product price
</ParamField>

<ParamField body="type" type="string" required>
  Product type: `physical` or `digital`
</ParamField>

<ParamField body="classification" type="string" required>
  Classification: `main`, `upsell`, `downsell`, `bump`, or `bonus`
</ParamField>

<ParamField body="retail_price" type="number">
  Retail/original price
</ParamField>

<ParamField body="units" type="number">
  Number of units
</ParamField>

<ParamField body="sku" type="string">
  Stock Keeping Unit
</ParamField>

<ParamField body="image" type="file">
  Product image file (`multipart/form-data` only). Must be an uploaded image file — **not** a JSON URL string. Omit from JSON-only creates; upload via [File Manager](/api-reference/endpoints/file-manager#upload-file) and reference the CDN URL in HTML, or send `image` on a multipart create request.
</ParamField>

<ParamField body="is_subscription" type="boolean">
  Whether this is a subscription product
</ParamField>

<ParamField body="subscription_frequency" type="number">
  Billing frequency (required if is\_subscription is true)
</ParamField>

<ParamField body="subscription_frequency_unit" type="string">
  Frequency unit: `day`, `week`, `month`, `year`
</ParamField>

```bash Request theme={null}
POST /api/brands/{brand}/products
```

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/products \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "product_123",
    "title": "Premium Product",
    "price": 49.99,
    "retail_price": 79.99,
    "type": "physical",
    "classification": "main",
    "units": 1,
    "sku": "PREM-001"
  }'
```

### Subscription Product Example

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/products \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "monthly_subscription",
    "title": "Monthly Subscription",
    "price": 29.99,
    "type": "physical",
    "classification": "main",
    "is_subscription": true,
    "subscription_frequency": 1,
    "subscription_frequency_unit": "month",
    "subscription_trial_days": 14,
    "subscription_first_charge_free": true
  }'
```

### Create with image (multipart)

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/products \
  -H "EF-Access-Key: your_api_key_here" \
  -F "code=product_123" \
  -F "title=Premium Product" \
  -F "price=49.99" \
  -F "type=physical" \
  -F "classification=main" \
  -F "image=@/path/to/bottle.png"
```

<Note>
  Sending `"image": "https://..."` in a JSON body fails validation (`422`) or is ignored — the API expects a file upload for the `image` field.
</Note>

***

## Update Product

Update an existing product. Authenticate with `EF-Access-Key` on every request.

<Note>
  Both **PUT** and **POST** are supported for the same update handler. If PUT returns a redirect to login, use **POST** with the same URL and body.
</Note>

<ParamField path="brand" type="string" required>
  The brand/project ID
</ParamField>

<ParamField path="product" type="string" required>
  The product ID
</ParamField>

<ParamField body="title" type="string">
  Product title (use `title`, not `name`)
</ParamField>

<ParamField body="price" type="number">
  Product price
</ParamField>

<ParamField body="image" type="file">
  Replace product image (`multipart/form-data` file upload, same rules as create)
</ParamField>

```bash Request theme={null}
PUT /api/brands/{brand}/products/{product}
```

Alternative (same handler):

```
POST /api/brands/{brand}/products/{product}
```

```bash cURL (PUT) theme={null}
curl -X PUT https://app.elasticfunnels.io/api/brands/{brand_id}/products/{product_id} \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Product Title",
    "price": 59.99
  }'
```

```bash cURL (POST — preferred if PUT redirects) theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/products/{product_id} \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Product Title",
    "price": 59.99
  }'
```

<ResponseExample>
  ```json 200 Updated theme={null}
  {
    "id": 2,
    "title": "Updated Product Title",
    "code": "glucotil",
    "price": 59.99,
    "brand_id": 42
  }
  ```
</ResponseExample>

***

## Delete Product

Delete a product.

<ParamField path="brand" type="string" required>
  The brand/project ID
</ParamField>

<ParamField path="product" type="string" required>
  The product ID
</ParamField>

```bash Request theme={null}
DELETE /api/brands/{brand}/products/{product}
```

```bash cURL theme={null}
curl -X DELETE https://app.elasticfunnels.io/api/brands/{brand_id}/products/{product_id} \
  -H "EF-Access-Key: your_api_key_here"
```

***

## Clone Product

Create a copy of an existing product.

<ParamField path="brand" type="string" required>
  The brand/project ID
</ParamField>

<ParamField path="product" type="string" required>
  The product ID to clone
</ParamField>

```bash Request theme={null}
POST /api/brands/{brand}/products/{product}/clone
```

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/products/2/clone \
  -H "EF-Access-Key: your_api_key_here"
```

***

## Product Classifications

| Classification | Description        | Usage                          |
| -------------- | ------------------ | ------------------------------ |
| `main`         | Main product offer | Primary product in funnel      |
| `upsell`       | Upsell product     | Offered after initial purchase |
| `downsell`     | Downsell product   | Offered if upsell is declined  |
| `bump`         | Order bump         | Checkbox add-on at checkout    |
| `bonus`        | Bonus product      | Free gift or bonus item        |

***

## Product Bonuses

Products can have associated bonus products. The `bonuses` array contains bonus product relationships:

```json theme={null}
{
  "bonuses": [
    {
      "bonus_product_id": 23,
      "product_id": 3,
      "brand_id": 1
    },
    {
      "bonus_product_id": 24,
      "product_id": 3,
      "brand_id": 1
    }
  ]
}
```

When a customer purchases a product with bonuses, they automatically receive the bonus products.

***

## Downloadable Files

Digital downloads and bonus downloads are stored in `product_files`.

Each file row can include:

* `purpose` - `digital` or `bonus`
* `title` - Optional display title
* `description` - Optional description shown in customer-facing download UIs
* `image` - Optional thumbnail/cover image URL for this file
* `file` - File URL
* `kind` - Coarse classification derived from the file extension and `metadata.mime_type`. One of `audio`, `video`, `document`, `image`, `archive`, `file`. Lets clients branch on file type without re-parsing the URL.
* `is_primary` - Marks the default file for that purpose
* `sort_order` - Controls display order
* `metadata` - Public file metadata. Only the following fields are exposed:

  * `mime_type` - MIME type of the file (e.g. `"audio/mpeg"`, `"application/pdf"`)
  * `size` - Size in bytes
  * `checksum` - Optional checksum recorded at upload (may be `null`)
  * `extension` - Lowercase extension without the dot (e.g. `"pdf"`, `"mp3"`). Derived from the file URL when possible, with a small mime-type fallback for opaque/signed URLs. Empty string when neither source can produce one.

  Internal storage pointers (`provider`, `storage_path`, `brand_file_id`) are intentionally not returned.

```json theme={null}
{
  "product_files": [
    {
      "purpose": "digital",
      "title": "Quick Start Guide",
      "description": "PDF onboarding guide",
      "image": "https://cdn.example.com/guide-cover.jpg",
      "file": "https://cdn.example.com/guide.pdf",
      "kind": "document",
      "is_primary": true,
      "sort_order": 0,
      "metadata": {
        "mime_type": "application/pdf",
        "size": 182734,
        "checksum": null,
        "extension": "pdf"
      }
    },
    {
      "purpose": "bonus",
      "title": "Bonus Audio",
      "file": "https://cdn.example.com/bonus.mp3",
      "kind": "audio",
      "is_primary": true,
      "sort_order": 0,
      "metadata": {
        "mime_type": "audio/mpeg",
        "size": 10876045,
        "checksum": null,
        "extension": "mp3"
      }
    }
  ]
}
```

Use bonus-product relationships for entitlement, and use `product_files` for the actual downloadable assets those products expose.

***

## Custom Attributes

Products support custom key-value attributes for flexibility:

```json theme={null}
{
  "custom_attributes": [
    {
      "key": "download",
      "value": "https://example.com/download"
    },
    {
      "key": "access_code",
      "value": "ABC123"
    }
  ]
}
```

Common uses:

* Download links for digital products
* Access codes for member areas
* Special instructions
* Custom tracking parameters

***

## Merchant Product IDs

Map your ElasticFunnels products to merchant platform product IDs:

```json theme={null}
{
  "merchant_product_ids": {
    "buygoods": "1234",
    "clickbank": "PROD-001",
    "stripe": "prod_abc123"
  }
}
```

***

## Subscription Tiers

Products can have tiered subscription billing — different prices at different intervals (e.g. monthly then annually). Pass `subscription_tiers` as an array in the Create or Update body.

<ParamField body="is_subscription" type="boolean">
  Mark product as a subscription product
</ParamField>

<ParamField body="subscription_frequency" type="number">
  Default billing interval count (e.g. `1` for every-1-month)
</ParamField>

<ParamField body="subscription_frequency_unit" type="string">
  `day`, `week`, `month`, or `year`
</ParamField>

<ParamField body="subscription_trial_days" type="number">
  Free trial length in days (min: 0)
</ParamField>

<ParamField body="subscription_first_charge_free" type="boolean">
  Waive the first billing charge
</ParamField>

<ParamField body="subscription_tiers" type="array">
  Array of tier objects. When provided, overrides the flat `subscription_frequency` / `subscription_frequency_unit` values.
</ParamField>

<ParamField body="subscription_tiers[].interval" type="string" required>
  `day`, `week`, `month`, or `year`
</ParamField>

<ParamField body="subscription_tiers[].interval_count" type="number" required>
  How many units of `interval` between charges (min: 1)
</ParamField>

<ParamField body="subscription_tiers[].price" type="number" required>
  Billing amount for this tier (min: 0)
</ParamField>

<ParamField body="subscription_tiers[].tier" type="string">
  Optional tier label (max 64 chars)
</ParamField>

<ParamField body="subscription_tiers[].label" type="string">
  Display label shown to the customer (max 255 chars)
</ParamField>

<ParamField body="subscribe_save_discount_percent" type="number">
  Discount shown as "subscribe & save" (0–100%)
</ParamField>

```json Subscription tiers example theme={null}
{
  "is_subscription": true,
  "subscription_trial_days": 14,
  "subscribe_save_discount_percent": 15,
  "subscription_tiers": [
    {
      "tier": "monthly",
      "interval": "month",
      "interval_count": 1,
      "price": 29.99,
      "label": "Monthly — $29.99/mo"
    },
    {
      "tier": "annual",
      "interval": "year",
      "interval_count": 1,
      "price": 249.00,
      "label": "Annual — $249/yr (save 30%)"
    }
  ]
}
```

***

## Product Variants & Options

Products can have variants (e.g. size/colour combinations). Variants are managed inline via the `product_fields` and `variants` arrays on Create/Update.

### Product Fields (variant dimensions)

`product_fields` defines the variant dimensions (e.g. "Size", "Colour"):

```json theme={null}
{
  "product_fields": [
    { "name": "Size",   "type": "select", "options": ["S", "M", "L", "XL"], "position": 0 },
    { "name": "Colour", "type": "select", "options": ["Red", "Blue"],        "position": 1 }
  ]
}
```

### Variants

Each variant is a combination of field values with an optional per-variant price and SKU:

```json theme={null}
{
  "variants": [
    { "option1": "S",  "option2": "Red",  "code": "SKU-S-RED",  "price": 29.99 },
    { "option1": "M",  "option2": "Red",  "code": "SKU-M-RED",  "price": 29.99 },
    { "option1": "L",  "option2": "Blue", "code": "SKU-L-BLUE", "price": 34.99 }
  ]
}
```

<ParamField body="variants[].id" type="number">
  Existing variant ID for update (omit to create new)
</ParamField>

<ParamField body="variants[].code" type="string">
  Variant SKU/code (max 128 chars)
</ParamField>

<ParamField body="variants[].option1" type="string">
  Value for the first product field (max 128 chars)
</ParamField>

<ParamField body="variants[].option2" type="string">
  Value for the second product field (max 128 chars)
</ParamField>

<ParamField body="variants[].option3" type="string">
  Value for the third product field (max 128 chars)
</ParamField>

<ParamField body="variants[].price" type="number">
  Override price for this variant (null = use product base price)
</ParamField>

<ParamField body="variants[].image" type="string">
  Variant image URL (max 512 chars)
</ParamField>

<ParamField body="variants[].currency" type="string">
  3-letter ISO currency code (e.g. `USD`)
</ParamField>

***

## Variant Options

Variant options are brand-level reusable option sets (e.g. "Shoe Sizes", "Colours"). They can be attached to multiple products.

### List Variant Options

```
GET /api/brands/{brand}/variant-options
```

### Get All Variant Options (unpaginated)

```
GET /api/brands/{brand}/variant-options/all
```

### Create Variant Option

```
POST /api/brands/{brand}/variant-options
```

<ParamField body="name" type="string" required>
  Option set name (e.g. `Shirt Sizes`)
</ParamField>

<ParamField body="values" type="array" required>
  Array of option value strings (e.g. `["XS", "S", "M", "L", "XL"]`)
</ParamField>

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/variant-options \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Shirt Sizes", "values": ["XS", "S", "M", "L", "XL"]}'
```

### Update Variant Option

```
PUT /api/brands/{brand}/variant-options/{variant_option}
```

### Delete Variant Option

```
DELETE /api/brands/{brand}/variant-options/{variant_option}
```

***

## Product Categories

Organise products into a two-level category hierarchy (category → subcategory).

### List Categories

```
GET /api/brands/{brand}/product-categories
```

### Get All Categories (unpaginated)

```
GET /api/brands/{brand}/product-categories/all
```

### Create Category

```
POST /api/brands/{brand}/product-categories
```

<ParamField body="name" type="string" required>
  Category name
</ParamField>

<ParamField body="parent_id" type="number">
  Parent category ID — omit for top-level categories, provide for subcategories.
</ParamField>

```bash cURL — create top-level category theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/product-categories \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Supplements"}'
```

```bash cURL — create subcategory theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/product-categories \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Weight Loss", "parent_id": 3}'
```

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "id": 4,
    "name": "Weight Loss",
    "parent_id": 3,
    "brand_id": 42,
    "created_at": "2024-12-11T10:00:00.000000Z"
  }
  ```
</ResponseExample>

### Update Category

```
PUT /api/brands/{brand}/product-categories/{product_category}
```

### Delete Category

```
DELETE /api/brands/{brand}/product-categories/{product_category}
```

<Warning>
  When creating a product, `product_category_id` must be a **top-level** category (no `parent_id`). To assign a subcategory, also provide `product_subcategory_id` (which must belong to the category). Providing a subcategory without a category returns a 422.
</Warning>

***

## Coupons

Create and manage discount codes for products.

### List Coupons

```
GET /api/brands/{brand}/coupons
```

<ParamField query="per_page" type="number">
  Results per page (max 100, default: 25)
</ParamField>

### Get Coupon

```
GET /api/brands/{brand}/coupons/{coupon}
```

### Create Coupon

```
POST /api/brands/{brand}/coupons
```

<ParamField body="code" type="string" required>
  Coupon code (max 64 chars). Must be unique per brand.
</ParamField>

<ParamField body="type" type="string" required>
  `percent` — percentage off, or `fixed` — fixed amount off
</ParamField>

<ParamField body="value" type="number" required>
  Discount value. For `percent`, clamped to 0–100 automatically.
</ParamField>

<ParamField body="name" type="string">
  Internal label (max 128 chars)
</ParamField>

<ParamField body="status" type="string">
  `active` (default), `draft`, or `disabled`
</ParamField>

<ParamField body="product_ids" type="array">
  Limit discount to specific product IDs. Empty/null means all products.
</ParamField>

<ParamField body="excluded_product_ids" type="array">
  Product IDs to exclude from the discount.
</ParamField>

<ParamField body="applies_on_bump" type="boolean">
  Whether the coupon applies to order bump products.
</ParamField>

<ParamField body="valid_from" type="string">
  ISO 8601 date — coupon becomes active from this date.
</ParamField>

<ParamField body="valid_until" type="string">
  ISO 8601 date — coupon expires after this date. Must be on or after `valid_from`.
</ParamField>

<ParamField body="usage_limit" type="number">
  Maximum number of redemptions (null = unlimited).
</ParamField>

```bash cURL — 20% off all products theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/coupons \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "SAVE20",
    "name": "20% Off Launch Promo",
    "type": "percent",
    "value": 20,
    "status": "active",
    "valid_from": "2024-12-01",
    "valid_until": "2024-12-31",
    "usage_limit": 500
  }'
```

```bash cURL — $10 off specific products theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/coupons \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "BOTTLE10",
    "type": "fixed",
    "value": 10,
    "product_ids": [5, 6]
  }'
```

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "id": 12,
    "code": "SAVE20",
    "name": "20% Off Launch Promo",
    "type": "percent",
    "value": 20,
    "status": "active",
    "product_ids": null,
    "excluded_product_ids": null,
    "applies_on_bump": false,
    "valid_from": "2024-12-01",
    "valid_until": "2024-12-31",
    "usage_limit": 500,
    "brand_id": 42,
    "created_at": "2024-12-11T10:00:00.000000Z"
  }
  ```

  ```json 422 Duplicate code theme={null}
  {
    "errors": {
      "code": ["The code has already been taken."]
    }
  }
  ```
</ResponseExample>

### Update Coupon

```
PUT /api/brands/{brand}/coupons/{coupon}
```

All fields optional on update. Use this to disable an expired coupon:

```bash cURL — disable coupon theme={null}
curl -X PUT https://app.elasticfunnels.io/api/brands/{brand_id}/coupons/12 \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"status": "disabled"}'
```

### Delete Coupon

```
DELETE /api/brands/{brand}/coupons/{coupon}
```

### Validate Coupon

Check if a coupon code is valid and get its details (useful for frontend validation before applying at checkout).

```
GET /api/brands/{brand}/coupons/validate?code=SAVE20
```

<ParamField query="code" type="string" required>
  Coupon code to validate
</ParamField>

<ResponseExample>
  ```json 200 Valid theme={null}
  {
    "valid": true,
    "coupon": {
      "id": 12,
      "code": "SAVE20",
      "type": "percent",
      "value": 20,
      "product_ids": null
    }
  }
  ```

  ```json 200 Invalid theme={null}
  {
    "valid": false,
    "reason": "Coupon has expired."
  }
  ```
</ResponseExample>

***

## Notes

<Info>
  * Product `code` must be unique within the brand
  * `classification` of `bonus` products are automatically included with their parent products
  * Subscription products require `subscription_frequency` and `subscription_frequency_unit`
  * All costs (COGS, fulfillment, packaging) are tracked for profit margin calculations
  * Images are returned as both `image` (relative path) and `image_link` (full URL)
  * Downloadable assets live in `product_files`, not top-level `bonus_file` / `digital_file` columns
    * `product_category_id` must be a root-level category; use `product_subcategory_id` for sub-level
    * Coupon `type: "percent"` values are clamped to 0–100 automatically
</Info>

<Warning>
  When updating products that are actively used in funnels, changes may affect existing customer orders and analytics.
</Warning>
