POST /push/v1/{merchant}/ingest
merchant is your Depict merchant id. The request body is JSONL. Each line is
one JSON object carrying one operation. The endpoint processes lines in order
and responds with JSONL too, one line per input line, in the same order.
Authentication
401. The response does not distinguish an
unknown merchant from a wrong key.
Your Depict contact issues this key during the beta. It is specific to
this API, not the credentials Depict’s storefront or portal APIs use. It
authenticates a server-to-server write API, so keep it secret and send it
only from your backend, never from a browser.
Query parameters
Parameters parse strictly. Any value other than the ones listed returns
400
rather than defaulting to off, so a misspelled preview flag can never run a
real ingest.
Request-level errors
Request-level failures return a JSON error instead of receipts:401, 400 and 405 refuse the request before any line is processed. The
body caps can also trip mid-stream, because the endpoint processes lines as
they arrive. Earlier lines are then already applied, and the 413 body names
how many lines were processed.
The endpoint reports everything else per line, with HTTP status 200. An
invalid line gets a failure receipt and processing continues with the next
line.
Dry run
Add?dry_run=true to validate a request without changing anything. The
endpoint runs the same per-line validation, returns the same receipt
contract, and marks the response with an X-Push-Dry-Run: true header. It
writes nothing and starts no processing. Authentication is still required.
With mode=full_sync, a dry run also previews the sweep. The response
carries X-Push-Full-Sync-Would-Sweep with the number of products a real
request would remove, and the guardrail verdicts appear in
X-Push-Full-Sync-Sweep-Skipped exactly as they would for real. Nothing is
deleted.
The endpoint evaluates receipts against the currently stored state, so a dry
run is a statement about now, not a reservation:
success: truemeans the line passes validation now. A later real push can still fail on state that changed in between, for example aconflictfrom concurrent edits.- The stateful outcomes
unchanged,stale_source_updated_atandalready_absentcompare against what is stored at dry-run time. - A variant line whose parent product exists only earlier in the same request
validates against that in-request tree and never reports
unchanged, because there is no stored variant to compare with.
Machine-readable schema
The API serves the full JSONL line contract as a JSON Schema (draft 2020-12):$id names the deployed version it
belongs to, and responses carry an ETag for caching. Use it to generate
types for your client or to validate lines before sending them.
Passing the schema is necessary, not sufficient. The API enforces some
rules that JSON Schema cannot express: no leading or trailing whitespace or
control characters in ids, no
: in pricelist ids and market names,
duplicate-id rejection, and references between lines. The per-line receipts
are the authority.The line envelope
Every line is an object with the same three fields:"upsert" | "delete"
required
What to do with the object.
string
required
For
upsert: product, variant, pricelist, warehouse, market or
category. For delete: product, product_group or variant.object
required
The payload, shaped by
type. See below.External ids
Every object is identified byexternal_id, your identifier, 1–255
characters, with no leading or trailing whitespace and no control characters.
An id only has to be unique within its parent. Ids are contractual. Depict
derives its internal ids from yours, so changing an external_id is a delete
plus a create.
Pricelist ids, market ids and market names additionally must not contain :,
because they become part of field names in Depict’s search index.
References between lines
A product’spricelist_external_id, warehouse_external_id and category_ids
references, and a market’s pricelist and warehouse references, must resolve to
an entity that already exists, pushed in an earlier request or on an earlier
line of the same request. Put configuration lines before the products that use
them. An unknown reference fails the line with validation_error and an error
naming the exact path.
Upsert: product
{"operation":"upsert","type":"product","object":{...}} carries the whole
product tree. An upsert replaces the stored tree. The endpoint removes product
groups and variants absent from the pushed tree.
string
required
Product id, unique across your catalog.
"active" | "draft" | "archived"
default:"active"
Only
active products are shown.string
Product type, for example
T-shirts. 1–255 characters.string
Brand name. 1–255 characters.
string[]
default:"[]"
Up to 250 tags, each 1–255 characters.
string
ISO 8601 timestamp with timezone (
2026-08-26T09:12:00Z or with a numeric
offset). When present, Depict applies last-write-wins. A pushed product older
than the stored one is skipped with stale_source_updated_at, which makes
replays and out-of-order delivery safe.Media[]
default:"[]"
Up to 250 media objects, described under Media.
external_ids
must be unique within the product.ProductGroup[]
required
1–500 product groups, in display order.
external_ids must be unique within
the product.Product group
string
required
Group id, unique within the product.
"active" | "draft" | "archived"
default:"active"
string[]
default:"[]"
Up to 100 category
external_ids. Each must reference a pushed category.Variant[]
required
1–1000 variants, in display order.
external_ids must be unique within the
group.string[]
Which of the product’s media this group shows, by media
external_id, up to
100. Omitted means all of the product’s media. Ids not present in the
product’s media fail the line.Variant
string
required
Variant id, unique within the product group.
string
1–255 characters.
string
Display size, for example
M. 1–255 characters.string
1–64 characters.
boolean
default:"true"
Inactive variants are kept but not sellable.
Price[]
default:"[]"
Up to 100 prices, at most one per pricelist:
pricelist_external_id(string, required). Must reference a pushed pricelist.price(number, required). The current price, ≥ 0, in the pricelist’s currency.compare_at_price(number). The strikethrough price, ≥ 0. Depict derives on-sale flags and discounts from the two.
InventoryLevel[]
default:"[]"
Up to 100 levels, at most one per warehouse:
warehouse_external_id(string, required). Must reference a pushed warehouse.sellable_quantity(integer, required). Must be ≥ 0. Oversold stock is yours to clamp. The endpoint rejects negative values rather than folding them to zero. Prefer real quantities over booleans, since they power signals like low stock. If your source only knows in or out of stock, send1and0.
{ [locale]: LocaleContent }
default:"{}"
Keyed by locale code, lowercase with an underscore before an optional
region:
sv, en, sv_se. Depict stores codes verbatim, so sv and
sv_se are two different locales that never merge. Each value can carry:title(string, 1–4096)description(string, ≤ 65536)short_description(string, ≤ 65536)slug(string, 1–2048)seo_title(string, ≤ 4096)seo_description(string, ≤ 65536)
{ [key]: string[] }
default:"{}"
Locale-invariant keys (1–255 characters) mapping to 1–100 values (each
1–8192 characters). Display labels come from your attribute configuration.
string[]
Which of the product’s media this variant shows, up to 100. Omitted means
all of the product’s media.
Media
string
required
Media id, unique within the product.
"image" | "video" | "external_video" | "model_3d"
required
string
≤ 4096 characters.
string
Poster image URL for video media. ≤ 4096 characters.
MediaSource[]
required
1–20 sources, renditions of the same media, in order:
url(string, required). ≤ 4096 characters.format(string). For examplejpg.mime_type(string). For exampleimage/jpeg.width,height(positive integers).
Upsert: variant
{"operation":"upsert","type":"variant","object":{...}} patches exactly one
variant into the stored product tree without resending the tree. The object
is a full variant plus two addressing fields:
string
required
The variant’s product. It must already exist. An unknown product fails the
line with
validation_error. The endpoint never creates parents implicitly.string
required
The variant’s product group under that product. It must already exist.
- If the addressed group has a variant with this
external_id, the pushed variant replaces it. Otherwise the endpoint appends the pushed variant to the group. The unit is the whole variant object. There is no field-level patching. - The endpoint validates
media_external_idsagainst the stored product’s media. - A variant identical to what is stored returns
"skipped": "unchanged". - The product’s
source_updated_atis untouched. Last-write-wins ordering applies to whole-product upserts only. - If concurrent pushes edit the same product while the patch applies, the line
fails with
conflict. It is safe to retry.
Upsert: catalog configuration
These entities must exist before the first product or market that references them, or earlier in the same request.Pricelist
{"operation":"upsert","type":"pricelist","object":{...}}
string
required
Must not contain
:.string
required
ISO 4217 code, uppercase, like
"SEK".boolean
required
Whether prices on this pricelist include tax.
Warehouse
{"operation":"upsert","type":"warehouse","object":{...}}
string
required
string
Display name. 1–1024 characters.
Market
{"operation":"upsert","type":"market","object":{...}}
string
required
Must not contain
:.string
required
Display name, 1–1024 characters. Must not contain
:.string
required
The one pricelist serving this market. Must reference a pushed pricelist.
string[]
required
1–100 warehouse ids, in priority order. Earlier warehouses win when
computing availability. Each must reference a pushed warehouse.
Category
{"operation":"upsert","type":"category","object":{...}}
string
required
string
The parent category, for building the tree.
{ [locale]: CategoryLocaleContent }
default:"{}"
Keyed by locale code, same rules as variant locale content. Each value:
name(string, 1–1024, required)slug(string, 1–2048)description(string, ≤ 65536)
Delete
Deletes address any of the three levels by theexternal_id path from the
product down. Deletes are explicit and idempotent. Omission from an upsert of
a different product never deletes anything, and deleting something already
gone succeeds with "skipped": "already_absent".
Per-line response
The response hasContent-Type: application/x-ndjson, and line N answers
the Nth non-empty line of the request. Blank lines carry no receipt. The
response is the receipt. There are no job ids and no status endpoint.
boolean
required
Whether the line was accepted.
string
Machine-readable failure class. Present only when
success is false:invalid_json. The line is not valid JSON.validation_error. The line failed schema validation or references an unknown pricelist, warehouse or category.conflict. Concurrent pushes edited the same product while the endpoint applied this line’s variant upsert or sub-product delete. Safe to retry.internal_error. Something failed on Depict’s side. Retry, and tell us if it persists.
string
Human-readable path and reason. Present only when
success is false, for
example
product_groups[0].variants[0].prices[0]: unknown pricelist_external_id "nok".string
Present when the line was accepted but changed nothing:
unchanged. The payload is identical to what is stored.stale_source_updated_at. The stored product has a newersource_updated_at, so last-write-wins kept it.already_absent. A delete of something already gone.
Receipts currently arrive after the endpoint has processed the whole
request, not streamed line by line. Your client can still stream the upload
and read the full response at the end.
Full sync response headers
With?mode=full_sync, response headers report the outcome of the
absent-means-deleted sweep:

