curl --request GET \
--url https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id} \
--header 'X-API-Key: <api-key>'import requests
url = "https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}', 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}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"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"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
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"
}Poll a catalog import job
Return the job’s status and, once it completes, the engine’s full census: products created and enriched, listings created and updated, the cross-retailer overlap, per-field decision counts, brand and ingredient figures, and the quarantined rows with the reason each was refused.
On a dry run the quarantined rows come from the run’s in-memory record, because the quarantine table rolls back with each batch; on a committed run they are read back from the table and additionally carry attempt_count and resolved.
Polling a job belonging to another organization returns 404 rather than 403, so job ids cannot be enumerated by status code.
curl --request GET \
--url https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id} \
--header 'X-API-Key: <api-key>'import requests
url = "https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}', 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}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"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"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/products/api/v1/import/retailer-catalog/jobs/{job_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
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"
}Authorizations
Path Parameters
job_id returned when the job was opened.
22^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$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.