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

# Banners Guide

> Read the Banners array from the Cludo Search API response, render banners above your search results in any CMS or front end, and track banner clicks for analytics.

Banners are promotional or informational blocks that appear above your search results when a query matches a trigger term. If you call the Search API directly, your site is responsible for rendering them and tracking clicks on them. This guide covers both.

## Prerequisites

* A banner configured for your engine in MyCludo, with a test trigger term you can search for
* A working Search API integration using [SiteKey credentials](/authentication)

## How banners work

Banners are configured in MyCludo, not in your code, and can also be managed through the [banner endpoints](/api-reference/list-banners-by-engine). Each banner has trigger terms and a block of HTML. When a search matches a trigger term, the Search API returns that banner in the `Banners` array of the response. Your site only needs to take that HTML and place it on the page.

<Steps>
  <Step title="Search as usual">
    Your site sends the visitor's query to the Search API, as it does today.
  </Step>

  <Step title="Read the Banners array">
    Read `Banners` from the same response. No extra request or parameter is needed.
  </Step>

  <Step title="Render the banner HTML">
    Insert each banner's `Banner` value into a container above your results.
  </Step>

  <Step title="Track banner clicks">
    Send a click event when a visitor clicks a banner, so it is counted in your analytics. See [Track banner clicks](#track-banner-clicks).
  </Step>
</Steps>

Banners are matched on the query, not on the results. A query can return zero documents and still return a banner.

***

## Find banners in the response

Your existing search request does not change. The examples below use a test term as the query.

<CodeGroup>
  ```bash cURL theme={null}
  # Use https://api-us1.cludo.com for the US region
  curl -X POST "https://api.cludo.com/api/v4/{customerId}/{engineId}/search" \
    -H "Authorization: SiteKey {token}" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "test term",
      "page": 1,
      "perPage": 10,
      "responseType": "JsonObject"
    }'
  ```

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

  # Use https://api-us1.cludo.com for the US region
  url = "https://api.cludo.com/api/v4/{customerId}/{engineId}/search"
  headers = {
      "Authorization": "SiteKey {token}",
      "Content-Type": "application/json",
  }
  payload = {
      "query": "test term",
      "page": 1,
      "perPage": 10,
      "responseType": "JsonObject",
  }

  response = requests.post(url, json=payload, headers=headers)
  data = response.json()
  print(data["Banners"])
  ```

  ```javascript JavaScript theme={null}
  // Use https://api-us1.cludo.com for the US region
  const response = await fetch(
    "https://api.cludo.com/api/v4/{customerId}/{engineId}/search",
    {
      method: "POST",
      headers: {
        "Authorization": "SiteKey {token}",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        query: "test term",
        page: 1,
        perPage: 10,
        responseType: "JsonObject",
      }),
    },
  );

  const data = await response.json();
  console.log(data.Banners);
  ```

  ```typescript TypeScript theme={null}
  // Use https://api-us1.cludo.com for the US region
  interface Banner {
    Id: number;
    Name: string;
    Banner: string;
  }

  interface SearchResponse {
    Banners?: Banner[];
    QueryId: string;
  }

  const response = await fetch(
    "https://api.cludo.com/api/v4/{customerId}/{engineId}/search",
    {
      method: "POST",
      headers: {
        "Authorization": "SiteKey {token}",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        query: "test term",
        page: 1,
        perPage: 10,
        responseType: "JsonObject",
      }),
    },
  );

  const data: SearchResponse = await response.json();
  console.log(data.Banners);
  ```
</CodeGroup>

Response (trimmed):

```json theme={null}
{
  "TypedDocuments": [],
  "TotalDocument": 12,
  "Banners": [
    {
      "Id": 1234,
      "Name": "Example banner",
      "Banner": "<a href=\"https://www.example.com/help\" style=\"...\"> ... </a>"
    }
  ],
  "QueryId": "a3f2c1b8-4e9d-4a1c-9b2e-7f4d6e8a1c3d",
  "GenerativeAnswerAvailable": true
}
```

<Note>
  The API reference example shows lowercase banner keys (`id`, `name`). The live response uses `Id`, `Name` and `Banner`. Read `Banner` for the HTML.
</Note>

The `Banner` value is self-contained HTML with inline styles, so your site CSS does not need to change. This is a sample banner returned for a test term, not a recommended design:

```html theme={null}
<a href="https://www.example.com/help" style="--panel: #341669; --stripe: #deb7ff; display: block; text-decoration: none !important; color: inherit !important; font-family: system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;">
  <div style="background: var(--panel); border-radius: 8px; overflow: hidden; display: flex; align-items: stretch;">
    <div style="width: 6px; background: var(--stripe); flex-shrink: 0;"></div>
    <div style="flex: 1; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px 20px; padding: 18px 22px;">
      <div style="flex: 1 1 220px;">
        <div style="color: var(--stripe); font-size: 12px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 6px;">Need help with an order?</div>
        <div style="color: #ffffff; font-size: 22px; font-weight: 400; line-height: 1.25;">Contact our support team</div>
      </div>
      <span style="display: inline-block !important; flex-shrink: 0; border: 1px solid #ffffff; color: #ffffff !important; font-size: 15px; font-weight: 600; padding: 12px 22px; border-radius: 999px;">Get help &rarr;</span>
    </div>
  </div>
</a>
```

See the [Search API reference](/api-reference/v4/search/search) for all request and response fields.

***

## Render banners on the page

Add a container where the banner should appear, above your results list, then pass it the response your search code already receives. The same approach works in any CMS or framework: put the container in the search results template and load your script through the CMS's normal asset mechanism.

```html theme={null}
<div id="cludo-banner"></div>
```

<CodeGroup>
  ```javascript JavaScript theme={null}
  // Call this with the JSON your search code already receives.
  function renderBanners(data, container) {
    const banners = data.Banners || [];
    container.innerHTML = banners
      .map((b) => `<div data-banner-id="${b.Id}">${b.Banner}</div>`)
      .join("");
  }

  // Example: inside your existing search handler
  // const data = await response.json();
  // renderBanners(data, document.getElementById("cludo-banner"));
  ```

  ```typescript TypeScript theme={null}
  // Call this with the JSON your search code already receives.
  function renderBanners(data: SearchResponse, container: HTMLElement): void {
    const banners = data.Banners ?? [];
    container.innerHTML = banners
      .map((b) => `<div data-banner-id="${b.Id}">${b.Banner}</div>`)
      .join("");
  }

  // Example: inside your existing search handler
  // const data: SearchResponse = await response.json();
  // renderBanners(data, document.getElementById("cludo-banner")!);
  ```

  ```python Python theme={null}
  # Server-side: build the banner markup in your template or controller.
  def render_banners(data):
      html = ""
      for banner in data.get("Banners", []):
          # Output as raw HTML, not escaped
          html += f'<div data-banner-id="{banner["Id"]}">{banner["Banner"]}</div>'
      return f'<div id="cludo-banner">{html}</div>'
  ```
</CodeGroup>

Each banner is wrapped in an element carrying its `Id` as `data-banner-id`. You need that ID to track clicks.

* **Clear it on each search.** Setting `innerHTML` replaces the previous banner. If a new query returns no banners, the container empties, which is what you want.
* **Output as raw HTML.** Use your framework's raw HTML output (for example `innerHTML`, `v-html` or `dangerouslySetInnerHTML`). Escaping the value shows the markup as text, and some sanitizers strip inline styles and break the design.

<Warning>
  Banner HTML is authored in MyCludo by your own team. Keep MyCludo access limited to people you trust.
</Warning>

***

## Track banner clicks

A direct call to the Search endpoint does not record analytics, and banner clicks are not tracked automatically. If you skip this step, banner clicks will not appear in your reports. Send two kinds of event:

* **The search itself**, with [Track Search Query](/api-reference/track-search-query) (`querylog`), so the search that triggered the banner is counted.
* **Each banner click**, with [Track Result Click](/api-reference/track-result-click) (`clicklog`), using the banner-specific values below.

| Field | Value for a banner click |
| - | - |
| `ls` | `banner` (use `searchresult` for organic results) |
| `cloi` | The banner's `Id` from the `Banners` array (required when `ls` is `banner`) |
| `qid` | The `QueryId` from the same search response |
| `sw` | The query the visitor searched for |
| `clurl` | The URL the banner links to |
| `title` | The banner's `Name` |

<CodeGroup>
  ```bash cURL theme={null}
  # Use https://api-us1.cludo.com for the US region
  curl -X POST "https://api.cludo.com/api/v3/{customerId}/{engineId}/search/pushstat/clicklog" \
    -H "Content-Type: application/json" \
    -d '{
      "ls": "banner",
      "cloi": "1234",
      "sw": "test term",
      "qid": "a3f2c1b8-4e9d-4a1c-9b2e-7f4d6e8a1c3d",
      "clurl": "https://www.example.com/help",
      "title": "Example banner"
    }'
  ```

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

  BASE_URL = "https://api.cludo.com"  # Use https://api-us1.cludo.com for the US region


  def track_banner_click(customer_id, engine_id, query_id, query, banner_id, banner_name, click_url):
      requests.post(
          f"{BASE_URL}/api/v3/{customer_id}/{engine_id}/search/pushstat/clicklog",
          json={
              "ls": "banner",
              "cloi": str(banner_id),
              "sw": query,
              "qid": query_id,
              "clurl": click_url,
              "title": banner_name,
          },
      )
  ```

  ```javascript JavaScript theme={null}
  const BASE_URL = "https://api.cludo.com";
  // Use https://api-us1.cludo.com for the US region

  // Call once per search, after renderBanners(data, container).
  // Assigning onclick replaces the handler from the previous search.
  function trackBannerClicks(data, query, container, customerId, engineId) {
    const banners = new Map((data.Banners || []).map((b) => [String(b.Id), b]));

    container.onclick = (event) => {
      const link = event.target.closest("a");
      const wrapper = event.target.closest("[data-banner-id]");
      if (!link || !wrapper) return;

      const banner = banners.get(wrapper.dataset.bannerId);
      fetch(
        `${BASE_URL}/api/v3/${customerId}/${engineId}/search/pushstat/clicklog`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            ls: "banner",
            cloi: wrapper.dataset.bannerId,
            sw: query,
            qid: data.QueryId,
            clurl: link.href,
            title: banner ? banner.Name : "",
          }),
          keepalive: true, // lets the request finish if the click navigates away
        },
      );
    };
  }
  ```

  ```typescript TypeScript theme={null}
  const BASE_URL = "https://api.cludo.com";
  // Use https://api-us1.cludo.com for the US region

  // Call once per search, after renderBanners(data, container).
  // Assigning onclick replaces the handler from the previous search.
  function trackBannerClicks(
    data: SearchResponse,
    query: string,
    container: HTMLElement,
    customerId: number,
    engineId: number,
  ): void {
    const banners = new Map(
      (data.Banners ?? []).map((b): [string, Banner] => [String(b.Id), b]),
    );

    container.onclick = (event: MouseEvent) => {
      const target = event.target as HTMLElement;
      const link = target.closest("a");
      const wrapper = target.closest<HTMLElement>("[data-banner-id]");
      if (!link || !wrapper) return;

      const banner = banners.get(wrapper.dataset.bannerId ?? "");
      fetch(
        `${BASE_URL}/api/v3/${customerId}/${engineId}/search/pushstat/clicklog`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            ls: "banner",
            cloi: wrapper.dataset.bannerId,
            sw: query,
            qid: data.QueryId,
            clurl: link.href,
            title: banner ? banner.Name : "",
          }),
          keepalive: true, // lets the request finish if the click navigates away
        },
      );
    };
  }
  ```
</CodeGroup>

The tracking body uses flat key-value pairs. Do **not** wrap them in a `values` object. See the [Analytics Guide](/guides/analytics) for the full list of parameters, the Search-as-you-type rules and server-side IP forwarding.

***

## Troubleshooting

| Symptom | Check |
| - | - |
| `Banners` is empty | Confirm the banner's trigger terms in MyCludo match the query, and that the banner is assigned to your engine. |
| Banner renders unstyled | Check your sanitizer or theme is not stripping inline `style` attributes. |
| Banner HTML shows as text | The value is being escaped. Output it as raw HTML. |
| Banner clicks missing from reports | Confirm `clicklog` is sent with `ls` set to `banner` and `cloi` set to the banner `Id`, and that `qid` is the `QueryId` from the same response. |

For other API errors, see [Troubleshooting](/troubleshooting).


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