---
title: Bulk Create Customizations
description: Create multiple customizations in a single request.
slug: recommendations/customization/bulk-create
docKind: endpoint
hub: recommendations
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";

<ApiSection>
  <div slot="code">
    <ApiEndpoint method="POST" url="https://live.luigisbox.com/v1/recommender/pin/{TRACKER_ID}/scopes/_bulk" />
  </div>
## Overview

Create multiple customizations in one API call. This endpoint accepts NDJSON, where each line is one customization object.

<Aside type="caution">
  This endpoint requires HMAC authentication. See [Authentication](/platform-foundations/api-principles/#authentication).
</Aside>

Each line in the request body must be a valid JSON object. Up to 5000 pin scopes can be sent in a single request.
</ApiSection>

<ApiSection>
## Request parameters

### Path parameters

| Parameter | Type | Required | Description |
| :-- | :-- | :-- | :-- |
| `TRACKER_ID` | string | ✓ | Your unique tracker identifier. |

### Query parameters

| Parameter | Type | Default | Description |
| :-- | :-- | :-- | :-- |
| `conflict_strategy` | string | `raise` | Conflict handling mode: `raise`, `ignore`, or `override`. |

### Request headers

| Header | Value | Description |
| :-- | :-- | :-- |
| `Content-Type` | `application/x-ndjson` | Required. |
| `Date` | HTTP date | Required for HMAC. |
| `Authorization` | `ApiAuth {TRACKER_ID}:{SIGNATURE}` | Required for HMAC. |

  <div slot="code">
    <h4 class="code-section-title">Example Request</h4>

```shell
tracker_id="1234-5678"
private_key="your_private_api_key"
host="https://live.luigisbox.com"
base_path="/v1/recommender/pin/${tracker_id}/scopes/_bulk"
full_path="${base_path}?conflict_strategy=override"
method="POST"
content_type="application/x-ndjson"
date=$(date -u "+%a, %d %b %Y %H:%M:%S GMT")

string_to_sign="$(printf "${method}\n${content_type}\n${date}\n${base_path}")"
signature=$(echo -n "${string_to_sign}" | openssl dgst -sha256 -hmac "${private_key}" -binary | base64)

curl -X ${method} "${host}${full_path}" \
  -H "Content-Type: ${content_type}" \
  -H "Date: ${date}" \
  -H "Authorization: ApiAuth ${tracker_id}:${signature}" \
  --data-binary @pins.ndjson
```
  </div>
</ApiSection>

<ApiSection>
## Request body format

The request body uses [NDJSON](https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson). Each line must be a valid customization object.

<Aside type="tip">
  Each line follows the same schema as [Create Customization](/recommendations/customization/create-customization/).
</Aside>

  <div slot="code">
    <h4 class="code-section-title">Request Body Example</h4>

```json
{"target_type": "all", "model": "basket", "pin_definitions": [{"pin_type": "item", "pin_identity": "123"}]}
{"target_type": "item", "target_identity": "456", "model": "basket", "pin_definitions": [{"pin_type": "item", "pin_identity": "789"}]}
```
  </div>
</ApiSection>

<ApiSection>
## How to Make a Request

1. Build the HMAC string from the method, content type, date, and base resource path.
2. Exclude query parameters from the signed path.
3. Send the NDJSON body together with `Date`, `Content-Type`, and `Authorization`.

<div slot="code">
  <h4 class="code-section-title">Code Examples</h4>
  <ApiCodeTabs syncKey="recommendations-customization-bulk-create">
    <div slot="ruby">

```ruby
require 'faraday'
require 'base64'
require 'openssl'
require 'json'
require 'time'

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}"
  Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', private_key, data)).strip
end

tracker_id = 'YOUR_TRACKER_ID'
base_path = "/v1/recommender/pin/#{tracker_id}/scopes/_bulk"
full_path = "/v1/recommender/pin/#{tracker_id}/scopes/_bulk?conflict_strategy=override"
content_type = 'application/x-ndjson'
date_header = Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S GMT")
signature = generate_luigisbox_digest('your_private_api_key', 'POST', base_path, date_header, content_type)

ndjson_data = [
  { target_type: 'all', model: 'basket', pin_definitions: [{ pin_type: 'item', pin_identity: '123' }] },
  { target_type: 'item', target_identity: '456', model: 'basket', pin_definitions: [{ pin_type: 'item', pin_identity: '789' }] }
].map(&:to_json).join("\n")

response = Faraday.new(url: 'https://live.luigisbox.com').post(full_path) do |req|
  req.headers['Date'] = date_header
  req.headers['Content-Type'] = content_type
  req.headers['Authorization'] = "ApiAuth #{tracker_id}:#{signature}"
  req.body = ndjson_data
end

puts response.status
```
    </div>
    <div slot="php">

```php
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

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

$trackerId = 'YOUR_TRACKER_ID';
$basePath = "/v1/recommender/pin/{$trackerId}/scopes/_bulk";
$requestPath = "/v1/recommender/pin/{$trackerId}/scopes/_bulk?conflict_strategy=override";
$contentType = 'application/x-ndjson';
$date = gmdate('D, d M Y H:i:s') . ' GMT';
$signature = generateLuigisboxDigest('your_private_api_key', 'POST', $basePath, $date, $contentType);

$rows = [
    ["target_type" => "all", "model" => "basket", "pin_definitions" => [["pin_type" => "item", "pin_identity" => "123"]]],
    ["target_type" => "item", "target_identity" => "456", "model" => "basket", "pin_definitions" => [["pin_type" => "item", "pin_identity" => "789"]]],
];
$ndjson = implode("\n", array_map('json_encode', $rows));

$client = new Client();
$response = $client->request('POST', "https://live.luigisbox.com{$requestPath}", [
    'headers' => [
        'Content-Type' => $contentType,
        'Date' => $date,
        'Authorization' => "ApiAuth {$trackerId}:{$signature}",
    ],
    'body' => $ndjson,
]);

echo $response->getStatusCode();
```
    </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}`;
  return crypto.createHmac('sha256', privateKey).update(data).digest('base64').trim();
}

const trackerId = 'YOUR_TRACKER_ID';
const basePath = `/v1/recommender/pin/${trackerId}/scopes/_bulk`;
const requestPath = `/v1/recommender/pin/${trackerId}/scopes/_bulk?conflict_strategy=override`;
const contentType = 'application/x-ndjson';
const dateHeader = new Date().toUTCString();
const signature = generateLuigisBoxDigest('your_private_api_key', 'POST', basePath, dateHeader, contentType);

const rows = [
  { target_type: 'all', model: 'basket', pin_definitions: [{ pin_type: 'item', pin_identity: '123' }] },
  { target_type: 'item', target_identity: '456', model: 'basket', pin_definitions: [{ pin_type: 'item', pin_identity: '789' }] },
];

axios({
  method: 'POST',
  url: `https://live.luigisbox.com${requestPath}`,
  headers: {
    'Content-Type': contentType,
    Date: dateHeader,
    Authorization: `ApiAuth ${trackerId}:${signature}`,
  },
  data: rows.map(JSON.stringify).join('\n'),
}).then((response) => console.log(response.status));
```
    </div>
  </ApiCodeTabs>
</div>
</ApiSection>

<ApiSection>
## Response structure

Returns `200 OK` on success with an empty JSON object.

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

```json
{}
```
  </div>
</ApiSection>

<ApiSection>
## Error handling

| HTTP Status | Description |
| :-- | :-- |
| `200 OK` | Success. |
| `401 Unauthorized` | Missing or invalid authentication. |
| `404 Not Found` | Tracker not found. |
| `409 Conflict` | Conflict detected when `conflict_strategy=raise`. |
| `413 Content Too Large` | Too many customizations in one request. |
| `422 Unprocessable Entity` | Invalid pin definition provided. |

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

```json
{
  "reason": "Invalid pin definition provided",
  "exception_details": {
    "pin_definitions": ["Missing required field: position"],
    "target_type": ["Must be one of: item, criteria, all"]
  }
}
```
  </div>
</ApiSection>
