curl --request POST \
--url https://www.closient.com/products/api/v1/products/batch \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"dry_run": true,
"fetched_at": "2025-12-01T00:00:00Z",
"reclaim_unattributed": true,
"rows": [
{
"brand_name": "Example Beauty",
"product": {
"product_name": "Hydrating Face Serum 30ml"
},
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
}
],
"source": "ulta_catalog",
"source_file": "ulta_catalog_2025-12.psv"
}
'import requests
url = "https://www.closient.com/products/api/v1/products/batch"
payload = {
"dry_run": True,
"fetched_at": "2025-12-01T00:00:00Z",
"reclaim_unattributed": True,
"rows": [
{
"brand_name": "Example Beauty",
"product": { "product_name": "Hydrating Face Serum 30ml" },
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
}
],
"source": "ulta_catalog",
"source_file": "ulta_catalog_2025-12.psv"
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
dry_run: true,
fetched_at: '2025-12-01T00:00:00Z',
reclaim_unattributed: true,
rows: [
{
brand_name: 'Example Beauty',
product: {product_name: 'Hydrating Face Serum 30ml'},
raw_gtin: '614141000036',
row_number: 42,
sku: '2512345',
source_reference: '2512345'
}
],
source: 'ulta_catalog',
source_file: 'ulta_catalog_2025-12.psv'
})
};
fetch('https://www.closient.com/products/api/v1/products/batch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.closient.com/products/api/v1/products/batch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'dry_run' => true,
'fetched_at' => '2025-12-01T00:00:00Z',
'reclaim_unattributed' => true,
'rows' => [
[
'brand_name' => 'Example Beauty',
'product' => [
'product_name' => 'Hydrating Face Serum 30ml'
],
'raw_gtin' => '614141000036',
'row_number' => 42,
'sku' => '2512345',
'source_reference' => '2512345'
]
],
'source' => 'ulta_catalog',
'source_file' => 'ulta_catalog_2025-12.psv'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/products/api/v1/products/batch"
payload := strings.NewReader("{\n \"dry_run\": true,\n \"fetched_at\": \"2025-12-01T00:00:00Z\",\n \"reclaim_unattributed\": true,\n \"rows\": [\n {\n \"brand_name\": \"Example Beauty\",\n \"product\": {\n \"product_name\": \"Hydrating Face Serum 30ml\"\n },\n \"raw_gtin\": \"614141000036\",\n \"row_number\": 42,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\"\n }\n ],\n \"source\": \"ulta_catalog\",\n \"source_file\": \"ulta_catalog_2025-12.psv\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://www.closient.com/products/api/v1/products/batch")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"dry_run\": true,\n \"fetched_at\": \"2025-12-01T00:00:00Z\",\n \"reclaim_unattributed\": true,\n \"rows\": [\n {\n \"brand_name\": \"Example Beauty\",\n \"product\": {\n \"product_name\": \"Hydrating Face Serum 30ml\"\n },\n \"raw_gtin\": \"614141000036\",\n \"row_number\": 42,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\"\n }\n ],\n \"source\": \"ulta_catalog\",\n \"source_file\": \"ulta_catalog_2025-12.psv\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/products/api/v1/products/batch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"dry_run\": true,\n \"fetched_at\": \"2025-12-01T00:00:00Z\",\n \"reclaim_unattributed\": true,\n \"rows\": [\n {\n \"brand_name\": \"Example Beauty\",\n \"product\": {\n \"product_name\": \"Hydrating Face Serum 30ml\"\n },\n \"raw_gtin\": \"614141000036\",\n \"row_number\": 42,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\"\n }\n ],\n \"source\": \"ulta_catalog\",\n \"source_file\": \"ulta_catalog_2025-12.psv\"\n}"
response = http.request(request)
puts response.read_body{
"committed": true,
"ingredients_queued": 1,
"result": {
"duplicate_gtins_in_feed": 0,
"listings_created": 1,
"listings_updated": 0,
"products_created": 1,
"products_enriched": 0,
"products_unchanged": 0
},
"rows": [
{
"detail": "",
"gtin": "00614141000036",
"index": 0,
"ingredients_pending": true,
"listing": "created",
"outcome": "created",
"reason": "",
"row_number": 42,
"source_reference": "2512345"
}
]
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}Create or update up to 1,000 products in one call
Merge a batch of normalised catalog rows into the shared catalog synchronously and return what happened to each row. It is the same GTIN-first merge a catalog import job runs — one product per GTIN across every retailer, one listing per retailer, per-field provenance, and a populated field displaced only by a strictly higher measured confidence — without opening a job, staging rows or polling.
The call is idempotent: sending the same batch again reports every product unchanged and writes nothing. Each call is one transaction, so a batch lands whole or not at all.
Ingredient labels are not parsed inside the request. A committed batch queues the labels it changed for background parsing and reports how many in ingredients_queued.
dry_run defaults to true. Send dry_run: false to commit.
curl --request POST \
--url https://www.closient.com/products/api/v1/products/batch \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"dry_run": true,
"fetched_at": "2025-12-01T00:00:00Z",
"reclaim_unattributed": true,
"rows": [
{
"brand_name": "Example Beauty",
"product": {
"product_name": "Hydrating Face Serum 30ml"
},
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
}
],
"source": "ulta_catalog",
"source_file": "ulta_catalog_2025-12.psv"
}
'import requests
url = "https://www.closient.com/products/api/v1/products/batch"
payload = {
"dry_run": True,
"fetched_at": "2025-12-01T00:00:00Z",
"reclaim_unattributed": True,
"rows": [
{
"brand_name": "Example Beauty",
"product": { "product_name": "Hydrating Face Serum 30ml" },
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
}
],
"source": "ulta_catalog",
"source_file": "ulta_catalog_2025-12.psv"
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
dry_run: true,
fetched_at: '2025-12-01T00:00:00Z',
reclaim_unattributed: true,
rows: [
{
brand_name: 'Example Beauty',
product: {product_name: 'Hydrating Face Serum 30ml'},
raw_gtin: '614141000036',
row_number: 42,
sku: '2512345',
source_reference: '2512345'
}
],
source: 'ulta_catalog',
source_file: 'ulta_catalog_2025-12.psv'
})
};
fetch('https://www.closient.com/products/api/v1/products/batch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.closient.com/products/api/v1/products/batch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'dry_run' => true,
'fetched_at' => '2025-12-01T00:00:00Z',
'reclaim_unattributed' => true,
'rows' => [
[
'brand_name' => 'Example Beauty',
'product' => [
'product_name' => 'Hydrating Face Serum 30ml'
],
'raw_gtin' => '614141000036',
'row_number' => 42,
'sku' => '2512345',
'source_reference' => '2512345'
]
],
'source' => 'ulta_catalog',
'source_file' => 'ulta_catalog_2025-12.psv'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/products/api/v1/products/batch"
payload := strings.NewReader("{\n \"dry_run\": true,\n \"fetched_at\": \"2025-12-01T00:00:00Z\",\n \"reclaim_unattributed\": true,\n \"rows\": [\n {\n \"brand_name\": \"Example Beauty\",\n \"product\": {\n \"product_name\": \"Hydrating Face Serum 30ml\"\n },\n \"raw_gtin\": \"614141000036\",\n \"row_number\": 42,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\"\n }\n ],\n \"source\": \"ulta_catalog\",\n \"source_file\": \"ulta_catalog_2025-12.psv\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://www.closient.com/products/api/v1/products/batch")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"dry_run\": true,\n \"fetched_at\": \"2025-12-01T00:00:00Z\",\n \"reclaim_unattributed\": true,\n \"rows\": [\n {\n \"brand_name\": \"Example Beauty\",\n \"product\": {\n \"product_name\": \"Hydrating Face Serum 30ml\"\n },\n \"raw_gtin\": \"614141000036\",\n \"row_number\": 42,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\"\n }\n ],\n \"source\": \"ulta_catalog\",\n \"source_file\": \"ulta_catalog_2025-12.psv\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/products/api/v1/products/batch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"dry_run\": true,\n \"fetched_at\": \"2025-12-01T00:00:00Z\",\n \"reclaim_unattributed\": true,\n \"rows\": [\n {\n \"brand_name\": \"Example Beauty\",\n \"product\": {\n \"product_name\": \"Hydrating Face Serum 30ml\"\n },\n \"raw_gtin\": \"614141000036\",\n \"row_number\": 42,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\"\n }\n ],\n \"source\": \"ulta_catalog\",\n \"source_file\": \"ulta_catalog_2025-12.psv\"\n}"
response = http.request(request)
puts response.read_body{
"committed": true,
"ingredients_queued": 1,
"result": {
"duplicate_gtins_in_feed": 0,
"listings_created": 1,
"listings_updated": 0,
"products_created": 1,
"products_enriched": 0,
"products_unchanged": 0
},
"rows": [
{
"detail": "",
"gtin": "00614141000036",
"index": 0,
"ingredients_pending": true,
"listing": "created",
"outcome": "created",
"reason": "",
"row_number": 42,
"source_reference": "2512345"
}
]
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}Authorizations
Body
Up to 1,000 normalised rows to merge into the catalog in this request.
Catalog source this batch merges under. One of: sephora_catalog, ulta_catalog. It decides the canonical retailer, its storefront and the per-field confidence table, exactly as it does for a catalog import job.
ulta_catalog, sephora_catalog Rows to merge, at most 1,000 per call; a larger batch is refused with 422 before anything is written. The batch is one transaction — every row lands or none does. A barcode repeated inside the batch is merged once and the repeats are reported as duplicate; the server does not remember barcodes across calls, so a feed split over several calls should drop its own repeats between them.
1 - 1000 elementsShow child attributes
Show child attributes
Name of the feed drop these rows came from, recorded as listing provenance.
255When the feed drop was fetched. Send the drop's own timestamp; omitting it stamps the time of this request, which misdates a feed loaded weeks after it was published.
Run the real merge and roll it back. Default. The outcomes and counts are the ones a committing call would return, so this is a rehearsal rather than an estimate. Send false to commit; committing is limited to Closient staff and the catalog-import service account, and anyone else gets 403.
Allow this batch to claim fields on imported products that carry no recorded provenance, as a catalog import job does by default. A brand-claimed product is never touched either way.
Response
OK
The batch's census and one outcome per row.
True when the batch was written; false for a dry run.
The same census a catalog import job reports: products created, enriched and unchanged, listings created and updated, duplicates, quarantine counts by reason, per-field decisions, and brand and ingredient figures. A dry run reports the labels it would parse as ingredients_would_parse.
One entry per input row, in request order.
Show child attributes
Show child attributes
Ingredient labels handed to background parsing after the commit. Parsing runs at roughly 20 labels a second, so it is kept off the request; zero on a dry run.