---
title: "Building custom recommendations with the Recommender API"
description: "Learn how to build a minimal but functional recommender UI using Luigi's Box Recommender API and your own JavaScript."
slug: quickstart/recommendations/building-custom
docKind: guide
hub: quickstart
---

## Introduction

This guide provides a comprehensive walkthrough for building a custom recommendation widget by calling the [Recommender API](/recommendations/api/) directly from your frontend. This approach gives you complete control over the final look and feel, allowing you to create a user experience that is perfectly tailored to your brand.

By the end of this guide, you will have a fully functional recommendation widget, powered by your own client-side JavaScript, and a clear understanding of how to implement the detailed analytics tracking required to make it effective.

:::caution
This is a **demonstration guide**, not production code. For most frontend integrations, Luigi's Box recommends using
[Recco.js](/recommendations/recco-js/), which provides a more robust and production-ready solution.
:::

### What you'll learn

- How to call the Recommender API directly from the frontend.
- How to render recommendation results from the API response.
- How to manually and correctly track analytics events, which is critical for model performance.

### Who is this guide for

- Developers building single-page applications or custom storefront UIs.
- Anyone evaluating the Recommender API for custom integration.

### Prerequisites

- Your Luigi's Box `TrackerId`.
- The ability to write and serve a standard HTML, CSS, and JavaScript file.
- The ability to make HTTP requests from your frontend code.
- An existing product page with a unique product ID available.

:::note[Runnable example HTML]
- **Events API analytics:** [recommender-api.html](/examples/recommendations/recommender-api.html)
- **DataLayer analytics:** [recommender-api-datalayer.html](/examples/recommendations/recommender-api-datalayer.html)

Right-click any link and choose "Save as" to download the HTML file for local inspection.
:::

## Step-by-step

### Step 1: Set up the HTML structure

Start by creating the basic structure for your widget. This will be a simple container on a product page where the recommended items will be displayed.

#### Example: basic HTML layout on a product page

```html
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Product Page</title>
  </head>
  <body>
    <h1>Rider Aqua Thong Slipper</h1>
    <p>ID: /rider-aqua-thong-slipper-blue-green-38/</p>

    <div id="recommender-container"></div>

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
      // --- DOM ELEMENTS ---
      const recommenderContainer = document.getElementById(
        "recommender-container",
      );

      // JS will go here
    </script>
  </body>
</html>
```

### Step 2: Understand the Recommender API request

To get recommendations, you send a `POST` request to the [Recommender API](/recommendations/api/) endpoint with a JSON payload.

#### Endpoint

`POST` `https://live.luigisbox.com/v1/recommend`

#### Required parameters

- `tracker_id`: Your Luigi's Box Tracker ID.

#### Request body (JSON array)

The body of your `POST` request is an array containing one or more recommendation request objects.

- `recommendation_type`: The identifier of the recommender model you want to use.
- `recommender_client_identifier`: A unique name for this specific widget, used for analytics.
- `item_ids`: An array of product IDs to base the recommendation on.
- `hit_fields`: An array of product attributes to return. (**Highly recommended**) Requesting only the fields you need (e.g., `title,url,price,image_link`) significantly improves performance by reducing the response size.
- `size`: The number of results to return.
- `user_id`: A unique user identifier for personalization.

#### Example: Recommender API request

`POST` `https://live.luigisbox.com/v1/recommend?tracker_id=111111-111111`

```json
[
  {
    "recommendation_type": "item_detail_complements_1",
    "recommender_client_identifier": "item_detail_complements_1",
    "item_ids": ["/rider-aqua-thong-slipper-blue-green-38/"],
    "size": 4,
    "hit_fields": ["title", "url", "price_amount", "image_link", "brand", "id"],
    "user_id": "123456789012345678"
  }
]
```

### Step 3: Understand the API response

The API responds with a JSON array containing a response object for each request you sent.

#### Example: Recommender API response

```json
[
  {
    "recommendation_id": "a24588e9-0664-4637-91d5-165313a6eac8",
    "recommender": "c01",
    "recommendation_type": "item_detail_complements_1",
    "recommender_client_identifier": "item_detail_complements_1",
    "hits": [
      {
        "url": "/complementary-item-789",
        "attributes": {
          "title": "Matching Item",
          "id": ["ITEM-789"],
          "price_amount": 35.5
        }
      }
    ]
  }
]
```

#### Key fields overview

- `recommendation_id`: Unique identifier for the recommendation.
- `recommendation_type`: Type of recommendation (e.g., "item_detail_complements_1").
- `recommender_client_identifier`: Client identifier for the specific widget.
- `hits`: Array of recommended product objects.

### Step 4: Fetch recommendations

This function builds the request, makes the `POST` call, and, most importantly, stores the necessary data for analytics tracking.

#### Example: fetching recommendations with axios and preparing for analytics

```javascript
// --- CONFIGURATION ---
const TRACKER_ID = "YOUR_TRACKER_ID";
const API_ENDPOINT = `https://live.luigisbox.com/v1/recommend?tracker_id=${TRACKER_ID}`;
const CLIENT_ID = String(Math.floor(Math.random() * 1e18));
const CURRENT_PRODUCT_ID = "/rider-aqua-thong-slipper-blue-green-38/";
let analyticsData = {}; // Object to hold data for tracking

// --- API CALL ---
async function getRecommendations() {
  const requestPayload = {
    recommendation_type: "item_detail_complements_1",
    recommender_client_identifier: "item_detail_complements_1",
    item_ids: [CURRENT_PRODUCT_ID],
    size: 4,
    hit_fields: ["title", "url", "price_amount", "image_link", "brand", "id"],
    user_id: CLIENT_ID,
  };

  try {
    const response = await axios.post(API_ENDPOINT, [requestPayload]);
    const recommendationData = response.data[0];

    if (recommendationData && recommendationData.hits.length > 0) {
      // Store all necessary data for the analytics payload
      analyticsData = {
        RecommendationId: recommendationData.recommendation_id,
        Recommender: recommendationData.recommender,
        Type: recommendationData.recommendation_type,
        RecommenderClientId: recommendationData.recommender_client_identifier,
        ItemIds: requestPayload.item_ids,
      };

      renderRecommendations(recommendationData.hits);
      trackRecommendationView(recommendationData.hits);
    }
  } catch (error) {
    console.error("Error fetching recommendations:", error);
  }
}
```

### Step 5: Render the results

This function takes the API response and generates the HTML for the product cards. This part is fully customizable to your needs.

#### Example: render recommendations

```javascript
function renderRecommendations(hits, title = "Complement Products") {
  const productsHTML = hits
    .map((hit) => {
      const { url, attributes } = hit;
      const imageUrl =
        attributes.image_link ||
        "https://placehold.co/400x400/eee/ccc?text=No+Image";
      const productTitle = attributes.title || "No Title Available";
      const brand = attributes.brand ? attributes.brand[0] : "";
      const price = attributes.price_amount
        ? `${attributes.price_amount} EUR`
        : "";
      const productId = attributes.id ? attributes.id[0] : url;

      return `
                <div class="product-card">
                    <a href="${url}" target="_blank" class="product-link" data-product-id="${productId}">
                        <img src="${imageUrl}" alt="${productTitle}" class="product-image" onerror="this.onerror=null;this.src='https://placehold.co/400x400/eee/ccc?text=No+Image';">
                        <div class="product-info">
                            <h3 class="product-title">${productTitle}</h3>
                            <p class="product-brand">${brand}</p>
                            <div class="product-price">${price}</div>
                        </div>
                    </a>
                </div>`;
    })
    .join("");

  recommenderContainer.innerHTML = `
        <h2>${title}</h2>
        <div class="recommender-grid">${productsHTML}</div>
    `;
}
```

### Step 6: Track analytics events manually

This is the most critical step. You must manually construct and send analytics 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.

#### The "view_item_list" event

After displaying the recommendations, you must immediately send a "view_item_list" event. This tells Luigi's Box which products were shown to the user.

```javascript+dataLayer
// DataLayer approach
function trackRecommendationView(hits) {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: "view_item_list",
      ecommerce: {
        item_list_name: "Recommendation",
        items: hits.map((hit, index) => ({
          item_id: hit.attributes.id ? hit.attributes.id[0] : hit.url, // Must match your catalog identity
          item_name: hit.attributes.title,
          index: index + 1,
          price: hit.attributes.price_amount,
          type: "item"
        })),
        filters: {
          "RecommenderClientId": analyticsData.RecommenderClientId,
          "RecommendationId": analyticsData.RecommendationId,
          "ItemIds": analyticsData.ItemIds,
          "Recommender": analyticsData.Recommender,
          "Type": analyticsData.Type,
        }
      }
    });
}
```

```javascript+API
// Events API approach
function trackRecommendationView(hits) {
    const viewPayload = {
        id: crypto.randomUUID(),
        type: "event",
        tracker_id: TRACKER_ID,
        client_id: CLIENT_ID,
        lists: {
            "Recommendation": {
                items: hits.map((hit, index) => ({
                    title: hit.attributes.title,
                    type: "item",
                    url: hit.attributes.id ? hit.attributes.id[0] : hit.url, // Object identity
                    position: index + 1,
                    price: hit.attributes.price_amount
                })),
                query: {
                    filters: {
                        RecommendationId: analyticsData.RecommendationId,
                        RecommenderClientId: analyticsData.RecommenderClientId,
                        ItemIds: analyticsData.ItemIds,
                        Recommender: analyticsData.Recommender,
                        Type: analyticsData.Type,
                    }
                }
            }
        }
    };
    sendAnalyticsEvent(viewPayload);
}
```

#### Breaking down the `query.filters` object

This object provides the full context for the recommendation and is required for analytics to work correctly.

- `RecommendationId`: (**Required**) The unique ID for this set of results. We get this from the `analyticsData` object we saved.
- `RecommenderClientId`: (**Required**) The unique name for this widget. We also get this from `analyticsData`.
- `ItemIds`: The list of product IDs used in the original request.
- `Recommender`: The name of the recommender model, from the API response.
- `Type`: The type of the recommender, also from the API response.

#### The "click" event

You must also track when a user clicks on a recommended item.

```javascript
// DataLayer approach
function trackClickEvent(productId) {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: "select_item",
    ecommerce: {
      items: [
        {
          item_id: productId,
        },
      ],
    },
  });
}
```

```javascript
// Events API approach
function trackClickEvent(productId) {
  const clickPayload = {
    id: crypto.randomUUID(),
    type: "click",
    tracker_id: TRACKER_ID,
    client_id: CLIENT_ID,
    action: {
      type: "click",
      resource_identifier: productId,
      recommendation_id: analyticsData.RecommendationId, // Link the click to the view
    },
  };
  sendAnalyticsEvent(clickPayload);
}
```

## Best practices

- **Analytics is not optional:** Sending `view_item_list` and `click` events is mandatory. Without them, the models cannot learn, and you will not see performance data in your dashboard.
- **Use a persistent `CLIENT_ID`:** For accurate tracking, the `CLIENT_ID` should be a persistent identifier stored in a long-term cookie or local storage.

## Next steps

- **Implement batching:** If you want to show multiple recommenders on a single page, send both request objects in the same API call. This improves performance and ensures you don't see duplicate products across widgets.
