NAV Navbar
curl Ruby PHP Postman

Introduction

Welcome to the Luigi's Box Live API! You can use our API to access Luigi's Box Live features such as autocomplete or machine-consumable search analytics reports.

We have examples in multiple languages. You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right.

Authentication

Most of the available endpoints use HMAC authentication to restrict access. To use the API you'll need a

If you need help contact our support.

Our API expects you to include these HTTP headers:

HTTP Header Comment
Content-Type e.g., application/json; charset=utf-8
Standard HTTP header. Some endpoints allow you to POST JSON data, while some are purely GET-based
Authorization e.g., ApiAuth 1292-9381:sd73hdh881gfop228
The general format is client tracker_id:digest. The client part is not used, it's best to provide your application name, or a generic name, such as "ApiAuth". You must compute the digest using the method described below.
date e.g., Thu, 29 Jun 2017 12:11:16 GMT
Request date in the HTTP format. Note that this is cross-validated on our servers and if your clock is very inaccurate, your requests will be rejected. We tolerate ±5 second inaccuracies. You will be including this timestamp in the digest computation, so what this means in plain terms is that you cannot cache the digest and must recompute it for each request.

Digest computation

require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


date = Time.now.httpdate
digest("secret", "POST", "/v1/content", date)
<?php

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');
digest("secret", "POST", "/v1/content", $date);
// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestUri = request.url.replace(/^.*\/\/[^\/]+/, '').replace(/\?.*$/, '');
var timestamp = new Date().toUTCString();
var signature = [request.method, "application/json; charset=utf-8", timestamp, requestUri].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
echo $(digest "secret", "GET", "/v1/content", $date)

You must use HMAC SHA256 to compute the digest and it must be Base64 encoded.

Payload to be signed is computed as a newline-d concatenation of

Make sure that you are using the same values for digest computation as for the actual request. For example, if you compute digest from Content-Type: application/json; charset=utf-8, make sure you send the request with the exact same Content-Type (and not e.g. Content-Type: application/json).

Most programming languages provide crypto libraries which let you compute the HMAC digest with minimal effort. When the particular endpoint requires HMAC, we provide examples in several languages in the right column in its documentation.

The pseudocode for HMAC computation is:

signature = [request_method, content_type, timestamp, request_path].join("\n")
digest = base64encode(hmacsha256(signature, your_private_key))

Look for examples in the right column. You can find examples for other languages online, however, those were not tested by us. See the following external links for more examples:

Server will return HTTP 401 Not Authorized if your authentication fails. If this happens, look inside the response body. We include a diagnostics output which will tell you what was wrong with your request.

Diagnostics output as shown by Postman

Rate limiting

You can perform up to 30 requests per 5 second period from the same IP address for the same tracker_id. If you exceed this limit, you'll get a 429 Too Many Requests response for subsequent requests. Check the Retry-After header to see how many seconds to wait before retrying the request.

Importing your data

To use our Autocomplete and Search APIs, we need a way to synchronize your product catalog with our servers.

Once we have your catalog, we continuosly and automatically match the products from the catalog with our search analytics data and adjust their ranking.

We support two ways of catalog synchronization;

  1. The preferred way of synchronization is via our Content Updates API. Content updates enable near real-time synchronization of your database and make your search results accurate and up-to-date.

  2. We also support synchronization via XML or CSV feeds. We can setup regular processing of your feed and use the feed data to build your search index. However, be aware that even though we can process the feed several times a day, there will be periods of time where your search index is stale. For example, we process your feed at 8:00, and then, at 8:32 some of your products go out of stock. Your search index will be stale for several hours until we process your feed again. If you want to avoid stale search index, you need to implement the Content Updates API.

Content updates

When implementing Luigi's Box Search or Autocomplete service, you need to synchronize your product catalog (your database) with our search index. You should call the Content updates API in any of these cases:

Purpose Example trigger Endpoint
Make product searchable
  • New product gets in stock
  • Product which was unavailable becomes available again
Content update
Update product attributes
  • Product price changes
  • Someone updates product description
  • New product review was posted and product rating changes
Content update or Partial content update or Update by query
Remove project from search results
  • Product has sold out and will not be restocked
  • Product should be temporarily removed from all offerings
Content removal

Content update

HTTP Request

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.post("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "POST", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "POST" "/v1/content" "$date")

curl -i -XPOST \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'POST', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['POST', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}

POST https://live.luigisbox.com/v1/content

This endpoint requires a JSON input which specifies the objects that should be updated in Luigi's Box search index. This API accepts an array of objects - each item in the objects array is a single object with its attributes which should be updated or inserted into Luigi's Box index. This allows you to index several objects with a single API call. This is mainly useful for initial import when you can send many objects at once and speed up the indexing process. The optimal number of objects to send in a single batch depends on many factors, mainly your objects size. We recommend that you send around 100 objects in a single batch.

Be aware that updates to object attributes are not incremental. The object in Luigi's Box index is always replaced with the attributes you send. If you send all object attributes in the first call and then send just a single attribute in another call, your object will retain only the single attribute from the second call - all other attributes will be lost. In practice this means that you must always send all object attributes with each API call. If you would like to update only part of the object, see Partial content update.

The object's JSON has following top-level attributes.

Attribute Description
url Canonical URL of the object. It also serves as unique identifier of your object.
type You can have several searchable types (e.g. products and categories) and search them separately. Note, that we automatically build a special type with the name query which contains queries recorded on your site. You can use this type to build an autocomplete widget which suggests queries.
autocomplete_type If you wish to override type for the purpose of autocomplete, override it here. This can be either single value, or an array of autocomplete_types. Usually it is used to define a more narrow scope of a type (aka filter for autocomplete). For instance, you can define an item type with in promotion autocomplete_type, to be able to perform autocomplete solely within items in promotion.
generation Object generation, see Generations documentation below
active_from (Optional) The date/time at which this object should become searchable. This allows you to schedule search activation in advance. The date/time must be formatted in the ISO 8601 format, e.g. 2019-05-17T21:12:35+00:00
active_to (Optional) The date/time at which this object should stop being searchable. This allows you to schedule search deactivation in advance. The date/time must be formatted in the ISO 8601 format, e.g. 2019-05-17T21:12:35+00:00. To prevent accumulation of expired items, we will periodically delete expired items from our data stores. If you are issuing a partial update for an expired item, the partial update may fail because the item is no longer present.
fields Object attributes. Every field that you send will be searchable and can be used for filtering. You must send a field named title which we use as the object's display name. We automatically construct filtering facets out of the fields of your products. For instance, if you send a field called color with your product objects, we can show a color facet next to your search results and your users can filter the search results to only those products that have a specific color. Some fields are special, see Special fields below for more details.
nested (Optional) Array of nested items, each having type, url and title. Ideal for categories, brands and other objects, which are linked to the current object, but can also be addressed as a standalone object. For instance, to send products along with categories they belong to - the categories might be included as nested items. These are extracted server-side and stored also separately. You can also opt for including fields structure instead of title to include several attributes, not just the title.

There are no hard rules about field names, or which fields you have to send (except "title"), but when thinking about the fields, consider the following recommendations. If you are planning to use our Autocomplete widget see the note on Autocomplete widget integration.

There are few technical recommendations when dealing with fields:

Special fields

There are several fields which have special behavior:

Field name Description
title Required field. If you are using our Autocomplete widget, the title field will be automatically used by the widget as the result title.
availability If you send this field, it must have numeric value of 1 - meaning the product is available or 0 - product is unavailable. We are automatically sorting available results first. If an object does not have this field, we treat it as if it was available.
availability_rank This is a more advanced and granular version of the availability field. While availability is binary — a product is either available or not, availability_rank allows you to encode various availability "degrees". If you send this field, it must have a numeric value between 1 and 15. The semantics of this field is that the lower the number, the more available the product. It is up to you to devise an encoding between your domain availability semantics and availability_rank field. For example, you may set availability_rank: 1 for products which are ready for immediate shipment, availability_rank: 2 for products which will ship within 2 days, availability_rank: 3 for products which will ship within a week and availability_rank: 15 for products which are no longer available. We are automatically sorting "more available" results first.
_* Any attributes starting with underscore character _ are treated as hidden. They are searchable, but we will not expose them in autocomplete or search API responses. This is useful if you don't want to expose some private attributes to the world, but still want to be able to search them.
price* The price attribute should be a fully formatted string, including currency. Feel free to use formatting that is acceptable for the specific locale where the price will be displayed. Some valid values for price attribute are 1,232.60 €, kr12,341 or 8 129 zł. When we encounter a price attribute, we will do a best-effort extraction of a corresponding float value into a field called price_amount. If you are using an unusual price format or you want to have complete control over the extracted value, send the price_amount as part of the payload. When we encounter an existing _amount field, the auto-extraction will be skipped. Note that this behavior also applies to any field starting with price_ prefix, e.g., price_eur, or price_czk. For every price_-prefixed field, a corresponding _amount field will be auto-extracted, unless you send its value explicitely, e.g. price_eur_amount or price_czk_amount.
geo_* Any attributes starting with geo_ are considered as geographical location points, e.g., "geo_location" => {"lat" => 49.0448, "lon" => 18.5530}. If possible, use 'geo_location` name as your first choice.

Nested categories / ancestors

Nested category with ancestors for "T-shirt" which belongs to "Apparel > Men > T-shirts".

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "T-shirt",
      },
      "nested": [
        {
          "type": "category",
          "url": "https://myshop.example/categories/apparel/men/t-shirts",
          "fields": {
            "title": "T-shirts",
            "ancestors": [{
                "title": "Apparel",
                "type": "category",
                "url": "https://myshop.example/categories/apparel"
              }, {
                "title": "Men",
                "type": "category",
                "url": "https://myshop.example/categories/apparel/men"
              }
            ]
          }
        }
      ]
    }
  ]
}

Some objects have a natural hierarchy, which you may want to capture in the data. Most often, the products belong to a category which is part of a hierarchy, e.g. a product called "White plain T-Shirt" belongs to a category "T-Shirts", which belongs to a category "Men", which belongs to a category "Apparel". Naturally, the leaf category in the hierarchy (the one at the bottom of the hierarchy), is most specific for the product, but it is useful to send data about other categories in the hierarchy as well. To differentiate between the product-specific category and other categories, higher in the category hierarchy, use a special ancestors field in the nested object.

See the example to the right for a simple case of a product belonging to a single category.

If the product directly belongs to more than one category, send multiple nested categories, each with its own category hierarchy. See the example to the right for a case of a product which belongs to two categories.

If you decide to utilize this way of assigning products to categories, please look at searching within full category hierarchy to make sure you get the best results when using search service.

The product "Cheddar Cheese" belongs to categories "Dairy > Cow milk" and "Wine > Snacks"

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Cheddar cheese",
      },
      "nested": [
        {
          "type": "category",
          "url": "https://myshop.example/categories/dairy/cow-milk",
          "fields": {
            "title": "Cow milk",
            "ancestors": [{
              "title": "Dairy",
              "type": "category",
              "url": "https://myshop.example/categories/dairy"
            }]
          }
        },
        {
          "type": "category",
          "url": "https://myshop.example/categories/wine/snacks",
          "fields": {
            "title": "Snacks",
            "ancestors": [{
              "title": "Wine",
              "type": "category",
              "url": "https://myshop.example/categories/wine"
            }]
          }
        }
      ]
    }
  ]
}

Files processing

If you POST an item with a type _file, we will schedule a process of downloading and processing a real file located on a supplied url. Its fields will be enriched by a content attribute to make the file searchable by its content. Please note, that at the end, the final item will get assigned a type file (no underscore).

Autocomplete results filtering

Unlike search endpoint, autocomplete does not allow filtering results by explicit key/value filters (the f[] parameter in search). This is a performance tradeoff – to get results fast, items are indexed in a special way that does not allow arbitrary filtering. If you need to filter autocomplete results, you can use autocomplete_type parameter when indexing content. Imagine that your web shop should list all products, but you also have a mobile app which must only list a subset of all products. You can implement this requirement by using autocomplete_type field. All items will be indexed with type: 'product', but the products that should be available from the mobile app will get an additional autocomplete_type: 'mobile'. When querying from the web store, you will send an autocomplete request such as type=product:6, but when querying from the mobile app, you will ask for type=mobile:6. It is also possible to index items with several autocomplete_types, such as ['mobile', 'partners'].

Note, that the autocomplete_type is only relevant for autocomplete requests. To filter search requests, use search filter via the f[] parameter.

Autocomplete widget integration

Our Autocomplete widget expects certain fields in the object structure. If you send them, you will unlock specific features in the autocomplete dropdown.

Field name Required Description
title This is the absolute minimum that you must send.
image_link URL of the product preview image. The preview image should be small to load fast.
price Fully formatted price string, including currency.
price_old Fully formatted old price string which was discounted, including currency (Grid Layout).
itemgroup Groups items of identical type with different variants (Example: 1L milk, 5L milk, 10L milk).

Error handling

Example response for a batch with single failure

{
  "ok_count": 99,
  "errors_count": 1,
  "errors": {
    "http://example.org/products/1": {
      "type": "malformed_input",
      "reason": "incorrect object format",
      "caused_by": {
        "title": ["must be filled"]
      }
    }
  }
}

There are several failure modes:

HTTP Status Description
400 Bad Request Your request as a whole has invalid structure (e.g., missing the "objects" field) or the JSON has syntax errors. Look for more details in response body, fix the error and retry the request.
400 Bad Request Importing some of the objects failed. The response body will be JSON where you can extract the URLs of failed objects from "errors".
413 Payload Too Large You are sending batch larger than 5 megabytes. Try sending a smaller batch size. Note: we are checking the length of request content in bytes.

Checking your Data

If you ever want to know, which types you have pushed into our content endpoint, just open the following URL in any browser and check the facets key in JSON response.

https://live.luigisbox.com/search?tracker_id=<YOUR-TRACKER-ID>&facets=type&size=0

If you want to examine fields of your items of a specific type, it is also very easy, just use a filter:

https://live.luigisbox.com/search?tracker_id=<YOUR-TRACKER-ID>&f[]=type:<YOUR-TYPE>

Finally, you can search for virtually anything right from the location bar of your browser:

https://live.luigisbox.com/search?tracker_id=<YOUR-TRACKER-ID>&q=<YOUR-SEARCH-QUERY>

Partial content update

HTTP Request

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.patch("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "PATCH", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "PATCH" "/v1/content" "$date")

curl -i -XPATCH \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'PATCH', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('PATCH', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['PATCH', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}

PATCH https://live.luigisbox.com/v1/content

This endpoint requires a JSON input which specifies the objects that should be updated in Luigi's Box search index. This API accepts an array of up to 50 objects in a single request - each item in the objects array is an object with its attributes which should be updated in Luigi's Box index.

Every object requires url – it is the url of the object passed to the Content update API. Apart from that you only send what you would like to update. This is mainly useful for small real time updates of single objects or small batches of objects. Note when updating nested attribute that it replaces all previous content.

This endpoint cannot be used to create new objects and you cannot send multiple objects identified by the same url twice within one request. All other notes, recommendations and considerations from Content update API apply here as well.

Error handling

Example response for a batch with 48 successful updates, one object without URL and another with URL specified, but not found in the catalog.

{
  "ok_count": 48,
  "errors_count": 2,
  "errors": {
    "object #31": {
      "type": "malformed_input",
      "reason": "incorrect object format",
      "caused_by": {
        "url": ["is missing"]
      }
    },
    "http://example.org/products/99": {
      "type": "not_found",
      "reason": "URL not in catalog"
    }
  }
}

There are several failure modes:

HTTP Status Description
400 Bad Request Your request as a whole has invalid structure (e.g., missing the "objects" field) or the JSON has syntax errors. Look for more details in response body, fix the error and retry the request.
400 Bad Request Importing some of the objects failed. The response body will be JSON where you can extract the URLs of failed objects from "errors".
413 Payload Too Large You are sending more than 50 items in a single request. Try sending a smaller batch size.

Update by query

Additional way of keeping the product catalog up to date. Enables updates of item, that match search criteria. This endpoint works asynchronously, meaning that after you call it, it will start a job that will complete in time. After the first initializing call, you can check the state of the job.

HTTP Request

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.patch("/v1/update_by_query") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "PATCH", "/v1/update_by_query", date)}"
  req.body = '{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "PATCH" "/v1/update_by_query" "$date")

curl -i -XPATCH \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/update_by_query" -d '{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'PATCH', '/v1/update_by_query', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('PATCH', "https://live.luigisbox.com/v1/update_by_query", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/update_by_query'
var timestamp = new Date().toUTCString();
var signature = ['PATCH', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}

PATCH https://live.luigisbox.com/v1/update_by_query

This endpoint requires a JSON input consisting of two parts. search part consists of requirements, that item needs to fulfill to be updated. update part specifies how the product should be updated.

Search requirements for now work on principle of partial match, meaning that if product has attribute color: ['olive', 'red'] and you provide search requirement color: 'olive', the mentioned product will be a match and will be updated. Even though the field requirements are in partial object, it does not mean they will find fuzzy matches. Only partial matches on arrays of multiple values. On top of that, these requirements are also case sensitive. Meaning, that if product has category: Jeans and search requirement is category: jeans, the product won't be found.

Be aware, that updates to object attributes are not incremental. The attributes for found products in Luigi's Box index are always replaced with the attributes you send.

Required structrure of the request.

{
  "search": {
    "types": [], -> Array of strings, specifying the types of products we will include in search
    "partial": {
      "fields": {}, -> Hash of attribtues and their values, specifying the search criteria
    }
  },
  "update": {
    "fields": {} -> Hash of attribtues and their values, specifying the the attributes to be updated
  }
}

There are few technical recommendations when dealing with partial in search and fields in update part:

Special fields

There are several fields which have special behavior, specifically availability and availability_rank. Their behavior is described here

Checking state of job

If the asynchronous job was enqueued, API response will consist of url. Call GET method on this url to get the state of update job

Example response for request that enqueued async job (PATCH)

{
  "status_url": "/v1/update_by_query?job_id=1"
}
require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/v1/update_by_query?job_id=1") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/v1/update_by_query", date)}"
  req.body = '{
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/v1/update_by_query" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/update_by_query?job_id=1" -d '{
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/v1/update_by_query', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/v1/update_by_query?job_id=1", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/update_by_query'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
}

GET /v1/update_by_query?job_id=1

Response to this GET request is the state of the job. Job can be in one of theses states:

If job is not complete yet, only status and tracker id will be present in the response.

Example response for request to get the state of async job (GET)

  {
    "tracker_id": 1111-2222,
    "status": "complete",
    "updates_count": 5,
    "failures_count": 0,
    "failures": {}
  }

If some updates failed during the job execution, these failures are also reported.

  {
    "tracker_id": 1111-2222,
    "status": "complete",
    "updates_count": 0,
    "failures_count": 2,
    "failures": {
      "/products/1" => {
        "type" => "data_schema_mismatch",
        "reason" => "failed to parse [attributes.price]",
        "caused_by" => {"type" => "number_format_exception", "reason" => "For input string: \"wrong sale price\""}
      }
    }
  }

Error handling

There are several failure modes:

HTTP Status Description
400 Bad Request Your request as a whole has invalid structure (e.g., missing the "fields" field) or the JSON has syntax errors. Look for more details in response body, fix the error and retry the request.
403 API not allowed You don't have API request allowed for your site in Luigi's Box.
405 Method not allowed Unsupported HTTP method.
413 Payload Too Large You are sending request larger than 0.5 megabytes. Try sending a smaller request. Note: we are checking the length of request content in bytes.

Content removal

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.delete("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "DELETE", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "type": "product",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "product",
      "url": "https://myshop.example/products/2"
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "DELETE" "/v1/content" "$date")

curl -i -XDELETE \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "type": "product",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "product",
      "url": "https://myshop.example/products/2"
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'DELETE', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('DELETE', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "type": "product",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "product",
      "url": "https://myshop.example/products/2"
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['DELETE', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "type": "product",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "product",
      "url": "https://myshop.example/products/2"
    }
  ]
}

DELETE https://live.luigisbox.com/v1/content

This endpoint requires the url of the object - it is the url of the object passed to the Content update API. Calling this API will remove the object from Luigi's Box search index and the object with specified url will no longer appear in search results or in autocomplete results.

Generations

In some cases, you cannot effectively determine which objects were changed on your part, often because they are managed in some external system and only loaded to your own system through a periodic batch job. While you could reimport all objects through the Content Update API, you may end up with objects that are indexed in our system but are no longer present in your system. You can solve a scenario like this with Content Generations.

Content Generations allow you to import objects associated with a generation marker and then commit that generation and remove the past generation. An example:

  1. You have objects indexed in Luigi's Box which mirror your application database at some point in the past
URL Generation Fields
example.org/1 X color: red
example.org/2 X color: black
  1. A periodic job imported data to your own system, which you now need to sync with Luigi's Box. Your application database now contains product 2 which has a new color (changed from black to yellow) and product 3, which is a new product which was not present in your database before. Product 1 was deleted by the job.
  2. You iterate through all objects in your application database and build a Content Update batch. You assign a special generation attribute to each object in batch, e.g. 'generation': 'Y'
  3. We import your objects and since we are using URLs as unique identifiers, we will find existing object for the given URL and update the object, or create a new object with that URL if it does not exist
URL Generation Fields
example.org/1 X color: red
example.org/2 Y color: yellow
example.org/3 Y color: blue
  1. At this point, your Luigi's Box index can contain objects which are no longer present in your application database (product 1 in this example)
  2. You commit the generation Y via an API call to Luigi's Box and we will delete all objects that are from a different generation than what you specified. Your Luigi's Box index is now synced with your application database.
URL Generation Fields
example.org/2 Y color: yellow
example.org/3 Y color: blue

Marking objects with generation marker

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.post("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "POST", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "POST" "/v1/content" "$date")

curl -i -XPOST \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'POST', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['POST', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}

You use the Content Update API, but put a generation attribute inside each object top-level attributes. See the example on the right.

Note that the value of the generation marker is up to you to generate and can be any arbitrary string value. It is your responsibility to generate it and to make sure that the value is used consistently for all objects in the same generation.

We recommend that you use unix timestamp (cast to string) as the generation marker that you generate before initiating the content update process and use it for all subsequent objects.

POST https://live.luigisbox.com/v1/content

Committing a generation

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.post("/v1/content/commit?type=item&generation=1534199032554") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "POST", "/v1/content/commit", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "POST" "/v1/content/commit" "$date")

curl -i -XPOST \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content/commit?type=item&generation=1534199032554"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'POST', '/v1/content/commit', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/content/commit?type=item&generation=1534199032554", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content/commit'
var timestamp = new Date().toUTCString();
var signature = ['POST', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

Committing a generation ensures that only objects from the specified generation remain in the index. Committing is always type specific and will commit only the generation from the specified type.

To prevent unintentional deletes, the commit API makes sure that at least one object from the committed generation is present in the index. This is done to prevent simple mistakes/typos in generation marker and to prevent you from accidentally deleting all objects of a single type.

POST https://live.luigisbox.com/v1/content/commit?type=item&generation=1534199032554

Note, that when you use nested items, you need to commit their respective types separately. E.g., when you index item with type 'product', which has nested items with type 'category' and 'brand', then you need to issue 3 separate commit calls: commit for 'product', commit for 'category' and commit for 'brand'. The nested types are using the same generation marker as their parent.

Feeds

Each of your searchable type must have a separate feed in XML or CSV format.

Product feeds

We support several standard product feeds, namely

We can also process feeds in other formats (even your custom format), contact us to discuss.

Category feeds

This feed lives on your site, e.g. at https://example.org/feeds/categories.xml

<?xml version="1.0" encoding="UTF-8"?>
<categories>
  <category>
    <name>Shirts</name>
    <url>https://example.org/categories/shirts</url>
  </category>
  <category>
    <name>Blazers</name>
    <url>https://example.org/categories/blazers-women</url>
    <hierarchy>Apparel | Men | Blazers</hierarchy>
  </category>
  <category>
    <name>Blazers</name>
    <url>https://example.org/categories/blazers-men</url>
    <hierarchy>Apparel | Women | Blazers</hierarchy>
  </category>
  <!-- more categories -->
</categories>

Category feeds are simpler than product feeds and we only require 2 attributes: title and URL.

The feed must be flat, no nesting is allowed (i.e., <category> nested in another <category> tag). If you want to synchronize category hierarchy, use a separate hierarchy tag.

Attribute Description
nameREQUIRED Category name.
urlREQUIRED Canonical URL pointing to category listing. This is the URL where you want to take your users when they click on category suggestion in autocomplete.
hierarchyoptional Category hierarchy, where the | character serves as a category delimiter. We will automatically convert the hierarchy into an array that you can use to display category hierarchy in the autocomplete UI.
image_urloptional URL pointing to an image to show in the autocomplete UI. Make sure that the image is sized appropriately. We recommend that the images are not larger than 100x100 pixels.

We can also process feeds in other formats (even your custom format), contact us to discuss.

Brand feeds

This feed lives on your site, e.g. at https://example.org/feeds/brands.xml

<?xml version="1.0" encoding="UTF-8"?>
<brands>
  <brand>
    <name>NiceShirts</name>
    <url>https://example.org/brands/nice-shirts</url>
  </brand>
  <brand>
    <name>Blue</name>
    <url>https://example.org/brands/blue</url>
  </brand>
</brands>

Brand feeds are very similar to categories.

Attribute Description
nameREQUIRED Brand name.
urlREQUIRED Canonical URL pointing to brand listing. This is the URL where you want to take your users when they click on brand suggestion in autocomplete.
image_urloptional URL pointing to an image to show in the autocomplete UI. Make sure that the image is sized appropriately. We recommend that the images are not larger than 100x100 pixels.

Articles feeds

This feed lives on your site, e.g. at https://example.org/feeds/articles.xml

<?xml version="1.0" encoding="UTF-8"?>
<articles>
  <article>
    <name>Lorem ipsum title of the blog post</name>
    <annotation>Short description, perex</annotation>
    <text>Text of the article</text>
    <url>https://example.org/article/blog-post-lorem</url>
  </article>
  <article>
    <name>Lorem ipsum title of the article</name>
    <annotation>Short description, perex</annotation>
    <text>Text of the article</text>
    <url>https://example.org/brands/blog-post-ipsum</url>
  </article>
</articles>

Articles feeds are very similar to categories and brands.

Attribute Description
nameREQUIRED Article name.
urlREQUIRED Canonical URL pointing to the article. This is the URL where you want to take your users when they click on article suggestion in autocomplete.
annotationoptional Short annotation or perex of the article.
textoptional Complete text of the article.

We can also process feeds in other formats (even your custom format), contact us to discuss.

Exporting your data

Content export

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/v1/content_export") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/v1/content_export", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/v1/content_export" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content_export"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/v1/content_export', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/v1/content_export", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content_export'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

{
  "total": 14256,
  "objects": [
    {
      "url": "/item/1",
      "attributes":{
        "title": "Super product 1",
        ...
      },
      "nested": [],
      "type": "item",
      "exact": true
    },
    ...
  ],
  "links": [
    {
      "rel": "next",
      "href": "https://live.luigisbox.com/v1/content_export?cursor=23937182663"
    }
  ]
}

The content export endpoint returns all objects stored in our catalog in no particular order. It returns a list of products identified by their canonical URLs (relative ones) along with their attributes and nested fields.

The results returned by this API endpoint are paginated. To get to the next page, use the href attribute in the links section, where "rel": "next". When you receive a response which contains no link with "rel": "next", it means that there are no more pages to scroll and you have downloaded the full export.

HTTP Request

GET https://live.luigisbox.com/v1/content_export

Searching & Autocomplete

Autocomplete

You can use our autocomplete endpoint to get perfect search-as-you-type functionality.

To use this feature, we need to synchronize your product database with our search index. See Importing your data for more details.

Luigi's Box Autocomplete can learn the best results ordering. In order to enable learning, you need to integrate Luigi's Box Search Analytics service with your website by following the instructions.

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Autocomplete.js library which allows you to build and autocomplete widget with minimum programming effort.

JSON API

To invoke it, use this code:

require 'faraday'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/autocomplete/v2?q=harry+potter&tracker_id=1234-5678")

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XGET \
  "https://live.luigisbox.com/autocomplete/v2?q=harry+potter&tracker_id=1234-5678"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/autocomplete/v2?q=harry+potter&tracker_id=1234-5678"

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// This endpoint requires no body

The above command returns JSON structured like this. The exact content of attributes field depends on the content of your product catalog.

{
    "exact_match_hits_count": 6,
    "partial_match_hits_count": 0,
    "partial_match_terms": [],
    "hits": [
        {
            "url": "http://www.e-shop.com/products/123456",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "<em>Product</em> X",
                "title.untouched": "Product X",
                "availability": "true",
                "price": "5.52 EUR",
                "condition": "new"
            },
            "type": "item"
        },
        {
            "url": "http://www.e-shop.com/products/456789",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "Product Y",
                "title.untouched": "<em>Product</em> Y",
                "availability": "preorder",
                "price": "12.14 EUR",
                "condition": "new"
            },
            "type": "item"
        }
    ]
}

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

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Autocomplete.js library which allows you to build and autocomplete widget with minimum programming effort.

If you choose to implement the JSON API, we recommend that you consume it on the frontend side, i.e., directly from the HTML page. This API was designed for this use case and no sensitive information is required to call it (e.g., your private key). Do not proxy calls to Luigi's Box Autocomplete API via your backend servers as this will introduce additional latency.

We also recommend that you add DNS prefetch instruction to your HTML code to avoid the DNS lookup penalty on the first autocomplete request. Add the following code anywhere to your <head> to instruct browser to do the DNS lookup in advance.

<link rel="dns-prefetch" href="//live.luigisbox.com">

Query Parameters

Parameter Description
q User input
type Comma separated list of required types and their quantity, e.g. items:6,category:3
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.
unroll_variants Specifies whether multiple variants of the same product should be unrolled to fit the requested number of items (default), or if all variants of the same product should always be rolled to a single suggestion (value "never").

Autocomplete widget

We provide autocomplete widget which works directly with the JSON API. No programming is necessary, just include the script and CSS into your webpage and provide a simple configuration.

See standalone Autocomplete widget documentation for instructions and various configuration and styling examples.

Top items

You can use our Top items endpoint to get the list of most popular items of any type in the same output manner as with Autocomplete.

HTTP Request

require 'faraday'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/v1/top_items")

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XGET \
  "https://live.luigisbox.com/v1/top_items"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/v1/top_items"

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// This endpoint requires no body

The above command returns JSON structured like this. The exact content of attributes field depends on the content of your product catalog.

{
    "hits": [
        {
            "url": "http://www.e-shop.com/products/123456",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "Product X",
                "availability": "true",
                "price": "5.52 EUR",
                "condition": "new"
            },
            "type": "item",
            "exact": true
        },
        {
            "url": "http://www.e-shop.com/products/456789",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "Product Y",
                "availability": "preorder",
                "price": "12.14 EUR",
                "condition": "new"
            },
            "type": "item",
            "exact": true
        }
    ]
}

You can use the raw search endpoint and integrate it with your backend or frontend or as part of Luigi's Box Autocomplete widget (see Types option in Autocomple widget section).

GET https://live.luigisbox.com/v1/top_items

Query Parameters

Parameter Description
type Comma separated list of required types and their quantity, e.g. items:6,category:3
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.

Search as a Service

You can use our search endpoint to get a perfect fulltext search functionality with advanced filtering options.

To use this feature, we need to synchronize your product database with our search index. See Importing your data for more details.

Luigi's Box Search as a Service can learn the best results ordering. In order to enable learning, you need to integrate Luigi's Box Search Analytics service with your website by following the instructions.

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Search.js library which allows you to build a search interface with minimum programming effort.

JSON API

You can use the raw search endpoint and integrate it with your backend or frontend.

HTTP Request

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

Query Parameters

Parameter Description
q User input - query. Optional, if you do not send q parameter, the API will only apply filters (f[] parameter). This is useful for generating listing pages.
qu Allows to control query understanding process. Use qu=1 or qu=0 to turn it on or off. This feature is currently off by default. Important: if you want to use this feature, you must also include user_id parameter with the value of _lb cookie from your site.
f[] Filter, using key:value syntax e.g., f[]=categories:Gadgets to filter hits according to chosen criteria. Filtering on top of numerical and date attributes supports ranges, using pipe as a separator, e.g., f[]=price:5|7. This range can be left open from either side, e.g., f[]=price:6|. If a combination of filters for the same field is provided, they are applied with OR. E.g., filters f[]=categories:jackets&f[]=categories:coats will retrieve products, that have either jackets OR coats category.
f_must[] Optional. Explicitly required filter, using key:value syntax e.g., f_must[]=categories:Gadgets to filter hits according to chosen criteria. Same rules aply here as for normal Filter in sense of possible filter values. The main difference is, that if a combination of filters for the same field is provided, they are applied with AND, not OR. E.g., filters f_must[]=categories:jackets&f_must[]=categories:windproof will retrieve only products, that have both jackets AND windproof category.
size How many hits you want the endpoint to return. Defaults to 10.
sort Allows you to specify ordering of the results, using attr:{asc|desc} syntax, e.g., sort=created_at:desc
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.
quicksearch_types A comma separated list of types, which should be (also) searched for. These will be without any facets and the search will look only in title field.
facets A comma separated list of facets you want to have included in the response.
dynamic_facets_size Optional. If you wish our service to include additional, dynamically identified facets in the response, send the maximum number of such facets in this parameter. Defaults to 0 , i.e., no dynamically identified facets are returned. Dynamic identification of facets is based mainly on categories of retrieved items and their interesting attributes.
page Which page of the results you want the endpoint to return. Defaults to 1.
use_fixits Optional. Allows to control use of fixit rules. Use use_fixits=1 or use_fixits=true to explicitly enable usage of fixit rules. Use other values (such as use_fixits=false) to disable fixit rules for current request. Default value is true, so fixit rules are enabled by default.
prefer[] Optional. Soft filter, using key:value syntax e.g., prefer[]=category:Gadgets to prefer hits according to chosen criteria. Use context to indicate preference of autocomplete_type as described in autocomplete results filtering.
hit_fields Optional. A comma separated list of attributes and product parameters. Only these fields (in addition to some default ones) will be retrieved and present in results. If not provided, all fields will be present in results.
context[geo_location] Optional. A coma separated list of geographical coordinates (lat, lon) representing visitors location, e.g., context[geo_location]=49.0448,18.5530. Allows to consider distance between a visitor and the items she is searching for. To be able to consider geographical context in search, catalog objects also need to contain an attribute which holds geo coordinates. By default, we assume that these are stored at geo_location.
context[geo_location_field] Optional. A definition of a custom field with geo coordinates to be used for geo search by context[geo_location]. If not defined, we assume that these are stored at geo_location field but you can override this by specifying context['geo_location_field']=my_field.
context[availability field] Optional. Allows to change or disable consideration of item availability on results ranking. Without context definition, the default availability field is considered for ranking. Supply context[availability_field]=my_custom_field parameter to override this to your custom field. This field must contain integer value (0 for unavailable items or 1 for available items). If you want to disable influence of items availability on results ranking, set this context explicitly to nil: context[availability_field]=nil.
context[boost_field] Optional. Allows to change the default field used for boosting or disable boosting on results ranking. Without context definition, the default boost field is considered for ranking. Provide context[boost_field]=my_custom_field to change this to your custom field. Make sure that your custom field contains integer values from the interval 0-3 (where higher number means higher boosting priority). If you want to disable influence of boosting on results ranking, set this context explicitly to nil: context[boost_field]=nil.
context[freshness_field] Optional. Allows to change or disable consideration of item freshness (boosting of new items) on results ranking. Without context definition, the default freshness field is considered for ranking. Provide context[freshness_field]=my_custom_field to change this to your custom field. Make sure that your custom field holds date/timestamp value in ISO 8601 format. If you want to disable influence of freshness on results ranking, set this context explicitly to nil: context[freshness_field]=nil.

Searching within full category hierarchy

If you imported your products with categories as nested objects that have hierarchy of ancestors inside, use all_categories as a filter key when filtering by categories. E.g., to filter by category furniture issue a request with f[]=all_categories:furniture.

By default when searching, filters of same type are applied with OR and filters of different types are applied with AND. E.g., request with filters f[]=category:jackets&f[]=category:windproof will find products, that have category jackets OR category windproof OR both, and request with filters f[]=category:jackets&f[]=protection:windproof will find products, that have category jackets AND protection windproof.

If you want to combine two filters of same type in AND like fashion, use f_must[] instead of f[]. E.g., you want to find only products that have category jackets and category windproof matching query 'adidas'. So instead of using this request

GET https://live.luigisbox.com/search?tracker_id=*your_tracker_id*&f[]=type:item&f[]=category:jackets&f[]=category:windproof&query=adidas

you must use this request

GET https://live.luigisbox.com/search?tracker_id=*your_tracker_id*&f[]=type:item&f_must[]=category:jackets&f_must[]=category:windproof&query=adidas

The reason why we use this model, is that filters are automatically mapped onto facets. This way, you as a user of our API service, don't have to do think about it. You only provide filters, and we take care of the rest.

Filtering with geographical distance

To filter results based on geographical distance from the user's current location, for example to to find result within 50km, use f[]=geo_range:|50km. This way, all results with geo location farther than 50km will be filtered out. (For this filter to work, you must have a geo field indexed within your data, and provide geo location context in search parameters.)

The pattern for value of geo range filter is lower_range|upper_range, and lower and upper range need to match the pattern of /\d+km/. You can also ommit the lower or upper range to achieve an open interval.

Tips

require 'faraday'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/search?q=harry+potter&tracker_id=1234-5678")

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XGET \
  "https://live.luigisbox.com/search?q=harry+potter&tracker_id=1234-5678"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/search?q=harry+potter&tracker_id=1234-5678"

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// This endpoint requires no body

The above command returns JSON structured like this. The exact content of attributes field depends on the content of your product catalog.

{
  "results": {
    "total_hits": 223,
    "hits": [
      {
        "url": "http://www.e-shop.com/products/123456",
        "attributes": {
          "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
          "description": "Description field from your product catalog",
          "categories": [
            "Gadgets",
            "Kids"
          ],
          "categories_count": 2,
          "title": "<em>Product</em> X",
          "title.untouched": "Product X",
          "availability": "true",
          "price": "5.52 EUR",
          "condition": "new"
        },
        "type": "item"
      },
      {
        "url": "http://www.e-shop.com/products/456789",
        "attributes": {
          "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
          "description": "Description field from your product catalog",
          "categories": [
            "Gadgets",
            "Kids"
          ],
          "categories_count": 2,
          "title": "Product Y",
          "title.untouched": "<em>Product</em> Y",
          "availability": "preorder",
          "price": "12.14 EUR",
          "condition": "new"
        },
        "type": "item"
      }
    ],
    "facets": [
      {
        "name": "type",
        "type": "text",
        "values": [
          {
            "value": "item",
            "hits_count": 123
          },
          {
            "value": "article",
            "hits_count": 14
          }
        ]
      },
      {
        "name": "price",
        "type": "float",
        "values": [
          {
            "value": "0.0|9.0",
            "hits_count": 1
          },
          {
            "value": "9.0|18.0",
            "hits_count": 1
          }
        ]
      },
      {
        "name": "categories_count",
        "type": "float",
        "values": [
          {
            "value": "1.0|2.0",
            "hits_count": 147
          },
          {
            "value": "2.0|3.0",
            "hits_count": 71
          }
        ]
      },
      {
        "name": "created_at",
        "type": "date",
        "values": [
          {
            "value": "2017-10-23T00:00:00+00:00|2017-11-23T00:00:00+00:00",
            "hits_count": 18
          },
          {
            "value": "2017-11-23T00:00:00+00:00|2017-12-23T00:00:00+00:00",
            "hits_count": 80
          }
        ]
      }
    ],
    "offset": "20"
  },
  "next_page": "https://live.luigisbox.com/search?q=harry+potter&tracker_id=1234-5678&page=2"
}

Recommender

You can use our recommendation endpoint to get a plethora of item recommendations from your catalog based on popularity, similarity, users' interaction history and much more.

To use this feature, we need to synchronize your product database with our search index. See Importing your data for more details.

To take advantage of personalized and other advanced recommendations, you need to integrate Luigi's Box Search Analytics service with your website by following the instructions.

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Recco.js library which allows you to build a recommendation interface with minimum programming effort.

Recommendation types

To be able to recommend items Luigi's Box Recommender requires your product details in our databases and trained recommender models. The training takes place within the Recommender itself, but the configuration has to be crafted based on your specific use case. Once everything is set up you will be given the recommender_type to use in recommendation requests described below. Please contact our support to discuss the options in detail.

JSON API

You can use the raw recommendation endpoint and integrate it with your backend or frontend.

If you would like to integrate this endpoint with your backend, consider supplying your own unique user identifiers to Luigi's Box Search Analytics script for best experience. See User identifiers for more details.

require 'faraday'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.post("/v1/recommend?tracker_id=1234-5678") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.body = '[
  {
    "blacklisted_item_ids": [
      "http://www.e-shop.com/products/789012"
    ],
    "item_ids": [
      "http://www.e-shop.com/products/012345",
      "http://www.e-shop.com/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000"
  }
]'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XPOST \
  "https://live.luigisbox.com/v1/recommend?tracker_id=1234-5678" -d '[
  {
    "blacklisted_item_ids": [
      "http://www.e-shop.com/products/789012"
    ],
    "item_ids": [
      "http://www.e-shop.com/products/012345",
      "http://www.e-shop.com/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000"
  }
]'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/recommend?tracker_id=1234-5678", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '[
  {
    "blacklisted_item_ids": [
      "http://www.e-shop.com/products/789012"
    ],
    "item_ids": [
      "http://www.e-shop.com/products/012345",
      "http://www.e-shop.com/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000"
  }
]'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// Example request body

[
  {
    "blacklisted_item_ids": [
      "http://www.e-shop.com/products/789012"
    ],
    "item_ids": [
      "http://www.e-shop.com/products/012345",
      "http://www.e-shop.com/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000"
  }
]

The above command returns JSON structured like this. The exact content of attributes field depends on the time of the request and content of your product catalog.

[
  {
    "generated_at": "2020-05-05T12:44:22+00:00",
    "model_version": 1588682662,
    "recommendation_id": "a24588e9-0664-4637-91d5-165313a6eac8",
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "recommender": "c01",
    "recommender_version": "b36705710",
    "user_id": "6822981852855588000",
    "hits": [
      {
        "url": "http://www.e-shop.com/products/123456",
        "attributes": {
          "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
          "description": "Description field from your product catalog",
          "categories": [
            "Gadgets",
            "Kids"
          ],
          "title": "Product X",
          "availability": "true",
          "price": "5.52 EUR",
          "condition": "new"
        },
        "type": "item"
      },
      {
        "url": "http://www.e-shop.com/products/456789",
        "attributes": {
            "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
            "description": "Description field from your product catalog",
            "categories": [
                "Gadgets",
                "Kids"
            ],
            "title": "Product Y",
            "availability": "preorder",
            "price": "12.14 EUR",
            "condition": "new"
        },
        "type": "item",
        "exact": true
      }
    ]
  }
]

HTTP Request

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

Query Parameters

Parameter Description
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.

Request Headers

Header Content
Content-Type application/json; charset=utf-8

Request Body

Request body is a JSON array of recommendation request objects. Each recommendation request object contains following attributes:

Attribute Description
recommendation_type Unique identifier of a requested recommendation type. See Recommendation types for more details.
user_id Unique user identifier. Send the value of _lb cookie from your site or supply your own value. See User identifiers for more details.
item_ids A list of items to base the recommendation on. Depending on the type of recommendation and placement it might be a list of URLs of products in a shopping cart or category, current URL of a product user is exploring, etc.
blacklisted_item_ids Optional. List of product URLs that must not be recommended, e.g., different product variants that are very similar to one in item_ids.
recommender_client_identifier Optional. Arbitrary identifier by which you distinguish between different recommendations. Use when issuing multiple recommendation requests in a single API call.
size Optional. How many recommended items you want to return. Defaults to 10.

Analytics APIs

Frequent queries

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/frequent_queries") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/frequent_queries", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/frequent_queries" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/frequent_queries"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/frequent_queries', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/frequent_queries", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/frequent_queries'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

[
  {
    "query": "query x",
    "searches_count": 5917,
    "links": [
      {
        "rel": "self",
        "href": "https://live.luigisbox.com/query_detail?q=x"
      }
    ]
  },
  {
    "query": "query y",
    "searches_count": 1475,
    "links": [
      {
        "rel": "self",
        "href": "https://live.luigisbox.com/query_detail?q=y"
      }
    ]
  },
  {
    "query": "query z",
    "searches_count": 1127,
    "links": [
      {
        "rel": "self",
        "href": "https://live.luigisbox.com/query_detail?q=z"
      }
    ]
  }
]

You can simply follow the self href to get details on a specific query.

Our frequent queries endpoint gives you a list of your top queries, as we tracked them in Luigi's Box. All queries are lowercased and any non-ASCII characters are converted their ASCII approximation.

HTTP Request

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

No results queries

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/no_results_queries") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/no_results_queries", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/no_results_queries" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/no_results_queries"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/no_results_queries', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/no_results_queries", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/no_results_queries'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

[
  {
    "query": "query x",
    "searches_count": 5917,
    "links": [
      {
       "rel": "self",
       "href": "https://app.luigisbox.com/sites/23-7723/queries?in=Search+Results&q=query+x&show=noresults"
      }
    ]
  },
  {
    "query": "query y",
    "searches_count": 1475,
    "links": [
      {
        "rel": "self",
        "href": "https://app.luigisbox.com/sites/23-7723/queries?in=Search+Results&q=query+y&show=noresults"
      }
    ]
  },
  {
    "query": "query z",
    "searches_count": 1127,
    "links": [
      {
        "rel": "self",
        "href": "https://app.luigisbox.com/sites/23-7723/queries?in=Search+Results&q=query+z&show=noresults"
      }
    ]
  }
]

The self href leads to query detail page in Luigi's Box app.

Our no results queries endpoint gives you a list of queries for which we tracked a "no-results" response in last 30 days. All queries are lowercased and any non-ASCII characters are converted their ASCII approximation.

HTTP Request

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

Trending queries

require 'faraday'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/v2/trending_queries?tracker_id=1234-5678")

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XGET \
  "https://live.luigisbox.com/v2/trending_queries?tracker_id=1234-5678"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/v2/trending_queries?tracker_id=1234-5678"

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// This endpoint requires no body

The above command returns JSON structured like this.

[
  {
    "title": "Query A",
    "links": [{
      "rel": "top_content",
      "href": "https://example.com/news/"
    }]
  },
  {
    "title": "query B"
  }
]

Trending queries endpoint is particularly suited for building a "trending queries" widget on your site.

Calling this endpoint gives you a list of your top queries for the past 30 days but you can customize this list in the main Luigi's Box application. There are no API parameters, the output is only controlled from Luigi's Box application UI.

In the application, look for the menu "Trending queries API settings". If you don't see it, contact us to enable it.

From the management UI, you can:

GET https://live.luigisbox.com/v2/trending_queries

Parameter Description
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.

Query Detail

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/query_detail?q=term") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/query_detail", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/query_detail" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/query_detail?q=term"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/query_detail', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/query_detail?q=term", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/query_detail'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

{
  "with_clicks": [
    {
      "title": "Product X",
      "url": "www.e-shop.com/products/123",
      "clicks": 465
    },
    {
      "title": "Product Y",
      "url": "www.e-shop.com/products/456",
      "clicks": 324
    }
  ],
  "with_conversions": [
    {
      "title": "Product X",
      "url": "www.e-shop.com/products/123",
      "conversions": 29
    },
    {
      "title": "Product Z",
      "url": "www.e-shop.com/products/789",
      "conversions": 16
    }
  ]
}

The query detail endpoint gives you top hits (in terms of CTR and conversions) of the chosen query.

HTTP Request

GET https://live.luigisbox.com/query_detail?q=term

Converting items

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/converting_items") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/converting_items", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/converting_items" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/converting_items"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/converting_items', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/converting_items", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/converting_items'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

[
    {
        "item": "https://www.eshop.com/products/123",
        "conversions_count": 25,
        "converting_queries": [
            {
                "string": "foo",
                "source": "Search Results",
                "searches_count": 17,
                "links": [
                    {
                        "rel": "self",
                        "href": "https://live.luigisbox.com/query_detail?q=foo"
                    }
                ]
            },
            {
                "string": "bar",
                "source": "Autocomplete",
                "searches_count": 3,
                "links": [
                    {
                        "rel": "self",
                        "href": "https://live.luigisbox.com/query_detail?q=bar"
                    }
                ]
            }
        ]
     }
]

The converting items endpoint gives you a list of your items, for which we recorded a search conversion in Luigi's Box. Each converting item has a list of queries from which the conversion happened.

HTTP Request

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

You can also make a POST request and include an array of items (their URLs) to scope the results to these items only.

POST https://live.luigisbox.com/converting_items

To invoke POST request with results scoped to given items, use this code:

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.post("/converting_items") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "POST", "/converting_items", date)}"
  req.body = '[
  "http://eshop.com/products/1",
  "http://eshop.com/products/2"
]'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "POST" "/converting_items" "$date")

curl -i -XPOST \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/converting_items" -d '[
  "http://eshop.com/products/1",
  "http://eshop.com/products/2"
]'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'POST', '/converting_items', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/converting_items", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '[
  "http://eshop.com/products/1",
  "http://eshop.com/products/2"
]'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/converting_items'
var timestamp = new Date().toUTCString();
var signature = ['POST', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

[
  "http://eshop.com/products/1",
  "http://eshop.com/products/2"
]

Filters

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/filters") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/filters", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/filters" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/filters"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/filters', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/filters", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/filters'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

[
  {
    "name": "Categories",
    "users_count": 11793,
    "conversion_rate": 2.74,
    "values": [
      {
        "name": "Laptop",
        "users_count": 644,
        "conversion_rate": 8.58,
        "links": [
          {
            "rel": "self",
            "href": "https://live.luigisbox.com/filters?pair[]=Categories:Laptop"
          }
        ]
      },
      {
        "name": "Desktop",
        "users_count": 1595,
        "conversion_rate": 5.45,
        "links": [
          {
            "rel": "self",
            "href": "https://live.luigisbox.com/filters?pair[]=Categories:Desktop"
          }
        ]
      }
    ]
  },
  {
    "name": "In Stock",
    "users_count": 11793,
    "conversion_rate": 2.74,
    "values": [
      {
        "name": "Yes",
        "users_count": 1192,
        "conversion_rate": 4.41,
        "links": [
          {
            "rel": "self",
            "href": "https://live.luigisbox.com/filters?pair[]=In+Stock:Yes"
          }
        ]
      }
    ]
  }
]

You can simply follow the self href to get details on a specific filter pair.

Filters endpoint gives you information about filters used for searching. See the Search analytics documentation section on Filters for more information.

Filter is always a pair of a name and a value. Both the name and value are taken from search analytics data.

When you invoke the endpoint without any parameters you will get an overview of all filters used, with basic statistical information - how many users used the filter and what was its conversion rate. The API returns the filter information in a hierarchy - filter name first, and then its values nested underneath. See the example on the right.

Filters are ordered by users_count attribute on the first level, and then by the filter conversion rate on the values level.

You can also pass an optional pair[] parameter to get information about filters used in conjunction with the filter sent in pair[] parameter. This can be understood as: "What other filters were used together with this filter?". For example, to answer the question: "What were the most used filters in "Laptops" category?", you can make a following request

GET https://live.luigisbox.com/filters?pair[]=categories:Laptops

The recommended way to use this API is to first invoke the endpoint without any parameters to get a list of all filter pairs. Each pair also contains a HATEOAS self link with prepopulated pair[] parameter. We recommend that you use this link, instead of trying to build it yourself.

HTTP Request

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

Query Parameters

Parameter Description
pair[] Filter pair in the name:value format to limit filter usage data to. Repeat this parameter multiple times to limit data to several filters at once.

Recommend items

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/recommend_items?item_url=https://eshop.com/products/789") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/recommend_items", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/recommend_items" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/recommend_items?item_url=https://eshop.com/products/789"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/recommend_items', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/recommend_items?item_url=https://eshop.com/products/789", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/recommend_items'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

[
  {
    "title": "a Title",
    "url": "https://eshop.com/products/123"
  },
    {
    "title": "another Title",
    "url": "https://eshop.com/products/456"
  }
]

The related items endpoint gives you a list of recommended items for a given item, identified by its URL.

HTTP Request

GET https://live.luigisbox.com/recommend_items?item_url=https://eshop.com/products/789

Global ranking

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/ranking_global") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/ranking_global", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/ranking_global" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/ranking_global"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/ranking_global', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/ranking_global", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/ranking_global'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

{
  "total": 14256,
  "ranks": [
    {
      "url": "https://eshop.com/products/123",
      "rank": 1,
      "rev_rank": 14257
    },
    {
      "url": "https://eshop.com/products/456",
      "rank": 2,
      "rev_rank": 14256
    },
    {
      "url": "https://eshop.com/products/918",
      "rank": 3,
      "rev_rank": 14255
    }
  ],
  "links": [
    {
      "rel": "next",
      "href": "https://live.luigisbox.com/ranking_global?s=23937182663"
    }
  ]
}

The global ranking endpoint returns information about the optimal global ranking (ordering) of products. It returns a list of products identified by their canonical URLs and a numeric rank for each of the listed products. The rank is computed using Luigi's Box proprietary algorithms and considers many signals collected via search analytics. We recommend that you treat the rank as an opaque number on a strictly "lower is better" basis (as it is sorted). We also include "rev_rank" with reversed rank ("higher is better") for convenience.

The results returned by this API endpoint are paginated. To get to the next page, use the href attribute in the links section, where "rel": "next". When you receive a response which contains no link with "rel": "next", it means that there are no more pages to scroll and you have downloaded the full ranking list.

HTTP Request

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

Per filter content ranking

require 'faraday'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com')

response = connection.get("/ranking_per_filter?f=Category:Ladies+shirts") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/ranking_per_filter", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/ranking_per_filter" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/ranking_per_filter?f=Category:Ladies+shirts"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/ranking_per_filter', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/ranking_per_filter?f=Category:Ladies+shirts", [
  'headers' => [
    'Content-Type' => 'application/json; charset=utf-8',
    'date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/ranking_per_filter'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

[
  "ranks": [
    {
      "url": "https://eshop.com/products/123",
      "rank": 291.2
    },
    {
      "url": "https://eshop.com/products/456",
      "rank": 61.9
    },
    {
      "url": "https://eshop.com/products/918",
      "rank": 816.2
    }
  ],
  "links": [
    {
      "rel": "next",
      "href": "https://live.luigisbox.com/ranking_global?f=Category:Ladies+shirts&s=23937182663"
    }
  ]
]

The per filter ranking endpoint returns information about the optimal ranking of products with respect to a single filter, provided as a key:value pair. It returns a list of products identified by their canonical URLs and a numeric rank for each of the listed products with respect to the provided filter. The rank is computed using Luigi's Box proprietary algorithms and considers many signals collected via search analytics. We recommend that you treat the rank as an opaque number on a strictly "higher is better" basis.

The results returned by this API endpoint are paginated. To get to the next page, use the href attribute in the links section, where "rel": "next". When you receive a response which contains no link with "rel": "next", it means that there are no more pages to scroll and you have downloaded the full ranking list.

HTTP Request

GET https://live.luigisbox.com/ranking_per_filter?f=Category:Ladies+shirts