---
title: Content Update (POST)
description: Add new objects or completely replace existing ones in your Luigi's Box search index using the POST /v1/content endpoint.
slug: indexing/api/v1/content-update
docKind: endpoint
hub: indexing
tableOfContents: true
---
import ApiSection from "../../../../../components/ApiSection.astro";
import ApiEndpoint from "../../../../../components/ApiEndpoint.astro";
import ApiCodeTabs from "../../../../../components/ApiCodeTabs.astro";
import { Aside } from "@astrojs/starlight/components";

<Aside type="note">
  This endpoint requires HMAC authentication. Refer to the [Authentication](/platform-foundations/api-principles/#authentication) documentation for details.
</Aside>

<ApiSection>
  <div slot="code">
    <ApiEndpoint method="POST" url="https://live.luigisbox.com/v1/content" />
  </div>
## Overview

This endpoint is the primary method for adding new objects or completely replacing existing ones in your search index. You send an array of objects in a JSON payload, and Luigi's Box processes them in a batch.

<Aside type="danger">
  This is a **full replacement operation**. When you update an existing object, any fields you omit from the request will be removed from the index. If you only need to update specific fields, it is more efficient to use the [Partial Content Update API](/indexing/api/v1/partial-update/).
</Aside>
</ApiSection>

<ApiSection>
## The index-object

The fundamental unit of data you send is the `index-object`. Your request body must be a JSON object containing a single root key, `objects`, which holds an array of these `index-objects`.

### Top-level Request Structure

| Parameter | Type | Required | Description |
| :-- | :-- | :-- | :-- |
| `objects` | Array | ✓ | An array of one or more objects to be indexed. Recommended batch size: around 100 objects. |

### Index-Object Parameters

Each object in the `objects` array must follow this structure:

| Parameter | Type | Required | Description |
| :-- | :-- | :-- | :-- |
| `identity` | String | ✓ | A unique identity for the object at the index level. It must match the identity reported by analytics events. See the [Identity guide](/platform-foundations/identity/). |
| `type` | String | ✓ | Object type such as `item`, `category`, or `article`. Different types can be searched separately. |
| `generation` | String |  | Object generation marker for bulk data synchronization. |
| `active_from` | String |  | ISO 8601 date/time when the object should become searchable, for example `2019-05-17T21:12:35+00:00`. |
| `active_to` | String |  | ISO 8601 date/time when the object should stop being searchable, for example `2019-05-17T21:12:35+00:00`. |
| `fields` | Object | ✓ | Object attributes. Every field is searchable and can be used for filtering. It must include a `title` field. |
| `nested` | Array |  | Array of nested objects such as categories or variants linked to the current object. |

## Fields guidelines

When structuring the `fields` object:

- **Required:** Include a `title` field. This serves as the object display name.
- **Numeric fields:** Format values as numbers, not strings, for example `"height": 70.5`.
- **Date fields:** Use ISO 8601 format with the `T` delimiter.
- **Array values:** Arrays are supported for any field, for example `"color": ["red", "black", "blue"]`.
- **Nesting:** Use only one level of nesting and avoid deeply nested objects.
- **Facets:** Any field you send can become a facet for filtering search results.

Read more about [structuring your data](/indexing/data-layout/).

  <div slot="code">
    <h4 class="code-section-title">Example: Index-Object</h4>

```json
{
  "identity": "SKU-XYZ-123",
  "type": "item",
  "generation": "daily-sync-20241014",
  "active_from": "2024-01-01T00:00:00Z",
  "fields": {
    "title": "Premium Quality T-Shirt",
    "web_url": "/products/premium-t-shirt",
    "price": 25.5,
    "color": ["Red", "Black", "Blue"],
    "availability": 1
  },
  "nested": [
    {
      "type": "category",
      "identity": "cat-apparel",
      "fields": { "title": "Apparel" }
    }
  ]
}
```

  </div>
</ApiSection>

<ApiSection>
## How to Send an Indexing Request

The examples show how to prepare and send your `objects` array to the API endpoint.

Authentication is required for all requests. The code demonstrates how to construct the necessary headers, including `Date` and `Authorization`. The `Authorization` header requires a dynamically generated HMAC signature. For a detailed explanation of how to create this signature, refer to the main API Authentication guide.

<Aside type="danger">
  Your **Private Key** is a secret and should never be exposed in client-side code, such as frontend JavaScript. It should only be used on a secure server.
</Aside>

<Aside type="tip">
  Your API Keys can be found in the Luigi's Box application under "General settings" > "Integration settings".
</Aside>

  <div slot="code">
    <h4 class="code-section-title">Code Examples</h4>
  <ApiCodeTabs syncKey="indexing-content-update-request">
    <div slot="ruby">

```ruby
# --- Ruby Example for POST /v1/content ---
# Assumes 'faraday' gem is installed (gem install faraday)

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

def generate_luigisbox_digest(private_key, http_method, endpoint_path, date_header, content_type_header)
  data = "#{http_method}\n#{content_type_header}\n#{date_header}\n#{endpoint_path}"
  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, private_key, data)).strip
end

YOUR_PUBLIC_KEY = "your_public_api_key"
YOUR_PRIVATE_KEY = "your_private_api_key"
LUIGISBOX_HOST = 'https://live.luigisbox.com'
ENDPOINT_PATH = '/v1/content'

product_data = [
  {
    identity: "product-001",
    type: "item",
    fields: {
      title: "My Very First Item",
      web_url: "https://www.example.com/products/product-001",
      price: "EUR 49.99",
      brand: "CoolBrand",
      availability: 1
    }
  }
]
request_body_json = { objects: product_data }.to_json

http_method = 'POST'
content_type = 'application/json; charset=utf-8'
current_date = Time.now.httpdate

signature = generate_luigisbox_digest(YOUR_PRIVATE_KEY, http_method, ENDPOINT_PATH, current_date, content_type)
authorization_header = "ApiAuth #{YOUR_PUBLIC_KEY}:#{signature}"

connection = Faraday.new(url: LUIGISBOX_HOST) do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.post(ENDPOINT_PATH) do |req|
  req.headers['Content-Type'] = content_type
  req.headers['Date'] = current_date
  req.headers['Authorization'] = authorization_header
  req.body = request_body_json
end

response_body = JSON.parse(response.body)
if response.success? && response_body['ok_count'] && response_body['ok_count'] > 0
  puts "Product successfully indexed:"
  puts JSON.pretty_generate(response_body)
else
  puts "Error indexing product: HTTP Status #{response.status}, Response: #{response.body}"
end
```
    </div>
    <div slot="php">

```php
<?php
// PHP Example for POST /v1/content
// Assumes Guzzle is installed:
// composer require guzzlehttp/guzzle

require 'vendor/autoload.php';

use GuzzleHttp\Client;

function generateLuigisboxDigest($privateKey, $httpMethod, $endpointPath, $dateHeader, $contentTypeHeader) {
    $data = "{$httpMethod}\n{$contentTypeHeader}\n{$dateHeader}\n{$endpointPath}";
    $hash = hash_hmac('sha256', $data, $privateKey, true);
    return trim(base64_encode($hash));
}

$YOUR_PUBLIC_KEY  = "your_public_api_key";
$YOUR_PRIVATE_KEY = "your_private_api_key";
$LUIGISBOX_HOST   = 'https://live.luigisbox.com';
$ENDPOINT_PATH    = '/v1/content';

$product_data = [
    [
        'identity' => 'product-001',
        'type' => 'item',
        'fields' => [
            'title' => 'My Very First Item',
            'web_url' => 'https://www.example.com/products/product-001',
            'price' => 'EUR 49.99',
            'brand' => 'CoolBrand',
            'availability' => 1
        ]
    ]
];

$request_body = ['objects' => $product_data];
$http_method  = 'POST';
$content_type = 'application/json; charset=utf-8';
$current_date = gmdate('D, d M Y H:i:s') . ' GMT';

$signature = generateLuigisboxDigest($YOUR_PRIVATE_KEY, $http_method, $ENDPOINT_PATH, $current_date, $content_type);
$authorization_header = "ApiAuth {$YOUR_PUBLIC_KEY}:{$signature}";

$client = new GuzzleHttp\Client();
$response = $client->request(
    $http_method,
    "{$LUIGISBOX_HOST}{$ENDPOINT_PATH}",
    [
        'headers' => [
            'Accept-Encoding' => 'gzip, deflate',
            'Content-Type' => $content_type,
            'Date' => $current_date,
            'Authorization' => $authorization_header,
        ],
        'json' => $request_body
    ]
);

$response_body = json_decode($response->getBody(), true);

if ($response->getStatusCode() == 200 && isset($response_body['ok_count']) && $response_body['ok_count'] > 0) {
    echo "Product successfully indexed:" . PHP_EOL;
    echo json_encode($response_body, JSON_PRETTY_PRINT);
} else {
    echo "Error indexing product: HTTP Status " . $response->getStatusCode() . "\nResponse: " . $response->getBody();
}
```
    </div>
    <div slot="javascript">

```javascript
const axios = require('axios');
const crypto = require('crypto');

function generateLuigisBoxDigest(privateKey, httpMethod, endpointPath, dateHeader, contentTypeHeader) {
  const data = `${httpMethod}\n${contentTypeHeader}\n${dateHeader}\n${endpointPath}`;
  const hmac = crypto.createHmac('sha256', privateKey);
  hmac.update(data);
  return hmac.digest('base64').trim();
}

const YOUR_PUBLIC_KEY = "your_public_api_key";
const YOUR_PRIVATE_KEY = "your_private_api_key";
const LUIGISBOX_HOST = 'https://live.luigisbox.com';
const ENDPOINT_PATH = '/v1/content';

const productData = [
  {
    identity: "product-001",
    type: "item",
    fields: {
      title: "My Very First Item",
      web_url: "https://www.example.com/products/product-001",
      price: "EUR 49.99",
      brand: "CoolBrand",
      availability: 1
    }
  }
];

const requestBody = { objects: productData };
const httpMethod = 'POST';
const contentType = 'application/json; charset=utf-8';
const currentDate = new Date().toUTCString();

const signature = generateLuigisBoxDigest(YOUR_PRIVATE_KEY, httpMethod, ENDPOINT_PATH, currentDate, contentType);
const authorizationHeader = `ApiAuth ${YOUR_PUBLIC_KEY}:${signature}`;

axios({
  method: httpMethod,
  url: LUIGISBOX_HOST + ENDPOINT_PATH,
  headers: {
    'Content-Type': contentType,
    'Date': currentDate,
    'Authorization': authorizationHeader
  },
  data: requestBody
})
  .then((response) => {
    const responseBody = response.data;
    if (response.status === 200 && responseBody.ok_count && responseBody.ok_count > 0) {
      console.log("Product successfully indexed:", JSON.stringify(responseBody, null, 2));
    } else {
      console.error("Error indexing product: HTTP Status " + response.status + ", Response: " + JSON.stringify(responseBody));
    }
  })
  .catch((error) => {
    console.error("Exception:", error.message);
  });
```
    </div>
  </ApiCodeTabs>

  </div>
</ApiSection>

<ApiSection>
## Error handling

| HTTP Status | Description |
| :-- | :-- |
| **400 Bad Request** | The request structure is invalid, JSON is malformed, or some objects failed validation. Check the response body for details. |
| **413 Payload Too Large** | The request exceeds 5 MB, or 10 MB if compressed. Reduce batch size or enable compression. |
| **429 Too Many Requests** | The rate limit has been exceeded. Check the `Retry-After` header and see the [Throttling docs](/platform-foundations/api-principles/#throttling). |

  <div slot="code">
    <h4 class="code-section-title">Example: Error Response</h4>

```json
{
  "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"]
      }
    }
  }
}
```

  </div>
</ApiSection>

## Performance guidelines

For optimal indexing performance, avoid:

- **High nested count:** More than 10 nested records per object.
- **High field count:** More than 10 fields per object.
- **Large objects:** More than 30 KB per object.
- **Large batches:** More than 100 objects per request.

## Related endpoints

- [Partial Content Update](/indexing/api/v1/partial-update/) updates specific fields only.
- [Content Removal](/indexing/api/v1/content-removal/) deletes objects from the index.
- [Update by Query](/indexing/api/v1/query-update/) performs bulk updates based on search criteria.
