Quickstart: Your first API-powered product listing

Introduction

This guide provides the fastest way to create a basic Product listing Page (PLP) by calling the Luigi's Box API directly. You will learn how to make a simple API call to fetch all products belonging to a specific category and render them in a clean grid using client-side JavaScript.

By the end of this guide, you will have a functional, single-file HTML page that displays products from your catalog, giving you a solid foundation for a custom integration. See full example.

!
Warning

This is a demonstration guide, not production code. In real-world application, for frontend integration, Luigi's box recommends using Search.js, which provides a more robust, maintainable, and production-ready code.

The recommended way to use the Product listing API is through your own backend proxy.

What you'll learn

  • How to make a basic Product listing API call.
  • How to parse the JSON response and render the product results.
  • How to track the necessary analytics events for the AI to learn.

Who is this guide for

  • Developers new to the Luigi's Box Product listing API.
  • Developers who want to understand the core API mechanics before building a full-featured integration.

Prerequisites

  • Your Luigi's Box TrackerId.
  • The ability to write and serve a standard HTML, CSS, and JavaScript file.
  • A product catalog synced with Luigi's Box that has filterable attributes (e.g., category).

Step-by-step

Step 1: Set up the HTML structure

Start by creating the basic structure of your listing page. This will include placeholders for the product grid, the filter sidebar, and the pagination controls. Styling is omitted for clarity.

Example: Basic HTML layout

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Product Listing Demo</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
    <div>
        <h1 id="page-title"></h1>
        <div>
            <aside id="facets-container"></aside>
            <main>
                <div id="product-grid"></div>
                <div id="pagination-container"></div>
            </main>
        </div>
    </div>

    <script>
        // --- DOM ELEMENTS ---
        const productGrid = document.getElementById("product-grid");
        const pageTitle = document.getElementById("page-title");
        const facetsContainer = document.getElementById("facets-container");
        const paginationContainer = document.getElementById("pagination-container");

        // --- STATE MANAGEMENT ---
        let activeFilters = {};
        let currentPage = 1;

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

Step 2: Understand the Product listing API request

To get products for a listing, you send a GET request to the Search API endpoint, omitting the q (query) parameter.

Endpoint

GET https://live.luigisbox.com/search

Required parameters

  • tracker_id: Your Luigi's Box Tracker ID
  • f[]: The filter that defines the product set (e.g., category:Kalimbas)
  • plp: Critical. This tells Luigi's Box this is a Product listing page and which filter key to use for merchandising. Its value must match the key in your f[] filter (e.g., plp=category)

Optional parameters

  • hit_fields: A comma-separated list of product attributes to return (e.g., title,url,price,image_link).
  • facets: A comma-separated list of attributes for which you want to receive filter options (e.g., brand,price_amount).
  • size: The number of results per page.
  • page: The page number for pagination.

Example: Product listing API request URL

GET https://live.luigisbox.com/search?tracker_id=<YOUR_TRACKER_ID>&f[]=type:product&f[]=category:Guitars&plp=category&facets=brand,price_amount&hit_fields=title,url,price_amount,image_link,brand,id&page=1

Step 3: Understand the API response

The API responds with a JSON object containing everything needed to render the page.

Example: Product listing API response

{
  "results": {
    "total_hits": 17,
    "hits": [
      {
        "url": "/cascha-hh-2145-mahagony-10-kalimba/",
        "attributes": {
          "id": ["2377383"],
          "title": "Cascha HH 2145 Mahagony 10 Kalimba",
          "brand": ["Cascha"],
          "price_amount": 39.9,
          "image_link": "https://cdn.myshoptet.com/usr/demoshop.luigisbox.com/user/shop/detail/1777383_cascha-hh-2145-mahagony-10-kalimba.jpg?673488c5"
        },
        "type": "product"
      },
      {
        "url": "/bolf-kalimbas-chroma-2-row-chromatic-21-kalimba/",
        "attributes": {
          "id": ["2373271"],
          "title": "Bolf Kalimbas Chroma 2-Row Chromatic 21 Kalimba",
          "brand": ["Bolf Kalimbas"],
          "price_amount": 149,
          "image_link": "https://cdn.myshoptet.com/usr/demoshop.luigisbox.com/user/shop/detail/1793271_bolf-kalimbas-chroma-2-row-chromatic-21-kalimba.jpg?673488c5"
        },
        "type": "product"
      }
    ],
    "facets": [
      {
        "name": "brand",
        "type": "text",
        "values": [
          { "value": "Cascha", "hits_count": 4 },
          { "value": "Bolf Kalimbas", "hits_count": 13 }
        ]
      },
      {
        "name": "price_amount",
        "type": "float",
        "values": [
          { "value": "19.9|80.0", "hits_count": 12 },
          { "value": "80.0|150.0", "hits_count": 5 }
        ]
      }
    ]
  },
  "next_page": "https://live.luigisbox.com/search?tracker_id=...&page=2"
}

Key fields overview

  • results.total_hits: The total number of products found, used for building pagination.
  • results.hits: An array of the product results for the current page.
  • results.facets: An array of filter groups (e.g., "brand," "price_amount") with available values and counts.

Step 4: Fetch Product listing results

This function calls the API with the current page and active filters, then invokes the rendering functions.

// --- CONFIGURATION ---
const TRACKER_ID = "YOUR_TRACKER_ID"; // Replace with your actual Tracker ID
const CATEGORY_NAME = "Guitars";
const API_ENDPOINT = "https://live.luigisbox.com/search";
const RESULTS_PER_PAGE = 6;

// --- API CALL ---
async function getPLPResults(page = 1, filters = {}) {
    currentPage = page;
    activeFilters = filters;

    const params = {
        tracker_id: TRACKER_ID,
        'f[]': [
            'type:product',
            `category:${CATEGORY_NAME}`
        ],
        plp: 'category',
        hit_fields: 'title,brand,image_link,url,id',
        facets: 'brand,price_amount',
        size: RESULTS_PER_PAGE,
        page: currentPage
    };

    for (const key in activeFilters) {
        activeFilters[key].forEach(value => {
            params['f[]'].push(`${key}:${value}`);
        });
    }

    try {
        const response = await axios.get(API_ENDPOINT, { params });
        const { hits, total_hits, facets } = response.data.results;

        renderProducts(hits, total_hits);
        renderFacets(facets);
        renderPagination(total_hits);
        updateURL(currentPage, activeFilters);
        trackListView(hits, CATEGORY_NAME, activeFilters);

    } catch (error) {
        console.error("Error fetching products:", error);
        productGrid.innerHTML = "<p>Sorry, there was an error loading products.</p>";
    }
}

Step 5: Render results, facets, and pagination

These functions take the API response and generate the HTML for the page.

// --- RENDERING FUNCTIONS ---
function renderProducts(hits = [], total_hits = 0) {
    pageTitle.textContent = `Products in ${CATEGORY_NAME} (${total_hits})`;

    if (hits.length === 0) {
        productGrid.innerHTML = "<p>No products found with the selected filters.</p>";
        return;
    }

    productGrid.innerHTML = hits.map(product => {
        const imageUrl = product.attributes.image_link || `https://placehold.co/300x200/eee/ccc?text=No+Image`;
        const title = product.attributes.title || 'Untitled';
        const brand = (product.attributes.brand && product.attributes.brand[0]) || 'No Brand';
        const productId = (product.attributes.id && product.attributes.id[0]) ? product.attributes.id[0] : null;
        const productIdAttribute = productId ? `data-product-id="${productId}"` : '';

        return `
            <div style="border: 1px solid #ccc; padding: 1rem; margin-bottom: 1rem;">
                <a href="${product.url}" class="product-link" ${productIdAttribute}>
                    <img src="${imageUrl}" alt="${title}" width="200" onerror="this.onerror=null;this.src='https://placehold.co/300x200/eee/ccc?text=No+Image';">
                </a>
                <div>
                    <h3>${title}</h3>
                    <p>${brand}</p>
                </div>
            </div>
        `;
    }).join('');
}

function renderFacets(facets = []) {
    facetsContainer.innerHTML = facets.map(facet => {
        const values = facet.values.map(val => {
            const isChecked = (activeFilters[facet.name] || []).includes(val.value);
            return `
                <li>
                    <label>
                        <input type="checkbox" name="${facet.name}" value="${val.value}" ${isChecked ? 'checked' : ''}>
                        ${val.value} <span>(${val.hits_count})</span>
                    </label>
                </li>
            `;
        }).join('');
        return `
            <div>
                <h3>${facet.name}</h3>
                <ul style="list-style: none; padding: 0;">${values}</ul>
            </div>
        `;
    }).join('');
}

function renderPagination(totalHits) {
    const totalPages = Math.ceil(totalHits / RESULTS_PER_PAGE);
    paginationContainer.innerHTML = "";
    if (totalPages <= 1) return;

    for (let i = 1; i <= totalPages; i++) {
        const button = document.createElement('button');
        button.textContent = i;
        button.dataset.page = i;
        paginationContainer.appendChild(button);
    }
}

function updateURL(page, filters) {
    const urlParams = new URLSearchParams(window.location.search);
    urlParams.set('page', page);

    urlParams.delete('f[]');
    for (const key in filters) {
        filters[key].forEach(value => {
            urlParams.append('f[]', `${key}:${value}`);
        });
    }

    history.pushState({ page, filters }, "", `?${urlParams.toString()}`);
}

Step 6: Handle user interactions

Finally, add event listeners to make the facets and pagination interactive.

// --- EVENT LISTENERS ---
facetsContainer.addEventListener('change', (e) => {
    if (e.target.matches('input[type="checkbox"]')) {
        const facetName = e.target.name;
        const facetValue = e.target.value;

        if (!activeFilters[facetName]) {
            activeFilters[facetName] = [];
        }

        if (e.target.checked) {
            activeFilters[facetName].push(facetValue);
        } else {
            activeFilters[facetName] = activeFilters[facetName].filter(v => v !== facetValue);
            if (activeFilters[facetName].length === 0) {
                delete activeFilters[facetName];
            }
        }
        getPLPResults(1, activeFilters);
    }
});

paginationContainer.addEventListener('click', (e) => {
    if (e.target.matches('button')) {
        const page = parseInt(e.target.dataset.page, 10);
        getPLPResults(page, activeFilters);
    }
});

document.addEventListener("DOMContentLoaded", () => {
    const urlParams = new URLSearchParams(window.location.search);
    const page = parseInt(urlParams.get('page'), 10) || 1;
    const filters = {};
    urlParams.getAll('f[]').forEach(filterString => {
        const [key, value] = filterString.split(":", 2);
        if (key && value) {
            if (!filters[key]) filters[key] = [];
            filters[key].push(value);
        }
    });
    getPLPResults(page, filters);
});

Step 7: Track analytics events manually

Analytics are not optional. For the Luigi's Box AI to learn from user behavior, you must manually track events when building a custom UI.

Example: Track list view and click events

// --- ANALYTICS CONFIGURATION ---
const ANALYTICS_API_URL = "https://api.luigisbox.com/";
const CLIENT_ID = Math.floor(Math.random() * 1e18); 

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

function trackListView(hits, categoryName, subsequentFilters = {}) {
    if (!hits || hits.length === 0) return;

    // Build the scopes object for the PLP context
    const scopes = {
        '_category_label': categoryName,
        'category': categoryName
    };

    // Build the filters object for any subsequent user-applied filters
    const filtersForAnalytics = {};
    for (const key in subsequentFilters) {
        filtersForAnalytics[key] = subsequentFilters[key].join(',');
    }

    const analyticsPayload = {
        id: crypto.randomUUID(),
        type: "event",
        tracker_id: TRACKER_ID,
        client_id: CLIENT_ID,
        lists: {
            "Product Listing": {
                items: hits.map((hit, index) => ({
                    title: hit.attributes.title,
                    url: hit.url,
                    position: (currentPage - 1) * RESULTS_PER_PAGE + index + 1
                })),
                query: {
                    scopes: scopes,
                    filters: filtersForAnalytics
                }
            }
        }
    };
    sendAnalyticsEvent(analyticsPayload);
}

function trackClickEvent(productId) {
    const clickPayload = {
        id: crypto.randomUUID(),
        type: "click",
        tracker_id: TRACKER_ID,
        client_id: CLIENT_ID,
        action: {
            type: "click",
            resource_identifier: productId
        }
    };
    sendAnalyticsEvent(clickPayload);
}

productGrid.addEventListener('click', function(e) {
    const productLink = e.target.closest('.product-link');
    if (productLink) {
        const productId = productLink.dataset.productId;
        if (productId) {
            trackClickEvent(productId);
        }
    }
});

Best Practices

  • Analytics is not optional: When building a custom UI, you are responsible for sending all analytics events. This is essential for the learning models that power search relevance and personalization.
  • Use a persistent CLIENT_ID: In this example, we generate a random CLIENT_ID on each page load. In a production environment, you should generate this ID once and store it in a long-term cookie or localStorage to track users across sessions.

Next Steps

With this basic integration complete, you are ready to handle more complex, real-world scenarios.