---
title: API Principles
description: Authentication, rate limits, error handling, and API conventions for Luigi's Box APIs.
slug: platform-foundations/api-principles
docKind: reference
hub: platform-foundations
---

Luigi's Box provides HTTP-based APIs for two main purposes: serving search results to users (Public) and managing your data/configuration (Private).

## Service reference and endpoints

:::note
**Base URL Note:** Most services (Search, Content, Recommender) live on `live.luigisbox.com`. The Analytics Event API lives on `api.luigisbox.com`.
:::

### Public API (unauthenticated)

**Auth:** Requires only your public `tracker_id`.

**Usage:** Safe to call from Frontend (Browsers, Mobile Apps).

| Category               | Method       | Base URL             | Endpoint               | Description                                                            |
| :--------------------- | :----------- | :------------------- | :--------------------- | :--------------------------------------------------------------------- |
| **Search**             | `GET`/`POST` | `live.luigisbox.com` | `/search`              | Full-text search & facets.                                             |
| **Autocomplete**       | `GET`        | `live.luigisbox.com` | `/autocomplete/v2`     | Prefix-based suggestions.                                              |
| **Recommender**        | `POST`       | `live.luigisbox.com` | `/v1/recommend`        | Product recommendations.                                               |
| **Shopping Assistant** | `POST`       | `live.luigisbox.com` | `/v1/assistant`        | AI-powered structured dialogue that helps customers discover products. |
| **Top items**          | `GET`        | `live.luigisbox.com` | `/v1/top_items`        | Most popular items.                                                    |
| **Trending queries**   | `GET`        | `live.luigisbox.com` | `/v2/trending_queries` | Trending search terms.                                                 |
| **Analytics**          | `POST`       | `api.luigisbox.com`  | `/v1`                  | **Events API.** Ingest user interactions.                              |

### Private API (authenticated)

**Auth:** Requires **HMAC Signature** using your Private Key.
**Usage:** Backend Server ONLY.

#### Indexing hub (content management)

_Manage your product catalog._

| Method   | Base URL             | Endpoint              | Description                                      |
| :------- | :------------------- | :-------------------- | :----------------------------------------------- |
| `POST`   | `live.luigisbox.com` | `/v1/content`         | **Create/Replace.** Add new products.            |
| `PATCH`  | `live.luigisbox.com` | `/v1/content`         | **Partial Update.** Update specific fields.      |
| `DELETE` | `live.luigisbox.com` | `/v1/content`         | **Delete.** Remove products.                     |
| `PATCH`  | `live.luigisbox.com` | `/v1/update_by_query` | **Batch Update.** Update items matching a query. |
| `GET`    | `live.luigisbox.com` | `/v1/content_export`  | **Export.** Download your current index data.    |

#### Recommender customization

_Manage pinning rules and batch recommendations._

| Method | Base URL             | Endpoint                              | Description                                        |
| :----- | :------------------- | :------------------------------------ | :------------------------------------------------- |
| `POST` | `live.luigisbox.com` | `/v1/recommender/pin/{ID}/scopes`     | Define pinning scopes (rules).                     |
| `GET`  | `live.luigisbox.com` | `/v1/recommender/pin/{ID}/summary`    | Get pinning configuration summary.                 |
| `POST` | `live.luigisbox.com` | `/v1/recommender/batching/{ID}/users` | Trigger batch recommendation generation for users. |
| `*`    | `live.luigisbox.com` | `/v1/recommender/pin/*`               | Various CRUD endpoints for managing pins.          |

:::danger
**Security Warning:** Never call the Private API (Content/Pinning) from a frontend application. Doing so exposes your Private Key and allows attackers to corrupt your data.
:::

---

## Authentication (HMAC)

Only the **Private API** endpoints require a custom **HMAC-SHA256** signature.

### Credentials

You will need your API credentials found in **Luigi's Box App > Settings > Integration Settings**:

- **Tracker_ID (Public Key):** Identifies your account.
- **Secret Key (Private Key):** Used to sign requests.

### The signing flow

To authenticate, you must construct a specific "String to Sign," hash it, and attach it as a header.

<div class="lb-mermaid-frame"><pre class="mermaid">flowchart TD
A["HTTP method"]
B["Content-Type"]
C["Date header"]
D["Path"]
E["Canonical string"]
F{"Hash function"}
G["Raw bytes"]
H["Final signature"]
I["Authorization header"]
J["API server"]
Key["Private secret"]
A --> E
B --> E
C --> E
D --> E
E -->|HMAC SHA-256| F
Key -.-> F
F -->|Binary output| G
G -->|Base64 encode| H
H -->|Format string| I
I -->|Send| J</pre></div>

### Detailed specification

#### Step 1: Required headers

Every authenticated request must include these headers. The values used here **must** match the values used in the signature generation byte-for-byte.

| Header          | Description                                                                                                                                                                                                                                                                |
| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Date`          | The current timestamp in [HTTP Date format](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date) (e.g., `Thu, 29 Jun 2017 12:11:16 GMT`). **Note:** Our server clock tolerance is **±5 seconds**. You must regenerate the date and signature for every request. |
| `Content-Type`  | Describes your payload (e.g., `application/json; charset=utf-8`).                                                                                                                                                                                                          |
| `Authorization` | The authentication string. Format: `AppName [tracker_id]:[signature]`                                                                                                                                                                                                      |

#### Step 2: Canonical string construction

Concatenate the following four strings, separated by a newline character (`\n`).

1. **HTTP Verb:** Uppercase (e.g., `POST`, `PUT`, `DELETE`).
2. **Content-Type:** Exactly as sent in the header (e.g., `application/json; charset=utf-8`).
3. **Date:** Exactly as sent in the header.
4. **Resource Path:** The absolute path **excluding** the query string (e.g., `/v1/content`, not `/v1/content?foo=bar`).

#### Step 3: Signature calculation

1. Compute the HMAC-SHA256 digest of the Canonical String using your **Secret Key**.
2. Encode the binary digest into a **Base64** string.

### Example implementations

```ruby
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
<?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);
```

```javascript
// 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 contentType = "application/json; charset=utf-8";

var requestUri = request.url.replace(/^.*\/\/[^\/]+/, "").replace(/\?.*$/, "");
var timestamp = new Date().toUTCString();
var signature = [request.method, contentType, timestamp, requestUri].join("\n");

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

pm.request.headers.add({
  key: "Content-Type",
  value: contentType,
});
pm.request.headers.add({
  key: "Authorization",
  value: "ApiAuth " + publicKey + ":" + encryptedSignature,
});
pm.request.headers.add({
  key: "Date",
  value: timestamp,
});
```

```shell
#!/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")
```

---

## User identification

Luigi's Box relies on a unified user identity model across analytics and live APIs (Search, Autocomplete, Product Listing, and Recommender) to deliver effective personalization.

The platform tracks two distinct identity concepts:

- **Anonymous / Device user:** A stable anonymous identifier for the current browser, device, or app install. It is not tied to a logged-in account. For integrations using the [DataLayer collector](/analytics/collector/), this value is typically available as the [`_lb` cookie](/analytics/collector/#user-identifiers) value. For fully custom API integrations, your application must generate, persist, and reuse this value across analytics events and live API requests.
- **Authenticated / Logged-in user:** A stable identifier of the logged-in user in your application. This should be your own durable account or customer identifier, not a temporary request identifier.

Because the Events API (analytics) and Live APIs (Search, Autocomplete, etc.) refer to these concepts using different parameter names, it is essential to map them correctly.

### Events API (Analytics)

When sending events to the Events API (`api.luigisbox.com`), you use the following parameters:

- `client_id`: Carries the **Anonymous / Device user** identifier. You must always include `client_id`, and it must stay the same for the same browser or device, even after the user logs in or logs out.
- `customer_id`: Carries the **Authenticated / Logged-in user** identifier. Send this alongside `client_id` when the user is logged in. Do not overwrite `client_id` with the customer identifier. See the [Events API common parameters](/analytics/api/events/#common-parameters) and [Identity rules](/analytics/api/events/#identity-rules).
- `consent_granted`: Set this to `true` when the user has accepted personalization cookies or equivalent consent. This is a consent flag, not an identifier, but it tells the Events API that the event can be used for personalization.

### Live APIs

When querying live services (`live.luigisbox.com`) like Search, Autocomplete, Product Listing, or Recommender, the identity parameters map slightly differently to support personalization:

- `client_id`: Passed to maintain the **Anonymous / Device user** identity. This should match the `client_id` sent in analytics. For integrations using the [DataLayer collector](/analytics/collector/), this value is typically available as the [`_lb` cookie](/analytics/collector/#user-identifiers) value.
- `user_id`: Carries the identifier Luigi's Box should personalize on for that request. It is not a third identity system. Its value must be one of the two identities defined above:
  - For **anonymous users**, set this to the `client_id`.
  - For **authenticated users**, set this to the `customer_id`.
  - Omit `user_id` entirely for non-personalized requests.

### Mapping reference

The following table summarizes the parameter mapping across the two API systems based on the user's state:

In this table, **Personalized** describes the Live API behavior, while **Consent Granted** describes the Events API consent state that allows personalization.

| User state                                             | Events API                                                                                         | Live APIs                                          |
| :----------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :------------------------------------------------- |
| **Anonymous**<br/>(Personalized / Consent Granted)     | `client_id` = device ID<br/>_(e.g., browser-123)_<br/>`consent_granted` = `true`                   | `client_id` = device ID<br/>`user_id` = device ID  |
| **Authenticated**<br/>(Personalized / Consent Granted) | `client_id` = device ID<br/>`customer_id` = account ID _(e.g., user-42)_<br/>`consent_granted` = `true` | `client_id` = device ID<br/>`user_id` = account ID |
| **Non-personalized**                                   | `client_id` = device ID<br/>`customer_id` = account ID _(if authenticated)_<br/>`consent_granted` omitted or `false` | `client_id` = device ID<br/>`user_id` omitted      |

## Rate limiting (throttling)

We enforce rate limits to ensure stability. Limits are applied per **Tracker ID** (Account level) and per **IP Address**.

| Endpoint            | Account Limit                    | IP Limit    |
| :------------------ | :------------------------------- | :---------- |
| **Content Updates** | 500 req / min, 5 concurrent reqs | -           |
| **Autocomplete**    | 800 req / min                    | 30 req / 5s |
| **Search**          | 350 req / min                    | 30 req / 5s |
| **Recommender**     | 60 req / 5s                      | 30 req / 5s |

### Handle 429 errors

If you exceed a limit, you will receive a `429 Too Many Requests` status. Your application **must** handle this gracefully.

We provide a `Retry-After` header indicating how many seconds you must wait.

<div class="lb-mermaid-frame"><pre class="mermaid">sequenceDiagram
participant Client
participant API
Client->>API: POST content request
API-->>Client: 429 Retry-After 5 seconds
Note over Client: Wait 5 seconds
Client->>API: Retry content request
API-->>Client: 200 OK</pre></div>

**Recommended Strategy:**

- **Realtime APIs (Search, Autocomplete):** Do not retry immediately. Discard the request.
- **Batch APIs (Content Updates):** Use an exponential backoff strategy (wait `Retry-After`, then retry).

---

## Error handling

You should design your integration to be resilient to network failures.

| Code    | Reason              | Type          | Action                                                                                                |
| :------ | :------------------ | :------------ | :---------------------------------------------------------------------------------------------------- |
| **401** | Auth Failed         | **Fatal**     | Check your Signature generation. The response body contains the string we expected vs. what you sent. |
| **408** | Timeout             | **Transient** | Safe to retry once.                                                                                   |
| **429** | Throttling          | **Transient** | Wait and retry (see above).                                                                           |
| **500** | Server Error        | **Fatal**     | Log the error and contact support.                                                                    |
| **502** | Bad Gateway         | **Transient** | Usually a deploy or network blip. Retry after 1s.                                                     |
| **503** | Service Unavailable | **Transient** | Check [luigisboxstatus.com](https://www.luigisboxstatus.com/).                                        |

### Development mode

To ensure your integration is robust, we may enable **Development Mode** on your account during the integration phase.

**What it does:**

- A small percentage of your requests will deliberately fail with various error codes (408, 429, 500).
- These errors are marked in the JSON body as `development_mode: true`.

**Why:**
This forces your developers to write proper error handling and retry logic _before_ you go to production.

:::note
**Note:** Development mode is never enabled on Production accounts without explicit consent.
:::
