> ## 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.

# Product Management

> Create and manage products with full e-commerce data: SEO, gallery, inventory, variants, attributes, fulfillment, subscriptions and more

Each product has one detail page for pricing, checkout behavior, shipping, fulfillment, subscriptions, SEO, and custom data.

## Creating a Product

Click **Add Product** in the Products list to open the quick-create modal. Enter the essential fields (Title, Code, Type, Classification, Price) and click **Create product**. You are automatically taken to the full Product Detail Page to complete configuration.

## Product Detail Page

Navigate to the Product Detail Page by clicking any product code or title in the Products list.

The page uses a persistent tab layout. Changes are saved by clicking **Save** in the top toolbar.

***

### General Tab

Core product identity, titles, images, descriptions, category assignment, and SEO metadata.

| Field                      | Description                                                                                                 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Type**                   | Physical, Digital, or Service                                                                               |
| **Code**                   | Unique identifier used in buy links (read-only after creation)                                              |
| **Classification**         | Main, Upsell, Downsell, Bump, or Bonus                                                                      |
| **Status**                 | Active, Draft, or Archived                                                                                  |
| **Title**                  | Primary display name                                                                                        |
| **Checkout Title**         | Overrides Title at checkout (optional)                                                                      |
| **Product Image**          | Primary product image — upload directly from this tab                                                       |
| **Gallery**                | Additional product images — upload files directly or remove existing images via the visual thumbnail picker |
| **Short Description**      | Brief summary for product cards and search results                                                          |
| **Full Description**       | Rich text or plain text description                                                                         |
| **Category / Subcategory** | Assign from brand-level product categories                                                                  |
| **SEO Title**              | Page title for search engines                                                                               |
| **SEO Slug**               | URL slug for storefront product pages                                                                       |
| **SEO Description**        | Meta description (max 160 chars)                                                                            |

***

### Pricing Tab

Selling price, retail (compare-at) price, currency, and tax settings.

| Field                | Description                                    |
| -------------------- | ---------------------------------------------- |
| **Retail Price**     | Compare-at price shown as strikethrough        |
| **Discounted Price** | Actual selling price                           |
| **Currency**         | 3-letter currency code (e.g. USD)              |
| **Tax Exempt**       | When enabled, no tax is calculated at checkout |
| **Tax Category**     | TaxJar TIC or Avalara tax code                 |

***

### Subscription Tab

Visible only when **This is a subscription product** is enabled. See [Subscriptions Overview](/subscriptions/overview) for full billing documentation.

| Field                    | Description                                                         |
| ------------------------ | ------------------------------------------------------------------- |
| **Is Subscription**      | Toggle to enable recurring billing                                  |
| **Build-a-Box**          | Customers select multiple items for their subscription box          |
| **Billing Frequency**    | How often to bill (e.g. 1)                                          |
| **Frequency Unit**       | Day / Week / Month / Year                                           |
| **Trial Days**           | Free trial days before first charge                                 |
| **First Charge Free**    | Customer pays shipping only on first order                          |
| **Subscription Tiers**   | Multiple billing frequencies at different prices (toggle to enable) |
| **Subscribe & Save (%)** | Discount when customer chooses subscription over one-time purchase  |
| **Stepped Pricing**      | Different prices at specific billing cycles (toggle to enable)      |
| **Prepaid Options**      | 3, 6, 12-month prepaid discounts                                    |

***

### Fulfillment Tab

Warehousing, inventory, cost breakdown, SKU categorisation, product specifications, and shipping profile.

| Section                    | Fields                                                                                              |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| **Identity & Structure**   | SKU, Barcode, Units per Package, Weight (oz), Dimensions (L/W/H cm)                                 |
| **Integration**            | Fulfillment Integration override, Multiply SKU toggle                                               |
| **Inventory**              | Stock Quantity, Out-of-Stock Policy (continue/deny)                                                 |
| **Cost Breakdown**         | Product Cost, Fulfillment Cost, Packaging Cost, Avg. Shipping, Fuel Surcharge, Auto-calculated COGS |
| **SKU Categorisation**     | SKU Category, SKU Subcategory (for internal reporting)                                              |
| **Product Specifications** | Repeater: Name / Value / Unit pairs (e.g. Dimensions: 10×5×3 cm)                                    |
| **Shipping Profile**       | Assign a brand-level shipping profile                                                               |

***

### Merchants Tab

Configure product identifiers per payment gateway integration.

For each active payment gateway (NMI, Stripe, BuyGoods, ClickBank, etc.) enter the product ID used on that gateway. These are exposed as `product | merchantPriceId:'stripe'` in templates.

***

### Bonuses Tab

Assign bonus products that customers receive when purchasing this product. Select from products with `classification = bonus`.

***

### Files Tab

Visible when product type is **Digital** or classification is **Bonus**.

Add one or more downloadable files:

* **Purpose** — Digital or Bonus
* **Title** — File display name
* **Description** — Optional description
* **Image** — Thumbnail image
* **File** — Upload or URL
* **Primary** — Mark as primary file for this purpose

***

### Courses Tab

Assign courses that customers are automatically enrolled in after purchase.

***

### Attributes Tab

Fill in values for brand-level Product Attributes defined under **Settings → Products → Product Attributes**.

Supports all field types: text, textarea, rich text, number, boolean, date, select, multiselect, color, image URL, file URL, and JSON.

Attributes with `applies_to = variant` are managed per variant in the Variants tab, not here.

***

### Custom Attributes Tab

Legacy unstructured key/value pairs for ad-hoc product metadata (e.g. a download URL or custom flag).

> **Note:** For structured, typed attributes, use the Attributes tab with Product Attribute definitions instead. Custom Attributes are a legacy system.

***

### Variants Tab

Define product variants (e.g. Color, Size) with individual pricing.

1. **Select variant options** — Check which brand-level variant options apply to this product (managed under Settings → Variant Options)
2. **Add variants** — Each row shows one field per selected option plus Code and Price

***

## Template Reference

All product data is available in ElasticFunnels templates via data functions and filters. Functions are called with `{% set x = FunctionName(args) %}` or inline. Filters are applied with `{{ value | filterName:arg }}`.

***

### Product Object Shape

Every product returned by a data function has the following fields:

| Field                            | Type                                                    | Description                                                        |
| -------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------ |
| `id`                             | `number`                                                | Unique product ID                                                  |
| `brand_id`                       | `number`                                                | Brand this product belongs to                                      |
| `title`                          | `string`                                                | Display title — uses `checkout_title` when set, otherwise `title`  |
| `checkout_title`                 | `string \| null`                                        | Raw checkout title (before effective resolution)                   |
| `code`                           | `string`                                                | Unique product code used in buy links                              |
| `type`                           | `"physical" \| "digital" \| "service"`                  | Product type                                                       |
| `classification`                 | `"main" \| "upsell" \| "downsell" \| "bump" \| "bonus"` | Role in the funnel                                                 |
| `status`                         | `"active" \| "draft" \| "archived"`                     | Visibility status                                                  |
| `price`                          | `number`                                                | Selling price                                                      |
| `retail_price`                   | `number \| null`                                        | Compare-at (strikethrough) price                                   |
| `currency`                       | `string \| null`                                        | 3-letter currency code (e.g. `"USD"`); `null` = brand default      |
| `sku`                            | `string \| null`                                        | Stock-keeping unit                                                 |
| `barcode`                        | `string \| null`                                        | UPC / EAN barcode                                                  |
| `units`                          | `number \| null`                                        | Package quantity                                                   |
| `physical_units_per_product`     | `number \| null`                                        | Individual units inside one package (for per-unit pricing)         |
| `description`                    | `string \| null`                                        | Full product description (HTML)                                    |
| `short_description`              | `string \| null`                                        | Brief summary                                                      |
| `image`                          | `string \| null`                                        | Primary image CDN URL                                              |
| `gallery`                        | `string[]`                                              | Additional image CDN URLs — **always an array**, never `null`      |
| `has_gallery`                    | `boolean`                                               | `true` when `gallery.length > 0`                                   |
| `has_description`                | `boolean`                                               | `true` when `description` is non-empty                             |
| `in_stock`                       | `boolean`                                               | `true` when `inventory_quantity` is `null` or `> 0`                |
| `inventory_quantity`             | `number \| null`                                        | Current stock count; `null` = unlimited                            |
| `inventory_policy`               | `"continue" \| "deny"`                                  | Behaviour when out of stock                                        |
| `is_subscription`                | `boolean`                                               | Whether recurring billing is enabled                               |
| `subscription_frequency`         | `number \| null`                                        | Billing interval value (e.g. `1`)                                  |
| `subscription_frequency_unit`    | `"day" \| "week" \| "month" \| "year" \| null`          | Billing interval unit                                              |
| `subscription_trial_days`        | `number \| null`                                        | Free trial days before first charge                                |
| `subscription_first_charge_free` | `boolean`                                               | First order ships free (customer pays shipping only)               |
| `is_build_a_box`                 | `boolean`                                               | Build-a-Box subscription mode                                      |
| `seo_title`                      | `string \| null`                                        | SEO page title                                                     |
| `seo_slug`                       | `string \| null`                                        | URL slug for storefront product pages                              |
| `seo_description`                | `string \| null`                                        | Meta description                                                   |
| `product_category_id`            | `number \| null`                                        | Assigned category ID                                               |
| `product_subcategory_id`         | `number \| null`                                        | Assigned subcategory ID                                            |
| `category_name`                  | `string \| null`                                        | Category display name (denormalised)                               |
| `subcategory_name`               | `string \| null`                                        | Subcategory display name (denormalised)                            |
| `product_files`                  | `ProductFile[]`                                         | All attached files                                                 |
| `digital_files`                  | `ProductFile[]`                                         | Subset: digital download files                                     |
| `bonus_files`                    | `ProductFile[]`                                         | Subset: bonus files                                                |
| `primary_download_url`           | `string \| null`                                        | URL of the primary digital file                                    |
| `merchant_product_ids`           | `Record<string, string>`                                | Gateway → merchant product ID map (e.g. `{ stripe: "price_xxx" }`) |

#### ProductFile shape

Each item in `product_files`, `digital_files`, and `bonus_files`:

| Field         | Type                                                                 | Description                                      |
| ------------- | -------------------------------------------------------------------- | ------------------------------------------------ |
| `url`         | `string`                                                             | CDN URL of the file                              |
| `title`       | `string \| null`                                                     | File display name                                |
| `description` | `string \| null`                                                     | File description                                 |
| `image`       | `string \| null`                                                     | Thumbnail image URL                              |
| `purpose`     | `"digital" \| "bonus"`                                               | Whether this is a digital or bonus file          |
| `kind`        | `"audio" \| "video" \| "document" \| "image" \| "archive" \| "file"` | Inferred from MIME type                          |
| `mime_type`   | `string \| null`                                                     | MIME type                                        |
| `size`        | `number \| null`                                                     | File size in bytes                               |
| `extension`   | `string \| null`                                                     | File extension (e.g. `"pdf"`)                    |
| `is_primary`  | `boolean`                                                            | Whether this is the primary file for its purpose |
| `sort_order`  | `number`                                                             | Display order                                    |

***

### Data Functions

<h4 id="fn-getproduct">
  `GetProduct(code)`
</h4>

Loads a single product by its **code** for the current brand. Also pre-loads structured attribute values so the `attr` and `attrLabel` filters work immediately on the returned object.

```
{% set product = GetProduct('PROD-001') %}
{{ product.title }} — {{ product.price | formatPrice:product.currency }}
```

**Returns:** `ProductObject | null`

***

<h4 id="fn-getproducts">
  `GetProducts(filters?)`
</h4>

Returns all active products for the brand, with optional filters.

| Filter key       | Type               | Description                                                |
| ---------------- | ------------------ | ---------------------------------------------------------- |
| `type`           | `string`           | `"physical"`, `"digital"`, or `"service"`                  |
| `classification` | `string`           | `"main"`, `"upsell"`, `"downsell"`, `"bump"`, or `"bonus"` |
| `codes`          | `string`           | Comma-separated list of product codes                      |
| `category_id`    | `number \| string` | Filter by product category ID                              |
| `subcategory_id` | `number \| string` | Filter by subcategory ID                                   |

```
{% set upsells = GetProducts({ classification: 'upsell' }) %}
{% for p in upsells %}
  <div>{{ p.title }} — {{ p.price | formatPrice:p.currency }}</div>
{% endfor %}
```

**Returns:** `ProductObject[]`

***

<h4 id="fn-getallproducts">
  `GetAllProducts()`
</h4>

Returns every product for the brand without filters. Fastest when you need to iterate all products and filter in the template.

```
{% set all = GetAllProducts() %}
{% for p in all %}{% if p.status == 'active' %}
  {{ p.title }}
{% endif %}{% endfor %}
```

**Returns:** `ProductObject[]`

***

<h4 id="fn-getproductcategories">
  `GetProductCategories(filters?)`
</h4>

Returns product categories. By default returns a full tree ordered by position, then name, with a `depth` field.

| Filter key  | Type               | Description                           |
| ----------- | ------------------ | ------------------------------------- |
| `top_level` | `boolean`          | Only root categories (no parent)      |
| `parent_id` | `number \| string` | Only direct children of this category |
| `flat`      | `boolean`          | Return flat list instead of tree      |
| `tree`      | `boolean`          | Set `false` to force flat list        |

Each item: `{ id, name, position, parent_id, depth? }`

```
{% set cats = GetProductCategories({ top_level: true }) %}
{% for cat in cats %}
  <h3>{{ cat.name }}</h3>
  {% set subs = GetProductCategories({ parent_id: cat.id }) %}
  {% for sub in subs %}<span>{{ sub.name }}</span>{% endfor %}
{% endfor %}
```

**Returns:** `CategoryObject[]`

***

<h4 id="fn-productdownloads">
  `ProductDownloads(product)`
</h4>

Flattens all downloadable files attached to a product into a single deduplicated array. Useful for rendering download lists on digital product pages.

```
{% set files = ProductDownloads(product) %}
{% for f in files %}
  <a href="{{ f.url }}">{{ f.title or f.extension | upper }} — {{ f.kind }}</a>
{% endfor %}
```

Each item: `{ url, title, kind, purpose, extension, mime_type, size, is_primary, sort_order }`

**Returns:** `DownloadObject[]`

***

<h4 id="fn-productsavings">
  `ProductSavings(product)`
</h4>

Returns a structured savings breakdown comparing `retail_price` to `price`. Returns `null` if there is no saving.

```
{% set savings = ProductSavings(product) %}
{% if savings and savings.amount > 0 %}
  You save {{ savings.amountFormatted }} ({{ savings.percent }}% off)
{% endif %}
```

**Returns:** `{ amount: number, percent: number, amountFormatted: string, percentFormatted: string } | null`

***

<h4 id="fn-subscriptionsummary">
  `SubscriptionSummary(product)`
</h4>

Generates a human-readable billing summary string for a subscription product (e.g. `"7-day free trial · Every 1 month"`).

```
{% if product.is_subscription %}
  <p>{{ SubscriptionSummary(product) }}</p>
{% endif %}
```

**Returns:** `string`

***

<h4 id="fn-getproductattributes">
  `GetProductAttributes(filters?)`
</h4>

Returns the brand's structured attribute definitions. See [Product Attributes](/products/product-attributes) for details on attribute types and scoping.

| Filter key   | Type                     | Description                                           |
| ------------ | ------------------------ | ----------------------------------------------------- |
| `visible`    | `boolean`                | Only attributes with `visible_on_product_page = true` |
| `filterable` | `boolean`                | Only attributes marked as filterable                  |
| `applies_to` | `"product" \| "variant"` | Filter by scope                                       |

Each item: `{ id, name, slug, type, applies_to, options, filterable, visible_on_product_page, required }`

**Returns:** `AttributeDefinition[]`

***

<h4 id="fn-getproductattributevalues">
  `GetProductAttributeValues(product)`
</h4>

Returns all attribute values for a product as a `{ slug: value }` map.

```
{% set defs = GetProductAttributes({ visible: true }) %}
{% set vals = GetProductAttributeValues(product) %}
{% for attr in defs %}
  <dt>{{ attr.name }}</dt>
  <dd>{{ vals[attr.slug] }}</dd>
{% endfor %}
```

**Returns:** `Record<string, string | number | boolean>`

***

### Filters

<h4 id="filter-formatprice">
  `formatPrice`
</h4>

Formats a numeric price using the brand's active currency locale.

```
{{ product.price | formatPrice:product.currency }}
{# → "$49.00" #}

{{ 9.99 | formatPrice:'EUR' }}
{# → "€9.99" #}
```

***

<h4 id="filter-savings">
  `savings`
</h4>

Computes the saving between `retail_price` and `price` and returns a formatted string.

| Format      | Example output         |
| ----------- | ---------------------- |
| *(default)* | `"Save $20"`           |
| `'percent'` | `"Save 25%"`           |
| `'both'`    | `"Save $20 (25% off)"` |

```
{{ product | savings }}
{{ product | savings:'percent' }}
{{ product | savings:'both' }}
```

Returns `""` when `retail_price` is not set or not greater than `price`.

***

<h4 id="filter-compareat">
  `compareAt`
</h4>

Formats `retail_price` as a "Was \$X" string.

```
{{ product | compareAt }}
{# → "Was $99.00" #}
```

Returns `""` when `retail_price` is not set.

***

<h4 id="filter-priceperunit">
  `pricePerUnit`
</h4>

Divides `price` by `physical_units_per_product` to give the per-unit price. Useful for "per bottle" or "per capsule" displays.

```
{{ product | pricePerUnit }}
{# → "33.00" (for a 6-pack at $198) #}
```

**Returns:** `string` (float formatted to 2 decimal places)

***

<h4 id="filter-subscriptionlabel">
  `subscriptionLabel`
</h4>

Short-form billing label. Returns trial info if present, otherwise the billing frequency.

```
{{ product | subscriptionLabel }}
{# → "7-day free trial"  or  "Every 1 month" #}
```

Returns `""` for non-subscription products.

***

<h4 id="filter-merchantpriceid">
  `merchantPriceId`
</h4>

Looks up the merchant-specific product identifier from `merchant_product_ids` (configured in the Merchants tab).

```
{{ product | merchantPriceId:'stripe' }}
{# → "price_1abc..." #}

{{ product | merchantPriceId:'nmi' }}
```

***

<h4 id="filter-attr">
  `attr`
</h4>

Returns the value of a structured attribute by its slug. Only works on products loaded via `GetProduct` (which pre-loads attribute values).

```
{% set product = GetProduct('MY-PRODUCT') %}
{{ product | attr:'material' }}
{# → "Organic Cotton" #}
```

Returns `""` when the attribute is not set.

***

<h4 id="filter-attrlabel">
  `attrLabel`
</h4>

Returns the human-readable **label** (name) of a structured attribute definition by slug.

```
{{ product | attrLabel:'material' }}
{# → "Material" #}
```

***

<h3 id="gallery">
  Gallery
</h3>

`product.gallery` is a `string[]` — an array of CDN image URLs. It is always an array (never `null` or a JSON string). `product.has_gallery` is `true` when the array has at least one entry.

```html theme={null}
{% if product.has_gallery %}
<div class="product-gallery">
  {% for url in product.gallery %}
  <img src="{{ url }}" alt="{{ product.title }}" />
  {% endfor %}
</div>
{% endif %}
```

Show the primary image with gallery fallback:

```html theme={null}
{% set mainImage = product.image or (product.gallery[0] if product.has_gallery) %}
{% if mainImage %}
<img src="{{ mainImage }}" alt="{{ product.title }}" />
{% endif %}
```
