curl --request POST \
--url https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"attributes": {
"Category1": "Skin Care",
"Category2": "Moisturizers",
"highlights": [
"Vegan",
"Fragrance Free"
]
},
"badges": [
{
"label": "Good For Oily Hair"
},
{
"label": "Clean at Sephora"
}
],
"gtin": "00614141000036",
"is_active": true,
"sku": "2512345",
"source_reference": "2512345",
"urls": [
{
"store_url": "https://www.sephora.com",
"url": "https://www.sephora.com/product/example-P123456"
}
]
}
'import requests
url = "https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings"
payload = {
"attributes": {
"Category1": "Skin Care",
"Category2": "Moisturizers",
"highlights": ["Vegan", "Fragrance Free"]
},
"badges": [{ "label": "Good For Oily Hair" }, { "label": "Clean at Sephora" }],
"gtin": "00614141000036",
"is_active": True,
"sku": "2512345",
"source_reference": "2512345",
"urls": [
{
"store_url": "https://www.sephora.com",
"url": "https://www.sephora.com/product/example-P123456"
}
]
}
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({
attributes: {
Category1: 'Skin Care',
Category2: 'Moisturizers',
highlights: ['Vegan', 'Fragrance Free']
},
badges: [{label: 'Good For Oily Hair'}, {label: 'Clean at Sephora'}],
gtin: '00614141000036',
is_active: true,
sku: '2512345',
source_reference: '2512345',
urls: [
{
store_url: 'https://www.sephora.com',
url: 'https://www.sephora.com/product/example-P123456'
}
]
})
};
fetch('https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings', 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/retailers/api/v1/retailers/{retailer_id}/listings",
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([
'attributes' => [
'Category1' => 'Skin Care',
'Category2' => 'Moisturizers',
'highlights' => [
'Vegan',
'Fragrance Free'
]
],
'badges' => [
[
'label' => 'Good For Oily Hair'
],
[
'label' => 'Clean at Sephora'
]
],
'gtin' => '00614141000036',
'is_active' => true,
'sku' => '2512345',
'source_reference' => '2512345',
'urls' => [
[
'store_url' => 'https://www.sephora.com',
'url' => 'https://www.sephora.com/product/example-P123456'
]
]
]),
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/retailers/api/v1/retailers/{retailer_id}/listings"
payload := strings.NewReader("{\n \"attributes\": {\n \"Category1\": \"Skin Care\",\n \"Category2\": \"Moisturizers\",\n \"highlights\": [\n \"Vegan\",\n \"Fragrance Free\"\n ]\n },\n \"badges\": [\n {\n \"label\": \"Good For Oily Hair\"\n },\n {\n \"label\": \"Clean at Sephora\"\n }\n ],\n \"gtin\": \"00614141000036\",\n \"is_active\": true,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\",\n \"urls\": [\n {\n \"store_url\": \"https://www.sephora.com\",\n \"url\": \"https://www.sephora.com/product/example-P123456\"\n }\n ]\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/retailers/api/v1/retailers/{retailer_id}/listings")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"attributes\": {\n \"Category1\": \"Skin Care\",\n \"Category2\": \"Moisturizers\",\n \"highlights\": [\n \"Vegan\",\n \"Fragrance Free\"\n ]\n },\n \"badges\": [\n {\n \"label\": \"Good For Oily Hair\"\n },\n {\n \"label\": \"Clean at Sephora\"\n }\n ],\n \"gtin\": \"00614141000036\",\n \"is_active\": true,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\",\n \"urls\": [\n {\n \"store_url\": \"https://www.sephora.com\",\n \"url\": \"https://www.sephora.com/product/example-P123456\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings")
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 \"attributes\": {\n \"Category1\": \"Skin Care\",\n \"Category2\": \"Moisturizers\",\n \"highlights\": [\n \"Vegan\",\n \"Fragrance Free\"\n ]\n },\n \"badges\": [\n {\n \"label\": \"Good For Oily Hair\"\n },\n {\n \"label\": \"Clean at Sephora\"\n }\n ],\n \"gtin\": \"00614141000036\",\n \"is_active\": true,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\",\n \"urls\": [\n {\n \"store_url\": \"https://www.sephora.com\",\n \"url\": \"https://www.sephora.com/product/example-P123456\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"committed": true,
"created": 1,
"errors": 0,
"invalid": 0,
"rows": [
{
"badges": 2,
"error": "",
"gtin": "00614141000036",
"index": 0,
"listing_id": "b2c3d4e5-f678-9012-abcd-ef2345678901",
"outcome": "created",
"urls": 1
}
],
"submitted_rows": 2,
"unknown_gtin": 0,
"updated": 1
}{
"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"
}Upsert one retailer product listing
Create or update a single retailer product listing. Identical to the batch endpoint with one row, and returns the same envelope with a single entry in rows.
retailer_id is a retailers.Retailer. An organization-private retailer requires OWNER or MANAGER on the organization that owns it; a canonical (Closient-curated) retailer requires a staff account.
curl --request POST \
--url https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"attributes": {
"Category1": "Skin Care",
"Category2": "Moisturizers",
"highlights": [
"Vegan",
"Fragrance Free"
]
},
"badges": [
{
"label": "Good For Oily Hair"
},
{
"label": "Clean at Sephora"
}
],
"gtin": "00614141000036",
"is_active": true,
"sku": "2512345",
"source_reference": "2512345",
"urls": [
{
"store_url": "https://www.sephora.com",
"url": "https://www.sephora.com/product/example-P123456"
}
]
}
'import requests
url = "https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings"
payload = {
"attributes": {
"Category1": "Skin Care",
"Category2": "Moisturizers",
"highlights": ["Vegan", "Fragrance Free"]
},
"badges": [{ "label": "Good For Oily Hair" }, { "label": "Clean at Sephora" }],
"gtin": "00614141000036",
"is_active": True,
"sku": "2512345",
"source_reference": "2512345",
"urls": [
{
"store_url": "https://www.sephora.com",
"url": "https://www.sephora.com/product/example-P123456"
}
]
}
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({
attributes: {
Category1: 'Skin Care',
Category2: 'Moisturizers',
highlights: ['Vegan', 'Fragrance Free']
},
badges: [{label: 'Good For Oily Hair'}, {label: 'Clean at Sephora'}],
gtin: '00614141000036',
is_active: true,
sku: '2512345',
source_reference: '2512345',
urls: [
{
store_url: 'https://www.sephora.com',
url: 'https://www.sephora.com/product/example-P123456'
}
]
})
};
fetch('https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings', 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/retailers/api/v1/retailers/{retailer_id}/listings",
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([
'attributes' => [
'Category1' => 'Skin Care',
'Category2' => 'Moisturizers',
'highlights' => [
'Vegan',
'Fragrance Free'
]
],
'badges' => [
[
'label' => 'Good For Oily Hair'
],
[
'label' => 'Clean at Sephora'
]
],
'gtin' => '00614141000036',
'is_active' => true,
'sku' => '2512345',
'source_reference' => '2512345',
'urls' => [
[
'store_url' => 'https://www.sephora.com',
'url' => 'https://www.sephora.com/product/example-P123456'
]
]
]),
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/retailers/api/v1/retailers/{retailer_id}/listings"
payload := strings.NewReader("{\n \"attributes\": {\n \"Category1\": \"Skin Care\",\n \"Category2\": \"Moisturizers\",\n \"highlights\": [\n \"Vegan\",\n \"Fragrance Free\"\n ]\n },\n \"badges\": [\n {\n \"label\": \"Good For Oily Hair\"\n },\n {\n \"label\": \"Clean at Sephora\"\n }\n ],\n \"gtin\": \"00614141000036\",\n \"is_active\": true,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\",\n \"urls\": [\n {\n \"store_url\": \"https://www.sephora.com\",\n \"url\": \"https://www.sephora.com/product/example-P123456\"\n }\n ]\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/retailers/api/v1/retailers/{retailer_id}/listings")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"attributes\": {\n \"Category1\": \"Skin Care\",\n \"Category2\": \"Moisturizers\",\n \"highlights\": [\n \"Vegan\",\n \"Fragrance Free\"\n ]\n },\n \"badges\": [\n {\n \"label\": \"Good For Oily Hair\"\n },\n {\n \"label\": \"Clean at Sephora\"\n }\n ],\n \"gtin\": \"00614141000036\",\n \"is_active\": true,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\",\n \"urls\": [\n {\n \"store_url\": \"https://www.sephora.com\",\n \"url\": \"https://www.sephora.com/product/example-P123456\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/retailers/api/v1/retailers/{retailer_id}/listings")
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 \"attributes\": {\n \"Category1\": \"Skin Care\",\n \"Category2\": \"Moisturizers\",\n \"highlights\": [\n \"Vegan\",\n \"Fragrance Free\"\n ]\n },\n \"badges\": [\n {\n \"label\": \"Good For Oily Hair\"\n },\n {\n \"label\": \"Clean at Sephora\"\n }\n ],\n \"gtin\": \"00614141000036\",\n \"is_active\": true,\n \"sku\": \"2512345\",\n \"source_reference\": \"2512345\",\n \"urls\": [\n {\n \"store_url\": \"https://www.sephora.com\",\n \"url\": \"https://www.sephora.com/product/example-P123456\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"committed": true,
"created": 1,
"errors": 0,
"invalid": 0,
"rows": [
{
"badges": 2,
"error": "",
"gtin": "00614141000036",
"index": 0,
"listing_id": "b2c3d4e5-f678-9012-abcd-ef2345678901",
"outcome": "created",
"urls": 1
}
],
"submitted_rows": 2,
"unknown_gtin": 0,
"updated": 1
}{
"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
Path Parameters
Unique identifier of the retailer this listing belongs to.
22^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$Body
A single listing plus the same provenance and dry-run knobs.
GTIN of the product being listed, digits only. Normalised to GTIN-14 before lookup, so an 8, 12 or 13-digit barcode is accepted as sent. The product must already exist — a listing never creates one, so that per-field provenance is only ever written by the import engine.
^\d{8,14}$The retailer's internal SKU for this product.
100Designations the retailer applies to this product. Retailer-scoped and attached to the listing, never to the shared product — a badge is a retailer's marketing claim, not a certification. Repeats of the same derived slug are collapsed rather than rejected, because feeds routinely repeat a badge. Syncing is exact: a badge absent from this payload stops rendering.
Show child attributes
Show child attributes
One product URL per retailer web property, at most one per storefront. An empty list means 'this feed carries no URL information' and deactivates nothing — deliberately not symmetric with a non-empty list, which deactivates the properties it omits. A feed with no URL column (Ulta's PSV) would otherwise switch off every URL another feed supplied.
Show child attributes
Show child attributes
Retailer-scoped facts with no typed column yet, under their original feed names. A bounded staging area, not a document store: at most 200 keys, keys 1-60 characters, string values up to 8000 characters, lists up to 250 items. Exceeding a bound fails the row loudly — the pressure to promote a recurring key to a real column is the point.
False when the retailer has dropped the product.
The retailer's own product identifier in the feed.
255Dataset name recorded for provenance.
100Execute everything and roll back.
Response
OK
Batch census plus the per-row results.
created + updated + unknown_gtin + invalid + errors equals the number
of submitted rows for any 200 response. Per-row failures are reported
here rather than aborting the batch; only request-level failures are 4xx.
False when this was a dry run and the transaction was rolled back.
Number of rows in the request.
x >= 0Rows that produced a new listing.
x >= 0Rows that refreshed an existing listing.
x >= 0Rows whose GTIN has no product. Nothing was written for them.
x >= 0Rows rejected by barcode or storefront validation.
x >= 0Rows that raised.
x >= 0One result per submitted row, in request order.
Show child attributes
Show child attributes