curl --request POST \
--url https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"rows": [
{
"attributes": {
"Category1": "Skin Care"
},
"brand_name": "Example Beauty",
"product": {
"product_name": "Hydrating Face Serum 30ml"
},
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
},
{
"quarantine_detail": "expected 17 columns, got 2",
"quarantine_reason": "malformed_row",
"raw_gtin": "",
"raw_row": {
"_raw": [
"Active",
"SKU9"
]
},
"row_number": 43,
"source_reference": "TRUNCATED-9"
}
]
}
'import requests
url = "https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows"
payload = { "rows": [
{
"attributes": { "Category1": "Skin Care" },
"brand_name": "Example Beauty",
"product": { "product_name": "Hydrating Face Serum 30ml" },
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
},
{
"quarantine_detail": "expected 17 columns, got 2",
"quarantine_reason": "malformed_row",
"raw_gtin": "",
"raw_row": { "_raw": ["Active", "SKU9"] },
"row_number": 43,
"source_reference": "TRUNCATED-9"
}
] }
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({
rows: [
{
attributes: {Category1: 'Skin Care'},
brand_name: 'Example Beauty',
product: {product_name: 'Hydrating Face Serum 30ml'},
raw_gtin: '614141000036',
row_number: 42,
sku: '2512345',
source_reference: '2512345'
},
{
quarantine_detail: 'expected 17 columns, got 2',
quarantine_reason: 'malformed_row',
raw_gtin: '',
raw_row: {_raw: ['Active', 'SKU9']},
row_number: 43,
source_reference: 'TRUNCATED-9'
}
]
})
};
fetch('https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows', 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/import/retailer-catalog/jobs/{job_id}/rows",
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([
'rows' => [
[
'attributes' => [
'Category1' => 'Skin Care'
],
'brand_name' => 'Example Beauty',
'product' => [
'product_name' => 'Hydrating Face Serum 30ml'
],
'raw_gtin' => '614141000036',
'row_number' => 42,
'sku' => '2512345',
'source_reference' => '2512345'
],
[
'quarantine_detail' => 'expected 17 columns, got 2',
'quarantine_reason' => 'malformed_row',
'raw_gtin' => '',
'raw_row' => [
'_raw' => [
'Active',
'SKU9'
]
],
'row_number' => 43,
'source_reference' => 'TRUNCATED-9'
]
]
]),
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/import/retailer-catalog/jobs/{job_id}/rows"
payload := strings.NewReader("{\n \"rows\": [\n {\n \"attributes\": {\n \"Category1\": \"Skin Care\"\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 \"quarantine_detail\": \"expected 17 columns, got 2\",\n \"quarantine_reason\": \"malformed_row\",\n \"raw_gtin\": \"\",\n \"raw_row\": {\n \"_raw\": [\n \"Active\",\n \"SKU9\"\n ]\n },\n \"row_number\": 43,\n \"source_reference\": \"TRUNCATED-9\"\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/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"rows\": [\n {\n \"attributes\": {\n \"Category1\": \"Skin Care\"\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 \"quarantine_detail\": \"expected 17 columns, got 2\",\n \"quarantine_reason\": \"malformed_row\",\n \"raw_gtin\": \"\",\n \"raw_row\": {\n \"_raw\": [\n \"Active\",\n \"SKU9\"\n ]\n },\n \"row_number\": 43,\n \"source_reference\": \"TRUNCATED-9\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows")
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 \"rows\": [\n {\n \"attributes\": {\n \"Category1\": \"Skin Care\"\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 \"quarantine_detail\": \"expected 17 columns, got 2\",\n \"quarantine_reason\": \"malformed_row\",\n \"raw_gtin\": \"\",\n \"raw_row\": {\n \"_raw\": [\n \"Active\",\n \"SKU9\"\n ]\n },\n \"row_number\": 43,\n \"source_reference\": \"TRUNCATED-9\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"chunk_rows": 10000,
"chunks_done": 25,
"chunks_total": 25,
"completed_at": "2026-09-11T07:41:00Z",
"dry_run": true,
"job_id": "b2c3d4e5-f678-9012-abcd-ef2345678901",
"poll_url": "/products/api/v1/import/retailer-catalog/jobs/b2c3d4e5-f678-9012-abcd-ef2345678901",
"processed_rows": 248566,
"reclaim_unattributed": true,
"result": {
"dual_listed": 14300,
"listings_created": 247566,
"products_created": 233241,
"products_enriched": 14300,
"quarantine": [
{
"detail": "net_content is 117 characters, limit 100",
"raw_gtin": "3378872412345",
"reason": "field_too_long",
"row_number": 18342,
"source_reference": "2598765"
}
],
"quarantine_reasons": {
"field_too_long": 1
},
"quarantined": 1,
"rows_read": 248566
},
"source": "ulta_catalog",
"source_file": "ulta_catalog_2025-12.psv",
"staged_rows": 248566,
"started_at": "2026-09-11T07:10:00Z",
"status": "completed"
}{
"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"
}{
"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"
}Stage rows onto a catalog import job
Append a batch of normalised rows to a job that is still staging. Call it repeatedly to stream a whole feed: at most 1,000 rows per call, 400,000 per job.
Rows are staged to storage and consumed lazily by the run, one batch at a time, so a quarter-million-row feed never materialises in memory.
Returns 409 when the job has already started — rows can only be added before the run, and silently ignoring a late batch would under-import a feed while reporting success.
curl --request POST \
--url https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"rows": [
{
"attributes": {
"Category1": "Skin Care"
},
"brand_name": "Example Beauty",
"product": {
"product_name": "Hydrating Face Serum 30ml"
},
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
},
{
"quarantine_detail": "expected 17 columns, got 2",
"quarantine_reason": "malformed_row",
"raw_gtin": "",
"raw_row": {
"_raw": [
"Active",
"SKU9"
]
},
"row_number": 43,
"source_reference": "TRUNCATED-9"
}
]
}
'import requests
url = "https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows"
payload = { "rows": [
{
"attributes": { "Category1": "Skin Care" },
"brand_name": "Example Beauty",
"product": { "product_name": "Hydrating Face Serum 30ml" },
"raw_gtin": "614141000036",
"row_number": 42,
"sku": "2512345",
"source_reference": "2512345"
},
{
"quarantine_detail": "expected 17 columns, got 2",
"quarantine_reason": "malformed_row",
"raw_gtin": "",
"raw_row": { "_raw": ["Active", "SKU9"] },
"row_number": 43,
"source_reference": "TRUNCATED-9"
}
] }
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({
rows: [
{
attributes: {Category1: 'Skin Care'},
brand_name: 'Example Beauty',
product: {product_name: 'Hydrating Face Serum 30ml'},
raw_gtin: '614141000036',
row_number: 42,
sku: '2512345',
source_reference: '2512345'
},
{
quarantine_detail: 'expected 17 columns, got 2',
quarantine_reason: 'malformed_row',
raw_gtin: '',
raw_row: {_raw: ['Active', 'SKU9']},
row_number: 43,
source_reference: 'TRUNCATED-9'
}
]
})
};
fetch('https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows', 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/import/retailer-catalog/jobs/{job_id}/rows",
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([
'rows' => [
[
'attributes' => [
'Category1' => 'Skin Care'
],
'brand_name' => 'Example Beauty',
'product' => [
'product_name' => 'Hydrating Face Serum 30ml'
],
'raw_gtin' => '614141000036',
'row_number' => 42,
'sku' => '2512345',
'source_reference' => '2512345'
],
[
'quarantine_detail' => 'expected 17 columns, got 2',
'quarantine_reason' => 'malformed_row',
'raw_gtin' => '',
'raw_row' => [
'_raw' => [
'Active',
'SKU9'
]
],
'row_number' => 43,
'source_reference' => 'TRUNCATED-9'
]
]
]),
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/import/retailer-catalog/jobs/{job_id}/rows"
payload := strings.NewReader("{\n \"rows\": [\n {\n \"attributes\": {\n \"Category1\": \"Skin Care\"\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 \"quarantine_detail\": \"expected 17 columns, got 2\",\n \"quarantine_reason\": \"malformed_row\",\n \"raw_gtin\": \"\",\n \"raw_row\": {\n \"_raw\": [\n \"Active\",\n \"SKU9\"\n ]\n },\n \"row_number\": 43,\n \"source_reference\": \"TRUNCATED-9\"\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/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"rows\": [\n {\n \"attributes\": {\n \"Category1\": \"Skin Care\"\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 \"quarantine_detail\": \"expected 17 columns, got 2\",\n \"quarantine_reason\": \"malformed_row\",\n \"raw_gtin\": \"\",\n \"raw_row\": {\n \"_raw\": [\n \"Active\",\n \"SKU9\"\n ]\n },\n \"row_number\": 43,\n \"source_reference\": \"TRUNCATED-9\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}/rows")
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 \"rows\": [\n {\n \"attributes\": {\n \"Category1\": \"Skin Care\"\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 \"quarantine_detail\": \"expected 17 columns, got 2\",\n \"quarantine_reason\": \"malformed_row\",\n \"raw_gtin\": \"\",\n \"raw_row\": {\n \"_raw\": [\n \"Active\",\n \"SKU9\"\n ]\n },\n \"row_number\": 43,\n \"source_reference\": \"TRUNCATED-9\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"chunk_rows": 10000,
"chunks_done": 25,
"chunks_total": 25,
"completed_at": "2026-09-11T07:41:00Z",
"dry_run": true,
"job_id": "b2c3d4e5-f678-9012-abcd-ef2345678901",
"poll_url": "/products/api/v1/import/retailer-catalog/jobs/b2c3d4e5-f678-9012-abcd-ef2345678901",
"processed_rows": 248566,
"reclaim_unattributed": true,
"result": {
"dual_listed": 14300,
"listings_created": 247566,
"products_created": 233241,
"products_enriched": 14300,
"quarantine": [
{
"detail": "net_content is 117 characters, limit 100",
"raw_gtin": "3378872412345",
"reason": "field_too_long",
"row_number": 18342,
"source_reference": "2598765"
}
],
"quarantine_reasons": {
"field_too_long": 1
},
"quarantined": 1,
"rows_read": 248566
},
"source": "ulta_catalog",
"source_file": "ulta_catalog_2025-12.psv",
"staged_rows": 248566,
"started_at": "2026-09-11T07:10:00Z",
"status": "completed"
}{
"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"
}{
"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
job_id returned when the job was opened.
22^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$Body
A batch of normalised rows to stage onto an open job.
Rows to stage, at most 1,000 per call. Appends are additive and ordered; a job holds at most 400,000 rows in total. Re-sending a call duplicates its rows in the staged file but not in the result — the engine reports a repeated barcode as duplicate_gtins_in_feed and writes one listing.
1 - 1000 elementsShow child attributes
Show child attributes
Response
OK
A job's current state. The same shape from open, append, start and poll.
Identifier of the job. Use it on the row, start and poll calls.
22^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$staging — accepting rows; the run has not started. queued — start accepted, waiting for a worker. running — the merge is executing; chunks_done and result advance as it goes. completed — finished; result carries the whole feed's census. failed — the run gave up; error says why, result carries the census of the work that did finish, and POST .../start resumes from where it stopped.
Catalog source this run merges under.
Whether this run rolls back each batch.
Whether this run may claim unattributed fields.
Rows accepted into the job so far.
x >= 0Staged rows merged by one chunk task. The run is a sequential chain of these, so no single task approaches the worker's time limit. Set at open time from dry_run unless you named one: 10,000 for a dry run, 2,000 for a commit, because a commit does an order of magnitude more work per row.
x >= 1Chunks this job's staged rows divide into. Zero until rows are staged.
x >= 0Chunks fully merged and recorded. A resumed run continues from here, so these rows are never merged twice.
x >= 0Staged rows already merged. A floor, not an estimate: rows counted here are recorded and survive a restart. It advances within a chunk as well as between chunks — a chunk checkpoints every batch_size rows — so it keeps moving during the minutes a commit chunk takes, and is the field to watch to tell a slow run from a stopped one.
x >= 0Path to poll for this job's status and result.
Feed drop name recorded as listing provenance.
When the run began. Null before it starts.
When the run reached a terminal state.
Why the run failed. Null unless status is failed.
The engine's census. Empty until the first chunk finishes, then the running total for the chunks merged so far, and the whole feed's figures once status is completed — so read status, not this field, to decide whether a run is done. Carries rows_read, products_created / products_enriched / products_unchanged, listings_created / listings_updated, dual_listed (products another retailer's feed had already created — the cross-retailer overlap), duplicate_gtins_in_feed, quarantined with quarantine_reasons, brand and ingredient counts, field_decisions (per field, how each value was decided), reclaimable_fields, and quarantine — the refused rows themselves, capped at 500 entries with quarantined remaining the true total. errors is capped the same way, with errors_total as its true count. census is the same figures as human-readable lines.