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
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 ±1 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'

  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';

  $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
//  - 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", 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"
  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

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

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
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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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 endoint 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 between 500-1000 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.

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 autocompelte widget which suggests queries.
autocomplete_type If you wish to override type for the purpose of autocomplete, override it here.
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.
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. 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 and 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.

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, including currency, to display

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.

Content removal

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

def digest(key, method, endpoint, date)
  content_type = 'application/json'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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. Commiting 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 commited 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

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 poiting 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 category suggestion in autocomplete
image_urloptional URL poiting 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.

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.

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

You can use the raw autocomplete endpoint and integrate it with your autocomplete library of choice, e.g., typeahead, or jquery-ui.

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 also 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.

JSON API

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

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
            }
          ]
        }
      ]
    }
  }

HTTP Request

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

Query Parameters

Parameter Description
q User input - query
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|.
size How many hits you want the endpoint to return. Defaults to 10.
page Which page of the results you want the endpoint to return. Defaults to 1.
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.
offset Used for scrolling through the results. Just repeat its value from the last server response.
facets A comma separated list of facets you want to have included in the response.

Analytics APIs

Frequent queries

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

def digest(key, method, endpoint, date)
  content_type = 'application/json'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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

Trending queries

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

def digest(key, method, endpoint, date)
  content_type = 'application/json'

  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("/trending_queries") do |req|
  req.headers['Content-Type'] = "application/json"
  req.headers['date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/trending_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"
  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" "/trending_queries" "$date")

curl -i -XGET \
  -H "Date: $date" \
  -H "Content-Type: application/json" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/trending_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';

  $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', '/trending_queries', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/trending_queries", [
  'headers' => [
    'Content-Type' => 'application/json',
    '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 = '/trending_queries'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json", 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": "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/trending_queries

Query Detail

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

def digest(key, method, endpoint, date)
  content_type = 'application/json'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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?s=23937182663"
    }
  ]
}

The global ranking endpoint returns information about the optimal global ranking 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 "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_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'

  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"
  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"
  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" \
  -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';

  $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',
    '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", 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