Upsert a delta product batch
const url = 'http://localhost:8080/v1/ingest/products';const options = { method: 'PUT', headers: { 'X-Antares-Environment-Fingerprint': 'example', 'Idempotency-Key': 'example', Authorization: 'Bearer <token>', 'Content-Type': 'application/json' }, body: '{"schemaVersion":1,"channel":"example","lang":"example","defaultCurrency":"EUR","currencies":["EUR"],"priceContexts":[{"ctx":"c0193d1ae60d471e7d0712efda7de10115a","label":"example","customerGroupId":"example","ruleIds":["example"],"tierBoundaries":[1]}],"products":[{"id":"example","sku":"example","ean":"example","name":"example","description":"example","url":"example","images":[{"src":"example","thumb280":"example","thumb560":"example","alt":"example"}],"manufacturer":{"id":"example","name":"example","number":"example"},"categories":[{"id":"example","name":"example","path":"example"}],"prices":{"additionalProperty":{"gross":1,"net":1,"list":1}},"priceRange":{"additionalProperty":{"min":1,"max":1}},"purchasePrice":1,"priceContexts":{"additionalProperty":{"prices":{"additionalProperty":{"gross":1,"net":1,"list":1}},"priceRange":{"additionalProperty":{"min":1,"max":1}},"tiers":{"additionalProperty":[{"qty":1,"gross":1,"net":1}]}}},"hasTierPrices":true,"stock":{"available":1,"isCloseout":true},"properties":{"additionalProperty":["example"]},"technicalData":"example","searchKeywords":["example"],"variant":{"count":1,"axes":["example"],"skus":["example"],"eans":["example"]},"flags":{"active":true,"isNew":true,"topseller":true},"releasedAt":"2026-04-15T12:00:00Z","custom":{}}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PUT \ --url http://localhost:8080/v1/ingest/products \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: example' \ --header 'X-Antares-Environment-Fingerprint: example' \ --data '{ "schemaVersion": 1, "channel": "example", "lang": "example", "defaultCurrency": "EUR", "currencies": [ "EUR" ], "priceContexts": [ { "ctx": "c0193d1ae60d471e7d0712efda7de10115a", "label": "example", "customerGroupId": "example", "ruleIds": [ "example" ], "tierBoundaries": [ 1 ] } ], "products": [ { "id": "example", "sku": "example", "ean": "example", "name": "example", "description": "example", "url": "example", "images": [ { "src": "example", "thumb280": "example", "thumb560": "example", "alt": "example" } ], "manufacturer": { "id": "example", "name": "example", "number": "example" }, "categories": [ { "id": "example", "name": "example", "path": "example" } ], "prices": { "additionalProperty": { "gross": 1, "net": 1, "list": 1 } }, "priceRange": { "additionalProperty": { "min": 1, "max": 1 } }, "purchasePrice": 1, "priceContexts": { "additionalProperty": { "prices": { "additionalProperty": { "gross": 1, "net": 1, "list": 1 } }, "priceRange": { "additionalProperty": { "min": 1, "max": 1 } }, "tiers": { "additionalProperty": [ { "qty": 1, "gross": 1, "net": 1 } ] } } }, "hasTierPrices": true, "stock": { "available": 1, "isCloseout": true }, "properties": { "additionalProperty": [ "example" ] }, "technicalData": "example", "searchKeywords": [ "example" ], "variant": { "count": 1, "axes": [ "example" ], "skus": [ "example" ], "eans": [ "example" ] }, "flags": { "active": true, "isNew": true, "topseller": true }, "releasedAt": "2026-04-15T12:00:00Z", "custom": {} } ] }'Delta upsert against the live alias. While a full run is open for the same (channel, lang) the same documents are additionally written to the shadow collection, which is what makes a full rebuild lossless. No batch commits partially: one failing product fails the whole request with one errors[] entry per product.
Authorizations
Abschnitt betitelt „Authorizations“Parameters
Abschnitt betitelt „Parameters“Header Parameters
Abschnitt betitelt „Header Parameters“SHA-256 over the shop’s APP_URL plus its platform version. APP_URL lives in .env and is not carried by a database dump, which is exactly why it is the discriminator between a production install and a clone of it.
Caller-generated key. Replaying the same key with the same body replays the stored response; replaying it with a different body is 409 idempotency_conflict.
Request Bodyrequired
Abschnitt betitelt „Request Bodyrequired“object
The Antares channel key, for example b2c-de. Must resolve to a (channel, lang) collection for the authenticated shop.
The channel’s own currency. Must equal the channel’s stored default currency; a mismatch is 422 validation_failed. Margin, the widget’s fallback display and the default sort field are all expressed in it, which is why a product without a price in it is rejected rather than indexed at zero.
Every currency the sales channel assigns, defaultCurrency first. The connector is the source of truth here: the API reconciles the channel’s stored currency assignment to this list on every envelope, so assigning a currency in the shop is enough to have it indexed.
The registry of the channel’s price contexts — not the batch’s. It may be sent unchanged on every batch; the API upserts it idempotently. A product may only reference a context present here or already registered for the channel. A context that no envelope has mentioned for 30 days and that no document carries is pruned.
One entry of the channel’s price-context registry, carrying the structured input of the fingerprint rather than the canonical string. The API rebuilds the canonical string from these fields, hashes it, and compares the result to ctx; a mismatch fails the whole batch with price_context_fingerprint_mismatch. That comparison is the only mechanism that catches a PHP↔Go disagreement on the day it appears instead of six months later in a support ticket about wrong prices.
object
The fingerprint the connector computed. The literal default must not appear here: the default context is implicit and always exists.
Merchant-facing description of the situation, for example Händler B2B · Preisliste 2026. Panel only, never reaches the widget, and deliberately not part of the fingerprint so renaming a price list does not orphan every materialised price field.
The raw platform customer-group id, as the platform spells it. Omit it when the platform has no customer groups. The API normalises it (trim, ASCII-only case fold, remove every -) before hashing, so the dashed and undashed spellings of the same UUID are the same context.
The raw platform ids of the price rules that matched for this situation, in any order. The API normalises, deduplicates and sorts them, so the platform’s priority ordering — which is meaningless for identity — never reaches the fingerprint.
Quantity thresholds contributed by this context’s own quantity-conditioned price rules, which make the matching rule set itself quantity-dependent. The value 1 is dropped because quantity 1 is always implied. Not the per-product price ladder: that is per (product, rule), varies from product to product, and lives in PriceContext.tiers as a value inside the context.
The neutral product schema. The platform ends at this boundary; nothing downstream ever sees a Shopware concept. Server-derived fields (margin_pct, margin_norm, boost_conv, boost_sales, weighted_score, embedding, price_val_*, price_sort_*, pc_*) must never be sent and are rejected by additionalProperties: false.
object
Stable platform id. The document id in the index.
Plain text, already stripped of markup by the connector.
SEO path relative to the channel domain, per (channel, lang).
object
280 px thumbnail, used by the instant dropdown.
560 px thumbnail, used by the result grid.
object
Manufacturer part number. Indexed for infix matching.
object
Breadcrumb path, >-separated, used as the facet’s display value.
Default-context price per ISO currency. Every currency the sales channel assigns and the product actually prices is carried, so the storefront can switch currency without a reindex. A missing defaultCurrency entry rejects the product; a missing secondary currency simply drops the product from that currency’s results. A currency the channel does not assign is 422 currency_not_indexed.
object
object
Strike-through reference price. Omit when the product is not on sale.
The variant “from–to” gross span per currency. Every currency present in prices must have an entry, because the API indexes one facet range per priced currency and a price without its range would silently vanish from the price facet. For a product without variants min == max == gross.
object
object
Net purchase price in the shop’s house currency. The only margin input the connector supplies. Stored unindexed, used to derive marginNorm, and never present in any response to a public credential.
Materialised customer prices keyed by price-context fingerprint. The literal default is reserved and rejected here — the default prices are prices and priceRange.
Only contexts that deviate from the default are sent. A context whose prices equal the default in every currency must be omitted; this is the sizing rule the whole feature depends on, because in a normal catalogue almost every product costs the same for almost every context and omitting them is what keeps documents inside the per-tenant RAM budget. At most 64 contexts and at most 128 (context × currency) families per product, or the product is rejected with price_context_budget_exceeded.
The API returns at most the single context a request proves entitlement to, and never the map itself.
object
One context’s deviating prices for one product. It may cover a subset of the product’s currencies; a currency absent here simply has no context price and falls back to the default for that currency. Pricing a currency the product’s default context does not price rejects the product, because such a price has nothing to fall back to and would leak through the default field family.
object
Prices per upper-case ISO 4217 currency.
object
object
Strike-through reference price. Omit when the product is not on sale.
Defaults to the product’s own priceRange when omitted.
object
object
Quantity ladders per upper-case ISO currency, ascending by qty, at most 32 rungs. These are the per-product ladders and are values inside the context, never part of its identity.
object
Quantity-scaled price. qty is the lowest quantity at which the price applies.
object
Display hint for the widget’s tier_prices badge, derived by the connector from “any context carries tiers for this product”.
object
Units on hand. Thresholded into in_stock/low/out before serving.
The product cannot be ordered beyond the available quantity, so zero stock means unavailable rather than backorderable.
The shop’s own property axes, keyed by normalised name, for example {"farbe": ["blau"]}. Each becomes a properties.<name> facet.
object
Flattened technical specification text, indexed for infix matching.
Merchant-maintained additional search terms.
Parent-level strategy: one document per parent, carrying the identifiers and axes of all its variants so a variant SKU still finds the parent. Channels that need one document per variant set indexVariantsSeparately.
object
object
false is not a validation failure: the id is moved to the delete list of the same request and removed from the live and shadow collections.
Release date, the input to the novelty ranking signal.
Connector-specific passthrough. Indexed as-is and never interpreted.
object
Responses
Abschnitt betitelt „Responses“Batch validated and upserted.
object
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "accepted": 1, "rejected": 1, "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}contract_validation_failed — the OpenAPI request validator rejected the request before the handler ran — or invalid_request when the body is unreadable or is not valid JSON for the declared operation.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}unauthorized — the credential is missing, malformed, unknown or revoked. For /v1/admin this also covers an actor JWS that fails signature, exp or kid verification, and a kid that no key in the panel’s JWKS matches.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}quota_exceeded — a hard cap was reached (sandbox plan, or a trial past its grace window); paid plans get the soft cap instead, which is 200 plus X-Antares-Quota: exceeded. Or plan_feature_unavailable — the organization’s resolved entitlements do not include the requested feature.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}One of index_run_active (a full run is already open for this channel and language), index_run_closed (batch or commit against a committed or aborted run), batch_out_of_order (batchIndex is not lastBatchIndex + 1 and not an exact replay), batch_conflict (the batch could not be recorded idempotently), idempotency_conflict (the same Idempotency-Key with a different body) or environment_mismatch.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}request_too_large — the body exceeds this endpoint’s byte limit. Also returned for chunked bodies without Content-Length once the limit is passed mid-stream.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}One of validation_failed (envelope-level failure, or one or more products failed; errors[] carries productIndex and no partial batch is committed), channel_not_found (the (channel, lang) pair is not mapped for this shop), currency_not_indexed (a product prices a currency the channel does not assign), price_context_fingerprint_mismatch (the API recomputed the fingerprint from the registry entry’s structured input and got a different value — the PHP↔Go drift alarm, which fails the whole batch), price_context_budget_exceeded (more than 64 contexts or more than 128 context × currency families on one product) or document_count_sanity_failed (a commit would drop the live document count below 50 % of the previous run; the old collection stays live).
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}One of rate_limit_exceeded, authentication_admission_full (the pre-auth admission guard for large connector bodies), connector_admission_full (this sk already occupies its single large-body slot) or request_admission_full (global large-body validation slots exhausted). All three admission codes are retryable after a short backoff.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}Headers
Abschnitt betitelt „Headers“Seconds to wait before retrying.
internal_error — unhandled failure. detail is always generic; the cause goes to the log with a correlation id.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}search_backend_unavailable — Typesense failed an ingest, alias, synonym or collection operation.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}One of idempotency_unavailable (the idempotency advisory lock could not be taken), index_lock_unavailable (the per-channel advisory lock could not be taken), analytics_unavailable (ClickHouse rejected a synchronous, business-critical write) or service_unavailable. All are retryable.
RFC 9457 problem details. type is always https://api.antares.commergy.de/problems/{code} and is built in exactly one place. type, code and title are stable and never localised; only detail is localised, and only where the registry marks the audience as merchant.
object
Absolute problem type URI. Stable identifier, never localised.
Stable English summary of the problem type.
The HTTP status code, repeated in the body.
Human-readable explanation of this occurrence. The only localised field; may be German or English depending on Accept-Language.
The request path this occurrence relates to.
The registry code, for example price_context_token_expired. This is what a client branches on; never branch on title or detail.
Present only for per-item validation failures, above all ingest batches.
object
Dotted path of the offending field inside the request body, for example prices.CHF.gross.
What is wrong with it, in English.
0-based index into the request’s products array. Present only for ingest batches, and the only way to map an error back to a product without echoing it.
Examplegenerated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "code": "example", "errors": [ { "field": "example", "message": "example", "productIndex": 1 } ]}