---
title: "Getting query suggestions via the Autocomplete API"
description: "Learn how to directly call the Autocomplete API to retrieve suggestion data for a custom UI, which requires you to manually handle API requests and implement analytics tracking."
slug: quickstart/autocomplete/query-suggestions
docKind: guide
hub: quickstart
---

## Introduction

This guide is for developers who need full control over the look and feel of their autocomplete suggestions or are integrating in a non-web environment, such as a mobile application. By calling the [Autocomplete API](/autocomplete/api/v2/autocomplete/) directly, you can fetch suggestion data and render it in any custom UI you design.

Unlike the [Autocomplete.js](/autocomplete/autocomplete-js/) library, this method requires you to handle the API requests manually, render the results, and, most importantly, implement the analytics tracking for user interactions.

### What you'll learn

- How to make a GET request to the [Autocomplete API](/autocomplete/api/v2/autocomplete/) endpoint.
- The required parameters to send with your request to get relevant suggestions.
- How to understand the basic structure of the JSON response.
- The critical importance of sending manual analytics events when using the API directly.

### Who is this guide for

- Developers who are building a custom autocomplete UI on their website.
- Mobile developers (iOS, Android) who are integrating search suggestions.
- Developers who are sending requests from a backend server.

### Prerequisites

Before you start, please ensure you have the following in place:

- The ability to make HTTP `GET` requests from your application or server.
- Your Luigi's Box `trackerId`.

:::note[Runnable example HTML]
Open or download the full standalone sample: [query-suggestions-datalayer.html](/examples/autocomplete/query-suggestions-datalayer.html) — includes DataLayer analytics wiring.
:::

## Step-by-step

The process involves three conceptual steps: making a request to the [API](/autocomplete/api/v2/autocomplete/) with the user's query, understanding the response, and then manually sending analytics events to ensure the system can learn from user behavior.

### Step 1: Set up the HTML structure

First, you need the basic HTML elements: an input field for the user to type in and a container where the results will be displayed.

```html
<div class="container">
    <input type="text" id="search-input" placeholder="Search for products, categories...">
    <div id="autocomplete-results"></div>
</div>
```

This snippet provides a search input with the ID `search-input` and an empty `div` with the ID `autocomplete-results` where we will dynamically render the suggestions.

### Step 2: Make the API request

As a user types into your search box, you will make a `GET` request to the following endpoint. It is recommended to debounce this request (e.g., wait 200-300ms after the user stops typing) to avoid sending excessive requests.

#### Endpoint

`GET` `https://live.luigisbox.com/autocomplete/v2`

#### Required parameters

- **`tracker_id`:** Your unique site identifier.
- **`q`:** The user's current input from the search box.
- **`type`:** A comma-separated list of the content type you want suggestions for, along with the quantity for each (e.g, `product:6,category:3,query:5`).

#### Optional parameters (recommended)

- **`hit_fields`:** A comma-separated list of attributes you need (e.g., title,url,price,image_link). Using this parameter is highly recommended to keep the API response fast and small by only fetching the data you will display.

#### Example

```javascript
// CONFIGURATION
const TRACKER_ID = "YOUR_TRACKER_ID"; // Replace with your actual Tracker ID
const AUTOCOMPLETE_API_URL = "https://live.luigisbox.com/autocomplete/v2";
const searchInput = document.getElementById('search-input');

const debounce = (func, delay) => {
  let timeout;

  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), delay);
  }
}

const getSuggestions = async (query) => {
  if (!query) {
    // Clear results if input is empty
    document.getElementById('autocomplete-results').innerHTML = '';
    return;
  }

  try {
    const response = await axios.get(AUTOCOMPLETE_API_URL, {
        params: {
            tracker_id: TRACKER_ID,
            q: query,
            type: 'product:5,category:3,query:5',
            hit_fields: 'title,url,price,image_link'
        }
    });

    const hits = response.data.hits; // We will render these hits in the next step
  } catch (error) {
    console.error("Failed to fetch autocomplete suggestions:", error);
  }
};

searchInput.addEventListener('input', debounce(e => {
    getSuggestions(e.target.value);
}, 300));
```

This code sets up the necessary configuration and an event listener on our search input. When the user types, it calls the `getSuggestions` function, which makes a GET request to the Autocomplete API with the required parameters (`tracker_id`, `q`, `type`) and the recommended `hit_fields` parameter.

### Step 3: Understand the JSON response and render it

The API will return a JSON object where the most important part is the `hits` array. Each object in this array is a suggestion containing its `type` (e.g., "item", "category") and an `attributes` object with details like `title`, `price`, and `image_link`.

#### Example: Simplified JSON response

```json
{
  "hits": [
    {
      "url": "https://yourshop.com/products/white-v-neck",
      "attributes": {
        "title": "<em>White</em> Shirt V-Neck",
        "price": "25 EUR",
        "image_link": "https://yourshop.com/images/shirt.jpg"
      },
      "type": "item",
      "updated_at": "..."
    },
    {
      "attributes": {
        "title": "<em>White</em> T-Shirts"
      },
      "type": "category"
    }
  ]
}
```

You will need to parse this JSON and use the data within the attributes of each hit to render your custom autocomplete UI. Note that the API may return highlighted text (using `<em>` tags) within fields like title.

#### Example: Rendering results

Now that we are fetching the suggestions, we need a function to render them into our `autocomplete-results` container. This function will parse the `hits` array from the API response and build the HTML for the suggestions.

```javascript
function renderResults(hits) {
    const resultsContainer = document.getElementById('autocomplete-results');
    resultsContainer.innerHTML = ''; // Clear previous results
    if (!hits || hits.length === 0) return;

    // Group results by type (e.g., 'item', 'category')
    const groupedResults = hits.reduce((acc, hit) => {
        (acc[hit.type] = acc[hit.type] || []).push(hit);
        return acc;
    }, {});

    const groupTitleMap = { item: 'Products', category: 'Categories', query: 'Popular Searches' };

    for (const type in groupedResults) {
        const groupDiv = document.createElement('div');
        // ... code to create and append group title ...

        groupedResults[type].forEach(item => {
            const itemDiv = document.createElement('div');
            itemDiv.className = 'result-item';
            // Set data attributes for analytics tracking
            itemDiv.dataset.itemId = item.url || item.attributes.title;
            itemDiv.dataset.itemType = item.type;

            // Build the inner HTML for the suggestion item
            let innerHTML = '';
            if (item.attributes.image_link) {
                innerHTML += `<img src="${item.attributes.image_link}" alt="">`;
            }
            innerHTML += `<div class="title">${item.attributes.title}</div>`;
            if (item.attributes.price) {
                innerHTML += `<div class="price">${item.attributes.price}</div>`;
            }
            itemDiv.innerHTML = innerHTML;
            groupDiv.appendChild(itemDiv);
        });
        resultsContainer.appendChild(groupDiv);
    }
}

// Now, call this function inside getSuggestions after fetching the data:
// (Inside the `getSuggestions` try block)
// const hits = response.data.hits;
// renderResults(hits); // Add this line
```

This `renderResults` function takes the `hits` array, groups the suggestions by type (e.g., "item", "category"), and dynamically creates HTML elements for each suggestion, including images and prices. You should call this function from within your `getSuggestions` function after you receive a successful API response.

### Step 4: Implement manual analytics tracking (critical)

This is the most important difference compared to using [Autocomplete.js](/autocomplete/autocomplete-js/). When you use the API directly, analytics are **NOT** tracked automatically. You are responsible for sending the events.

You have two options for sending analytics: the **DataLayer Collector** (recommended for web integrations that already use a `dataLayer`) or the **Events API** (recommended for backend or mobile integrations). If you choose the DataLayer Collector, make sure the [LBX script](/lbx-script/) is included on your page.

Without these manual analytics events, Luigi's Box cannot learn which suggestions are effective, and the quality of your autocomplete results will not improve over time.

#### Track the autocomplete view event

Immediately after you display the suggestions from the API response, you must send an Autocomplete list event to Luigi's Box. This tells the system which suggestions were shown to the user.

```javascript+dataLayer
// Send VIEW event after rendering results
// (Add this inside the `getSuggestions` function, after calling renderResults)
if (hits && hits.length > 0) {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: "view_item_list",
      ecommerce: {
        item_list_name: "Autocomplete", // This name is critical
        search_term: query,
        items: hits.map((hit, index) => ({
          item_id: hit.url || hit.attributes.title, // Must match your catalog ID
          item_name: hit.attributes.title,
          index: index + 1,
        }))
      }
    });
}
```

```javascript+API
const ANALYTICS_API_URL = "https://api.luigisbox.com/";
const CLIENT_ID = Math.floor(Math.random() * 1e18); // A persistent user ID is better

async function sendAnalyticsEvent(payload) {
    try {
        axios.post(ANALYTICS_API_URL, payload);
        console.log('Analytics event sent:', payload.type);
    } catch (error) {
        console.error('Failed to send analytics event:', error);
    }
}

// Send VIEW event after rendering results
// (Add this inside the `getSuggestions` function, after calling renderResults)
if (hits && hits.length > 0) {
    const analyticsPayload = {
        id: uuid.v4(),
        type: "event",
        tracker_id: TRACKER_ID,
        client_id: CLIENT_ID,
        lists: {
            "Autocomplete": {
                query: { string: query },
                items: hits.map((hit, index) => ({
                    title: hit.attributes.title,
                    url: hit.url || hit.attributes.title,
                    position: index + 1
                }))
            }
        }
    };
    sendAnalyticsEvent(analyticsPayload);
}
```

#### Track click events

When a user clicks on one of your rendered suggestions, you must send a click event to report this interaction.

```javascript+dataLayer
// Send CLICK event when a user selects a suggestion
document.getElementById('autocomplete-results').addEventListener('click', (e) => {
    const resultItem = e.target.closest('.result-item');
    if (resultItem) {
        const itemId = resultItem.dataset.itemId;
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
          event: "select_item",
          ecommerce: {
            items: [
              {
                item_id: itemId // The ID of the clicked item
              }
            ]
          }
        });

        // Hide results after click
        document.getElementById('autocomplete-results').innerHTML = '';
    }
});
```

```javascript+API
// Send CLICK event when a user selects a suggestion
document.getElementById('autocomplete-results').addEventListener('click', (e) => {
    const resultItem = e.target.closest('.result-item');
    if (resultItem) {
        const itemId = resultItem.dataset.itemId;

        const clickPayload = {
            id: uuid.v4(),
            type: "click",
            tracker_id: TRACKER_ID,
            client_id: CLIENT_ID,
            action: {
                type: "click",
                resource_identifier: itemId
            }
        };
        sendAnalyticsEvent(clickPayload);

        // Hide results after click
        document.getElementById('autocomplete-results').innerHTML = '';
    }
});
```

#### Tracking no results

:::caution
You must send an analytics event even when an autocomplete query returns **zero results**. This helps Luigi's Box identify queries that need improvement. Send the same autocomplete view event but with an empty items array.
:::

```javascript+dataLayer
// DataLayer Collector: No Results for Autocomplete
if (!hits || hits.length === 0) {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: "view_item_list",
      ecommerce: {
        item_list_name: "Autocomplete",
        search_term: query,
        items: [] // Empty array for no results
      }
    });
}
```

```javascript+API
// Events API: No Results for Autocomplete
if (!hits || hits.length === 0) {
    const analyticsPayload = {
        id: uuid.v4(),
        type: "event",
        tracker_id: TRACKER_ID,
        client_id: CLIENT_ID,
        lists: {
            "Autocomplete": {
                query: { string: query },
                items: [] // Empty array for no results
            }
        }
    };
    sendAnalyticsEvent(analyticsPayload);
}
```

### Step 5 (optional): Add and track an "add to cart" button

If you show products (`item` type) in your autocomplete, you can add an "Add to Cart" button directly into the results. This requires modifying your rendering function and updating your click listener to send a specific "add to cart" analytics event.

**1. Add the button to your `renderResults` function:**

First, update your `renderResults` function (from Step 3) to append a button for any result of type `item`.

```javascript
// Inside your renderResults function...

groupedResults[type].forEach(item => {
    const itemDiv = document.createElement('div');
    itemDiv.className = 'result-item';
    itemDiv.dataset.itemId = item.url || item.attributes.title;
    itemDiv.dataset.itemType = item.type;
    // Store all item data needed for analytics
    itemDiv.dataset.itemName = item.attributes.title;
    itemDiv.dataset.itemPrice = item.attributes.price;

    // ... (your existing code to build innerHTML)

    itemDiv.innerHTML = innerHTML;

    // --- START: Add to Cart Button ---
    // Only add the button for products (type: "item")
    if (item.type === 'item') {
        const addToCartBtn = document.createElement('button');
        addToCartBtn.className = 'add-to-cart-btn';
        // Add a data-action to identify this button in the click listener
        addToCartBtn.dataset.action = 'add-to-cart';
        addToCartBtn.textContent = 'Add to Cart';
        itemDiv.appendChild(addToCartBtn);
    }
    // --- END: Add to Cart Button ---

    groupDiv.appendChild(itemDiv);
});
```

**2. Update your click listener to handle the "add to cart" button:**

Next, modify the click listener (from Step 4) to distinguish between a user clicking the item (to navigate) and clicking the button (to add to cart). We use `e.target.closest()` to find what was clicked.

```javascript
// This is your main click listener for the results container
document.getElementById('autocomplete-results').addEventListener('click', (e) => {

    // Check if the "Add to Cart" button was clicked
    const cartButton = e.target.closest('.add-to-cart-btn[data-action="add-to-cart"]');
    if (cartButton) {
        // Stop the event from bubbling up to the .result-item
        // This prevents the "select_item" click from firing too
        e.stopPropagation();

        const resultItem = e.target.closest('.result-item');

        console.log("Add to cart clicked:", resultItem.dataset.itemId);
        // Here, you would trigger your site's "add to cart" logic

        // Send the specific "add to cart" analytics event (see next step)
        sendAddToCartAnalytics(resultItem.dataset);
        return; // We're done
    }

    // If not the cart button, check if a regular item was clicked
    const resultItem = e.target.closest('.result-item');
    if (resultItem) {
        const itemId = resultItem.dataset.itemId;

        // This is your ORIGINAL click event logic from Step 4
        const clickPayload = {
            id: uuid.v4(),
            type: "click",
            tracker_id: TRACKER_ID,
            client_id: CLIENT_ID,
            action: {
                type: "click",
                resource_identifier: itemId
            }
        };
        sendAnalyticsEvent(clickPayload);

        // Hide results after click
        document.getElementById('autocomplete-results').innerHTML = '';
    }
});
```

**3. Implement the `sendAddToCartAnalytics` function:**

Finally, create the function to send the analytics event.

```javascript+dataLayer
function sendAddToCartAnalytics(itemData) {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: "add_to_cart",
      ecommerce: {
        currency: "EUR", // Adjust currency as needed
        value: itemData.itemPrice,
        items: [
          {
            item_id: itemData.itemId,
            item_name: itemData.itemName,
          }
        ]
      }
    });
}
```

```javascript+API
function sendAddToCartAnalytics(itemData) {
    const cartClickPayload = {
        id: uuid.v4(),
        type: "click",
        tracker_id: TRACKER_ID,
        client_id: CLIENT_ID,
        action: {
            type: "add-to-cart", // Custom action type
            resource_identifier: itemData.itemId,
        }
    };
    sendAnalyticsEvent(cartClickPayload);
}
```

## Best practices

- **Avoid proxying requests:** For the best performance and lowest latency, make the API calls directly from the client-side (the user's browser or mobile app). Avoid proxying requests through your own backend server.
- **Use `hit_fields`:** To minimize the response size and improve speed, use the optional `hit_fields` parameter in your API call to request only the attributes you actually need to display (e.g., `&hit_fields=title,price,image_link`).
- **Use DNS prefetch for web:** If you are integrating on a website, add `<link rel="dns-prefetch" href="//live.luigisbox.com">` to your HTML `<head>` to speed up the first request.

## Next steps

- **Show top items on focus:** Learn how to show Top Items on focus by referring to our ["Implementing top items with the API"](/quickstart/autocomplete/top-items-api/) guide.
