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

# Components

> Create, read, update, and delete reusable page components via API

<Note>
  All component endpoints authenticate via `EF-Access-Key`. The brand user's role must have `components` module access.
</Note>

***

## List Components

Return all components for a brand with pagination. This endpoint does not include `html`, `css`, or `config` in the response — use [Get Component](#get-component) to retrieve full content for a specific component.

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

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

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

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

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

```javascript JavaScript theme={null}
const res = await fetch(
  'https://app.elasticfunnels.io/api/brands/{brand_id}/components',
  { headers: { 'EF-Access-Key': 'your_api_key_here' } }
);
const { data, total, current_page, last_page } = await res.json();
```

<ResponseExample>
  ```json Response theme={null}
  {
    "current_page": 1,
    "data": [
      {
        "id": 12,
        "name": "Hero Section",
        "code": "custom-hero-section",
        "type": "editor",
        "screenshot": null,
        "brand_id": 42,
        "created_at": "2024-11-01T09:00:00.000000Z",
        "updated_at": "2024-12-10T14:30:00.000000Z",
        "tags": []
      },
      {
        "id": 13,
        "name": "Guarantee Badge",
        "code": "custom-guarantee-badge",
        "type": null,
        "screenshot": null,
        "brand_id": 42,
        "created_at": "2024-11-05T10:00:00.000000Z",
        "updated_at": "2024-11-05T10:00:00.000000Z",
        "tags": []
      }
    ],
    "per_page": 25,
    "total": 2,
    "last_page": 1,
    "from": 1,
    "to": 2
  }
  ```
</ResponseExample>

***

## Get All Components (unpaginated)

Return all components for a brand in a single array, without pagination. Only components that have a builder config are included.

<Note>
  This endpoint returns a lightweight shape: `id`, `name`, `code`, `type`, `screenshot`, and (unless `?short=true`) `config`. It does **not** return `html` or `css`. Use [Get Component](#get-component) for the full record of a specific component.
</Note>

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

<ParamField query="type" type="string">
  Filter by type. Pass `editor` to return only visual-builder components.
</ParamField>

<ParamField query="short" type="boolean">
  When `true`, omits `config` from the response — returns only `id`, `name`, `code`, `type`, and `screenshot`. Useful for lightweight dropdowns.
</ParamField>

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

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

<ResponseExample>
  ```json Response theme={null}
  [
    {
      "id": 12,
      "name": "Hero Section",
      "code": "custom-hero-section",
      "type": "editor",
      "config": { "data": { "type": "wrapper", "components": [] }, "styles": [] },
      "screenshot": null
    }
  ]
  ```
</ResponseExample>

***

## Get Component

Retrieve a single component including its full `html`, `css`, and `config`. Accepts either a numeric ID or the component's `code` string.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string (e.g. `custom-hero-section`)
</ParamField>

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

```bash cURL (by ID) theme={null}
curl https://app.elasticfunnels.io/api/brands/{brand_id}/components/12 \
  -H "EF-Access-Key: your_api_key_here"
```

```bash cURL (by code) theme={null}
curl https://app.elasticfunnels.io/api/brands/{brand_id}/components/custom-hero-section \
  -H "EF-Access-Key: your_api_key_here"
```

<ResponseExample>
  ```json Response theme={null}
  {
    "id": 12,
    "name": "Hero Section",
    "code": "custom-hero-section",
    "type": "editor",
    "html": "<!DOCTYPE html><html><body><section class=\"hero\">...</section></body></html>",
    "css": ".hero { background: #fff; padding: 60px 0; }",
    "config": null,
    "brand_id": 42,
    "created_at": "2024-11-01T09:00:00.000000Z",
    "updated_at": "2024-12-10T14:30:00.000000Z"
  }
  ```

  ```json 404 Not found theme={null}
  {
    "message": "No query results for model [BrandPageComponent]."
  }
  ```
</ResponseExample>

***

## Create Component

Create a new reusable component. This is the only endpoint that accepts `html`, `css`, and `config` — [Update Component](#update-component) is metadata-only.

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

<ParamField body="name" type="string" required>
  Display name shown in the component library
</ParamField>

<ParamField body="code" type="string">
  Unique identifier used to embed the component in pages (e.g. `custom-guarantee-badge`). Auto-generated if omitted. Components are automatically prefixed with `custom-`.
</ParamField>

<ParamField body="html" type="string">
  HTML markup for the component
</ParamField>

<ParamField body="css" type="string">
  CSS styles scoped to this component
</ParamField>

<ParamField body="config" type="object">
  Builder config object. Required for visual-builder components; omit for plain HTML components.
</ParamField>

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

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/components \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Guarantee Badge",
    "code": "custom-guarantee-badge",
    "html": "<div class=\"badge\"><img src=\"https://cdn.elasticfunnels.io/42/assets/media/guarantee-badge.png\" alt=\"60-Day Guarantee\"></div>",
    "css": ".badge { max-width: 200px; margin: 20px auto; text-align: center; }"
  }'
```

```python Python theme={null}
import requests

r = requests.post(
    'https://app.elasticfunnels.io/api/brands/{brand_id}/components',
    headers={'EF-Access-Key': 'your_api_key_here', 'Content-Type': 'application/json'},
    json={
        'name': 'Guarantee Badge',
        'code': 'custom-guarantee-badge',
        'html': '<div class="badge">...</div>',
        'css': '.badge { max-width: 200px; }',
    },
)
component = r.json()['pageComponent']
print(component['id'], component['code'])
```

```javascript JavaScript theme={null}
const res = await fetch(
  'https://app.elasticfunnels.io/api/brands/{brand_id}/components',
  {
    method: 'POST',
    headers: {
      'EF-Access-Key': 'your_api_key_here',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: 'Guarantee Badge',
      code: 'custom-guarantee-badge',
      html: '<div class="badge">...</div>',
      css: '.badge { max-width: 200px; }',
    }),
  }
);
const { pageComponent } = await res.json();
```

<ResponseExample>
  ```json 200 Created theme={null}
  {
    "pageComponent": {
      "id": 14,
      "name": "Guarantee Badge",
      "code": "custom-guarantee-badge",
      "type": null,
      "html": "<div class=\"badge\">...</div>",
      "css": ".badge { max-width: 200px; margin: 20px auto; text-align: center; }",
      "brand_id": 42,
      "created_at": "2024-12-10T16:00:00.000000Z",
      "updated_at": "2024-12-10T16:00:00.000000Z"
    }
  }
  ```

  ```json 422 Validation error theme={null}
  {
    "errors": {
      "name": ["The name field is required."]
    }
  }
  ```
</ResponseExample>

***

## Update Component

Update a component's **metadata only** — name and code slug. This endpoint does **not** accept or persist `html`, `css`, or `config`. To update a component's HTML content use [Update Component Content](#update-component-content). To replace a component entirely, delete it and re-create it with [Create Component](#create-component).

<Warning>
  **`html`, `css`, and `config` are silently ignored on PUT.** The endpoint returns `200` even if you include them — but the content is not saved.
</Warning>

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

<ParamField body="name" type="string" required>
  Display name. **Required even if you are not changing it** — send the current name.
</ParamField>

<ParamField body="code" type="string">
  Rename the component's code slug. If changed, **all pages that reference this component are automatically updated** to use the new code.
</ParamField>

```bash Request theme={null}
PUT /api/brands/{brand}/components/{component}
```

```bash cURL (rename code) theme={null}
curl -X PUT https://app.elasticfunnels.io/api/brands/{brand_id}/components/14 \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Guarantee Badge",
    "code": "custom-guarantee-v2"
  }'
```

```bash cURL (rename by current code) theme={null}
curl -X PUT https://app.elasticfunnels.io/api/brands/{brand_id}/components/custom-guarantee-badge \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Guarantee Badge v2"
  }'
```

<ResponseExample>
  ```json 200 Updated theme={null}
  {
    "id": 14,
    "name": "Guarantee Badge v2",
    "code": "custom-guarantee-badge",
    "type": null,
    "brand_id": 42,
    "updated_at": "2024-12-11T09:15:00.000000Z"
  }
  ```

  ```json 422 Missing required name theme={null}
  {
    "errors": {
      "name": ["The name field is required."]
    }
  }
  ```

  ```json 404 Component not found theme={null}
  {
    "message": "No query results for model [BrandPageComponent]."
  }
  ```
</ResponseExample>

<Warning>
  **Common causes of errors on PUT:**

  | Symptom                            | Cause                                                                          | Fix                                                                                              |
  | ---------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
  | `422` with `name is required`      | `name` is always required by the validator, even if unchanged                  | Always include `"name"` in the body                                                              |
  | `422` code uniqueness error        | The new `code` is already taken by another component in the same brand         | Choose a unique code                                                                             |
  | `401` redirect to login            | Missing or invalid `EF-Access-Key` header, or key doesn't belong to this brand | Check header name and key value                                                                  |
  | `403` module access                | Brand user role doesn't have `components` access                               | Update role permissions in Project Settings                                                      |
  | `html`/`css`/`config` not updating | These fields are not writable via PUT                                          | Use `POST /{component}/editor` to update HTML; use DELETE + POST to replace a component entirely |
</Warning>

***

## Update Component Content

Update the HTML content of a component. Use this endpoint to publish new HTML or save a draft for later review. This is separate from [Update Component](#update-component), which only handles metadata.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

<ParamField body="html" type="string" required>
  The HTML content to save
</ParamField>

<ParamField query="draft" type="boolean">
  When `true`, the HTML is saved as a draft revision and the live component is **not** updated. Draft revisions are returned by [Get Component Content](#get-component-content) until discarded or published. When omitted (or `false`), the HTML is published immediately and any existing draft revisions are discarded.
</ParamField>

<ParamField body="revision_id" type="number">
  When saving a draft, pass an existing `revision_id` to update that specific draft instead of creating a new one.
</ParamField>

```bash Request theme={null}
POST /api/brands/{brand}/components/{component}/editor
```

```bash cURL (publish) theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/components/custom-hero-section/editor \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<section class=\"hero\"><h1>Updated headline</h1></section>"
  }'
```

```bash cURL (save as draft) theme={null}
curl -X POST "https://app.elasticfunnels.io/api/brands/{brand_id}/components/custom-hero-section/editor?draft=true" \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<section class=\"hero\"><h1>Draft headline</h1></section>"
  }'
```

```javascript JavaScript (publish) theme={null}
await fetch(
  'https://app.elasticfunnels.io/api/brands/{brand_id}/components/custom-hero-section/editor',
  {
    method: 'POST',
    headers: {
      'EF-Access-Key': 'your_api_key_here',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ html: '<section class="hero"><h1>Updated</h1></section>' }),
  }
);
```

<ResponseExample>
  ```json 200 Published theme={null}
  {
    "success": true,
    "revision_id": null
  }
  ```

  ```json 200 Saved as draft theme={null}
  {
    "success": true,
    "revision_id": 88
  }
  ```
</ResponseExample>

***

## Get Component Content

Read the current content of a component. If a draft revision exists, the draft HTML is returned along with its `revision_id`. Pass `?published=true` to always get the live published content, ignoring any draft.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

<ParamField query="published" type="boolean">
  When `true`, returns the published (live) HTML even if a draft revision exists.
</ParamField>

```bash Request theme={null}
GET /api/brands/{brand}/components/{component}/editor
```

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

<ResponseExample>
  ```json Response (no draft) theme={null}
  {
    "id": 12,
    "name": "Hero Section",
    "code": "custom-hero-section",
    "html": "<section class=\"hero\"><h1>Published headline</h1></section>"
  }
  ```

  ```json Response (draft exists) theme={null}
  {
    "id": 12,
    "name": "Hero Section",
    "code": "custom-hero-section",
    "html": "<section class=\"hero\"><h1>Draft headline</h1></section>",
    "revision_id": 88
  }
  ```
</ResponseExample>

***

## Discard Draft

Discard all draft revisions for a component, reverting to the live published content.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

```bash Request theme={null}
POST /api/brands/{brand}/components/{component}/editor/discard
```

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/components/custom-hero-section/editor/discard \
  -H "EF-Access-Key: your_api_key_here"
```

<ResponseExample>
  ```json Response theme={null}
  {
    "success": true
  }
  ```
</ResponseExample>

***

## Get Component Builder Config

Read the full builder config for a component, ready to load into the builder. The server injects brand assets into `config.assets`, normalizes style selectors, and removes empty ID components before returning.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

<ParamField query="revision_id" type="number">
  Load a specific revision's config instead of the current published state.
</ParamField>

```bash Request theme={null}
GET /api/brands/{brand}/components/{component}/builder
```

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

<ResponseExample>
  ```json Response theme={null}
  {
    "pageComponents": [
      {
        "frames": [
          {
            "component": {
              "type": "wrapper",
              "stylable": ["background", "background-color", "background-image"],
              "components": [
                {
                  "type": "text",
                  "attributes": { "id": "c12-hero-heading" },
                  "components": [{ "type": "textnode", "content": "Hello World" }]
                }
              ]
            }
          }
        ],
        "type": "main"
      }
    ],
    "styles": [
      {
        "selectors": ["#c12-hero-heading"],
        "style": { "color": "#333333", "font-size": "32px", "font-weight": "700" }
      },
      {
        "selectors": ["#c12-hero-heading"],
        "mediaText": "768px",
        "atRuleType": "media",
        "style": { "font-size": "22px" }
      }
    ],
    "assets": [
      { "src": "https://cdn.elasticfunnels.io/42/assets/media/logo.png", "type": "image" }
    ],
    "interactions": [],
    "fonts": []
  }
  ```
</ResponseExample>

***

## Save Component Builder Config

Save a full builder config — component tree, styles, rendered HTML, CSS, and interactions — for a component. The server processes the payload before storing it and returns the processed result. **You must replace your local state with the returned config and HTML** before any subsequent save (see [Server-side processing](#server-side-processing) below).

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

<ParamField body="config" type="string" required>
  The full builder config as a **JSON string** (not a nested object — stringify it before sending).
</ParamField>

<ParamField body="html" type="string" required>
  Rendered HTML output from the builder
</ParamField>

<ParamField body="css" type="string">
  Rendered CSS output from the builder
</ParamField>

<ParamField body="interactions" type="array">
  Interaction definitions attached to this component
</ParamField>

<ParamField body="type" type="string">
  Omit (or any value other than `revision`) to publish immediately. Pass `revision` to save as a draft without touching the live component.
</ParamField>

<ParamField body="revision_id" type="number">
  When saving a draft (`type=revision`), pass an existing `revision_id` to update that draft instead of creating a new one. Revisions older than 1 hour are always replaced with a new draft regardless.
</ParamField>

```bash Request theme={null}
POST /api/brands/{brand}/components/{component}/builder
```

```bash cURL (publish) theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/components/12/builder \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "config": "{\"pageComponents\":[{\"frames\":[{\"component\":{\"type\":\"wrapper\",\"components\":[{\"type\":\"text\",\"attributes\":{\"id\":\"hero-heading\"},\"components\":[{\"type\":\"textnode\",\"content\":\"Hello World\"}]}]}}],\"type\":\"main\"}],\"styles\":[{\"selectors\":[\"#hero-heading\"],\"style\":{\"color\":\"#333\",\"font-size\":\"32px\"}}],\"interactions\":[],\"fonts\":[]}",
    "html": "<div id=\"hero-heading\">Hello World</div>",
    "css": "#hero-heading { color: #333; font-size: 32px; }"
  }'
```

```bash cURL (save as draft) theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/components/12/builder \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "revision",
    "config": "...",
    "html": "...",
    "css": "..."
  }'
```

```javascript JavaScript theme={null}
const config = { /* your builder config object */ };
const res = await fetch(
  'https://app.elasticfunnels.io/api/brands/{brand_id}/components/12/builder',
  {
    method: 'POST',
    headers: {
      'EF-Access-Key': 'your_api_key_here',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      config: JSON.stringify(config), // must be a string
      html: renderedHtml,
      css: renderedCss,
      interactions: [],
    }),
  }
);

// IMPORTANT: replace your local state with the server-processed result
const { config: processedConfig } = await res.json();
localConfig = processedConfig;
```

<ResponseExample>
  ```json 200 Published theme={null}
  {
    "id": 12,
    "name": "Hero Section",
    "code": "custom-hero-section",
    "config": {
      "pageComponents": [
        {
          "frames": [
            {
              "component": {
                "type": "wrapper",
                "components": [
                  {
                    "type": "text",
                    "attributes": { "id": "c12-hero-heading" },
                    "components": [{ "type": "textnode", "content": "Hello World" }]
                  }
                ]
              }
            }
          ],
          "type": "main"
        }
      ],
      "styles": [
        {
          "selectors": ["#c12-hero-heading"],
          "style": { "color": "#333", "font-size": "32px" }
        }
      ],
      "interactions": [],
      "fonts": []
    }
  }
  ```

  ```json 200 Saved as draft theme={null}
  {
    "id": 12,
    "name": "Hero Section",
    "code": "custom-hero-section",
    "config": { "...": "processed config" },
    "revision_id": 44
  }
  ```

  ```json 422 Malformed config theme={null}
  {
    "message": "Malformed data structure detected. Please refresh the page and try again."
  }
  ```
</ResponseExample>

### Server-side processing

The server modifies the config, HTML, and CSS you send before storing them. The response always contains the **processed** versions. You must store and re-send the returned values — not your original pre-send state — otherwise the next save will be operating on stale data.

What gets modified:

| Field                                   | What happens                                                                                                                                                                                                                                             |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config.styles[*].selectors`            | All `#id` selectors are normalized to `#c{componentId}-{baseId}`. Any existing `c\d+-` prefixes are stripped first, then the current component ID prefix is applied. This makes cloned and imported components safe to save without selector collisions. |
| `config` component tree `attributes.id` | Same scoping applied recursively to every component node's `id` attribute.                                                                                                                                                                               |
| `html` element IDs and anchor hrefs     | `id="..."` and `href="#..."` values are rewritten to `c{componentId}-{baseId}` format.                                                                                                                                                                   |
| `css` ID selectors                      | Same prefix normalization. Hex color values (`#fff`, `#333333`, etc.) are left untouched.                                                                                                                                                                |
| `interactions` target/trigger selectors | `#id` references in interaction targets and trigger elements are rewritten to match.                                                                                                                                                                     |
| `config.styles`                         | Empty `state` fields removed; `atRuleType: 'media'` is set automatically when `mediaText` is present.                                                                                                                                                    |

**Corruption guard**: If `config.data[0]` has a single key `"0"` (a known serialization artifact), the save is aborted with `422` to prevent data loss. This means the config was malformed before being sent — fetch a fresh copy with `GET /builder` and retry.

**Snapshot on publish**: Before overwriting the live component, the server creates a revision snapshot of the current published state (at most once per hour). These snapshots are accessible via [Get Component Revisions](#get-component-revisions).

***

## Get Component Revisions

List revision history for a component. Revisions are created automatically as hourly snapshots when publishing, and explicitly when saving a draft.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

```bash Request theme={null}
GET /api/brands/{brand}/components/{component}/revisions
```

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

<ResponseExample>
  ```json Response theme={null}
  [
    {
      "id": 44,
      "created_at": "2024-12-11T09:00:00.000000Z",
      "updated_at": "2024-12-11T09:00:00.000000Z",
      "updated_by": 7,
      "updated_by_user": {
        "id": 7,
        "name": "Jane Smith",
        "email": "jane@example.com"
      }
    },
    {
      "id": 43,
      "created_at": "2024-12-10T14:00:00.000000Z",
      "updated_at": "2024-12-10T14:00:00.000000Z",
      "updated_by": null,
      "updated_by_user": null
    }
  ]
  ```
</ResponseExample>

***

## Delete Component

Delete a component. If the component is still referenced on one or more pages, the request returns a `409` error to prevent accidental data loss. Pass `?force=1` to delete regardless.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

<ParamField query="force" type="boolean">
  Pass `1` to delete even if the component is still referenced on pages. Pages that reference the component by code will render an empty slot where the component was.
</ParamField>

```bash Request theme={null}
DELETE /api/brands/{brand}/components/{component}
```

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

```bash cURL (force delete) theme={null}
curl -X DELETE "https://app.elasticfunnels.io/api/brands/{brand_id}/components/14?force=1" \
  -H "EF-Access-Key: your_api_key_here"
```

<ResponseExample>
  ```json 200 Deleted theme={null}
  {
    "success": true
  }
  ```

  ```json 409 Component in use theme={null}
  {
    "error": "component_in_use",
    "message": "Component is still used in 3 page(s). Pass force=1 to delete anyway.",
    "usage_count": 3
  }
  ```

  ```json 404 Not found theme={null}
  {
    "message": "No query results for model [BrandPageComponent]."
  }
  ```
</ResponseExample>

***

## Get Component Usage

Check how many pages reference a component. Optionally return the list of pages for display in tooling.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string
</ParamField>

<ParamField query="include" type="string">
  Pass `pages` to include the list of pages that reference this component (capped at 50).
</ParamField>

```bash Request theme={null}
GET /api/brands/{brand}/components/{component}/usage
```

```bash cURL theme={null}
curl "https://app.elasticfunnels.io/api/brands/{brand_id}/components/custom-guarantee-badge/usage?include=pages" \
  -H "EF-Access-Key: your_api_key_here"
```

<ResponseExample>
  ```json Response theme={null}
  {
    "component_id": 14,
    "component_code": "custom-guarantee-badge",
    "usage_count": 3,
    "pages": [
      { "id": 101, "slug": "checkout", "title": "Checkout Page" },
      { "id": 102, "slug": "upsell-1", "title": "Upsell Step 1" },
      { "id": 105, "slug": "landing-v2", "title": "Landing Page v2" }
    ]
  }
  ```
</ResponseExample>

***

## Clone Component

Create a copy of a component within the same brand. The clone gets a new auto-generated code and can optionally receive a new name.

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

<ParamField path="component" type="string" required>
  Numeric ID **or** `code` string of the component to clone
</ParamField>

<ParamField body="name" type="string">
  Name for the cloned component. If omitted, the trailing number in the original name is auto-incremented (e.g. `Hero 1` → `Hero 2`).
</ParamField>

```bash Request theme={null}
POST /api/brands/{brand}/components/{component}/clone
```

```bash cURL theme={null}
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/components/14/clone \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Guarantee Badge (Copy)" }'
```

<ResponseExample>
  ```json Response theme={null}
  {
    "component_id": 15
  }
  ```
</ResponseExample>

***

## Automated Component Provisioning

Provision a full set of reusable brand components in one run. Because [PUT does not update content](#update-component), the correct pattern for programmatic provisioning is to check whether each component exists first, then create it if it does not — not an upsert.

```python Python theme={null}
import requests

BRAND_ID = 42
BASE = f'https://app.elasticfunnels.io/api/brands/{BRAND_ID}'
CDN  = f'https://cdn.elasticfunnels.io/{BRAND_ID}/assets/media'
H    = {'EF-Access-Key': 'your_api_key_here', 'Content-Type': 'application/json'}


def get_existing_codes() -> set:
    """Fetch all component codes already in the brand."""
    r = requests.get(f'{BASE}/components/all?short=true', headers=H)
    r.raise_for_status()
    return {c['code'] for c in r.json()}


def create_component(code: str, name: str, html: str, css: str = '') -> dict:
    payload = {'name': name, 'code': code, 'html': html, 'css': css}
    r = requests.post(f'{BASE}/components', headers=H, json=payload)
    r.raise_for_status()
    comp = r.json()['pageComponent']
    print(f'  ✓  created {code} (id={comp["id"]})')
    return comp


COMPONENTS = [
    {
        'code': 'custom-refund-icon',
        'name': 'Refund Icon',
        'html': f'<div class="refund"><img src="{CDN}/refund-icon.png" alt="Money Back Guarantee"></div>',
        'css':  '.refund { max-width: 80px; margin: 0 auto; }',
    },
    {
        'code': 'custom-guarantee-badge',
        'name': '60-Day Guarantee Badge',
        'html': f'<div class="guarantee"><img src="{CDN}/guarantee-badge.png" alt="60-Day Money Back"></div>',
        'css':  '.guarantee { max-width: 200px; margin: 20px auto; text-align: center; }',
    },
    {
        'code': 'custom-trust-seal',
        'name': 'Trust Seal',
        'html': f'<div class="trust"><img src="{CDN}/trust-seal.svg" alt="Secure Checkout"></div>',
        'css':  '.trust { display: flex; justify-content: center; }',
    },
    {
        'code': 'custom-cc-icons',
        'name': 'Credit Card Icons',
        'html': f'<div class="cc"><img src="{CDN}/cc-icons.png" alt="Visa Mastercard Amex"></div>',
        'css':  '.cc { max-width: 260px; margin: 0 auto; }',
    },
]

print(f'Provisioning components for brand {BRAND_ID}…')
existing = get_existing_codes()

for comp in COMPONENTS:
    if comp['code'] in existing:
        print(f'  –  skipped {comp["code"]} (already exists)')
    else:
        create_component(**comp)

print('Done.')
```

```javascript Node.js theme={null}
const BRAND_ID = 42;
const BASE = `https://app.elasticfunnels.io/api/brands/${BRAND_ID}`;
const CDN  = `https://cdn.elasticfunnels.io/${BRAND_ID}/assets/media`;
const H    = { 'EF-Access-Key': API_KEY, 'Content-Type': 'application/json' };

async function getExistingCodes() {
  const res = await fetch(`${BASE}/components/all?short=true`, { headers: H });
  if (!res.ok) throw new Error(`Failed to fetch components: ${res.status}`);
  const data = await res.json();
  return new Set(data.map(c => c.code));
}

async function createComponent({ code, name, html, css = '' }) {
  const res = await fetch(`${BASE}/components`, {
    method: 'POST',
    headers: H,
    body: JSON.stringify({ name, code, html, css }),
  });
  if (!res.ok) throw new Error(`${code}: ${res.status} ${await res.text()}`);
  const { pageComponent } = await res.json();
  console.log(`  ✓  created ${code} (id=${pageComponent.id})`);
  return pageComponent;
}

const components = [
  { code: 'custom-guarantee-badge', name: '60-Day Guarantee Badge',
    html: `<div class="guarantee"><img src="${CDN}/guarantee-badge.png" alt="Guarantee"></div>`,
    css:  '.guarantee { max-width: 200px; margin: 20px auto; text-align: center; }' },
  { code: 'custom-trust-seal', name: 'Trust Seal',
    html: `<div class="trust"><img src="${CDN}/trust-seal.svg" alt="Secure"></div>`,
    css:  '.trust { display: flex; justify-content: center; }' },
];

const existing = await getExistingCodes();
for (const c of components) {
  if (existing.has(c.code)) {
    console.log(`  –  skipped ${c.code} (already exists)`);
  } else {
    await createComponent(c);
  }
}
```
