Skip to main content
POST
Open a retailer catalog import job

Authorizations

X-API-Key
string
header
required

Body

application/json

Flags for one catalog import run.

source
enum<string>
required

Catalog source this run merges under. One of: sephora_catalog, ulta_catalog. Not free text — the source decides the canonical retailer, its storefront, and the measured per-field confidence table that resolves every contested field, so an unregistered value is rejected with 422 rather than guessed at.

Available options:
ulta_catalog,
sephora_catalog
source_file
string
default:""

Name of the feed drop these rows came from. Recorded as listing provenance, so a listing can be traced to the file it came from.

Maximum string length: 255
Pattern: ^[^\x00]{0,255}$
fetched_at
string<date-time> | null

When the feed drop was fetched — provenance about the data, not the run. Send the drop's own timestamp; omitting it stamps the run's start time, which misdates a feed imported weeks after it was published.

dry_run
boolean
default:true

Run the real merge and roll back each batch. Default. The counts are directly comparable with a committed run's, so this is a real rehearsal rather than an estimate. Note that quarantine rows roll back too and are reported from the run's in-memory record instead.

reclaim_unattributed
boolean
default:true

Allow this run to claim fields on products that carry no recorded provenance — rows from earlier importers, which overwrote rather than enriched and left no field_sources. Restricted to products still marked imported/seeded, so a brand-claimed product or one sourced from a crawl is never touched. On by default ; a dry run reports exactly how many fields it would claim, and the flag can still be set to false for a specific run.

skip_ingredients
boolean
default:false

Skip ingredient-list parsing, the slowest part of a run.

batch_size
integer
default:500

Rows per transaction window inside the engine. Each window costs a fixed handful of queries regardless of size, so the default rarely needs changing. It is also the unit a run resumes from after an interruption, so a smaller window loses less work and costs more checkpoint writes.

Required range: 1 <= x <= 5000
chunk_rows
integer | null

Staged rows merged by one background chunk task. The run is a sequential chain of these. Omit it and the size is chosen from dry_run: 10,000 for a dry run, 2,000 for a commit. The two modes are not the same amount of work per row — a commit writes every row and parses every ingredient label inline, measured at 14-25 rows/s against a dry run's ~103 — and sizing both from the dry run is what killed the first full-feed commit run. Send a value only if you have measured your own throughput; the maximum is 12,000.

Required range: 1 <= x <= 12000

Response

OK

A job's current state. The same shape from open, append, start and poll.

job_id
string<shortuuid>
required

Identifier of the job. Use it on the row, start and poll calls.

Required string length: 22
Pattern: ^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$
status
string
required

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.

source
string
required

Catalog source this run merges under.

dry_run
boolean
required

Whether this run rolls back each batch.

reclaim_unattributed
boolean
required

Whether this run may claim unattributed fields.

staged_rows
integer
required

Rows accepted into the job so far.

Required range: x >= 0
chunk_rows
integer
required

Staged 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.

Required range: x >= 1
chunks_total
integer
required

Chunks this job's staged rows divide into. Zero until rows are staged.

Required range: x >= 0
chunks_done
integer
required

Chunks fully merged and recorded. A resumed run continues from here, so these rows are never merged twice.

Required range: x >= 0
processed_rows
integer
required

Staged 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.

Required range: x >= 0
poll_url
string
required

Path to poll for this job's status and result.

source_file
string
default:""

Feed drop name recorded as listing provenance.

started_at
string<date-time> | null

When the run began. Null before it starts.

completed_at
string<date-time> | null

When the run reached a terminal state.

error
string | null

Why the run failed. Null unless status is failed.

result
Result · object

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.