Import
The import pipeline ingests tenant catalogs into the PIM through three complementary channels:
- File upload — drop a CSV or Excel file, parsed by the import worker.
- Direct API — POST a JSON array and get results back in the same request.
- Queued API — POST a JSON array and let a background worker apply it.
All three reuse the same ImportSource (field mappings, locked fields,
auto-publish rules) and produce an ImportJob you can poll through the
Jobs API.
Authentication
Every endpoint below requires tenant authentication via
requireTenantAuth. Three credentials are accepted:
| Field | Type | Required | Description |
|---|---|---|---|
Bearer token | header | Optional | Authorization: Bearer <jwt> — SSO access token or portal-user token. |
API key | header | Optional | x-auth-method: api-key + x-api-key-id: ak_... + x-api-secret: sk_... |
Session cookie | cookie | Optional | vinc_b2b_session — set by the B2B portal login flow. |
Endpoints
/api/b2b/pim/importauth: session/api/b2b/pim/importauth: session/api/b2b/pim/import/analyzeauth: session/api/b2b/pim/import/apiauth: session/api/b2b/pim/import/queueauth: session/api/b2b/pim/import/queueauth: sessionFlow overview
- Create a source once per feed via POST /api/b2b/pim/sources.
The source stores
field_mappings(source → target) and auto-publish rules. - Optionally analyze a sample file with
POST /import/analyzeto inspect headers and infer types before committing to a mapping. - Send data through one of:
POST /import— multipart file upload, CDN-backed, background worker.POST /import/api— synchronous JSON upsert, returns full results.POST /import/queue— asynchronous JSON upsert, returns ajob_id.
- Poll status via GET /api/b2b/pim/jobs/:jobId for counters and per-row errors.
Analyze a file
/api/b2b/pim/import/analyzeauth: sessionParses the first rows of a CSV or Excel upload and returns inferred column
metadata. Use this before building a field mapping on the ImportSource.
Constraints
- Max 50 MB per file.
- Extensions:
.csv,.xlsx,.xls. - CSV is parsed with
relax_quotesandrelax_column_count; Excel reads the first sheet only.
Request
Multipart form-data with a single field:
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Required | The CSV or Excel file to analyze. |
Response
| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | Optional | Uploaded file name. |
fileSize | integer | Optional | Byte size. |
fileType | 'csv' | 'xlsx' | 'xls' | Optional | Detected extension. |
totalRows | integer | Optional | Rows read from the preview window (capped at 15). |
columns[] | ColumnAnalysis | Optional | Per-column stats: name, type, sampleValues, totalValues, uniqueCount, emptyCount. |
previewRows[] | object | Optional | First 10 rows as raw objects (header → value). |
Example
curl -X POST https://your-tenant.example.com/api/b2b/pim/import/analyze \
-H "x-auth-method: api-key" \
-H "x-api-key-id: ak_acme_live_1234" \
-H "x-api-secret: sk_live_abcdef..." \
-F "file=@products.csv"{
"fileName": "products.csv",
"fileSize": 18423,
"fileType": "csv",
"totalRows": 15,
"columns": [
{
"name": "sku",
"type": "string",
"sampleValues": ["SKU-0001", "SKU-0002", "SKU-0003"],
"totalValues": 15,
"uniqueCount": 15,
"emptyCount": 0
},
{
"name": "price",
"type": "number",
"sampleValues": ["12.5", "8.9", "42"],
"totalValues": 14,
"uniqueCount": 14,
"emptyCount": 1
}
],
"previewRows": [
{ "sku": "SKU-0001", "name": "Brass valve", "price": "12.5" },
{ "sku": "SKU-0002", "name": "Copper elbow", "price": "8.9" }
]
}CSV errors
The analyzer returns user-friendly hints for common CSV problems (inconsistent column counts, unescaped quotes):
{
"error": "CSV Format Error",
"details": "Your CSV file has inconsistent columns at line 42. Some rows have different numbers of columns than the header row. Please check for unescaped quotes (\") or extra commas in your data.",
"hint": "Common fixes: Remove special characters from field values, properly quote fields containing commas, or use Excel format instead.",
"technicalDetails": { "code": "CSV_INCONSISTENT_RECORD_LENGTH", "line": 42, "column": 7 }
}Upload a file
/api/b2b/pim/importauth: sessionStreams a CSV or Excel file to CDN storage, creates an ImportJob, and
enqueues it for background processing. Poll the returned job for status.
Constraints
- Max 50 MB per file.
- Extensions:
.csv,.xlsx,.xls. - The tenant must have CDN configured (see CDN config). A 500 is returned otherwise.
Request
Multipart form-data:
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Required | CSV or Excel file with product rows. |
source_id | string | Required | Existing ImportSource to apply (provides field mappings and rules). |
Response
Returns 202 Accepted with the freshly created job.
| Field | Type | Required | Description |
|---|---|---|---|
job.job_id | string | Optional | Correlation id, prefix 'import_'. |
job.source_id | string | Optional | Echoes the ImportSource applied. |
job.file_name | string | Optional | Uploaded file name. |
job.file_size | integer | Optional | Byte size. |
job.file_url | string | Optional | CDN URL of the stored file. |
job.status | 'pending' | Optional | Worker transitions to 'processing' then 'completed' or 'failed'. |
Example
curl -X POST https://your-tenant.example.com/api/b2b/pim/import \
-H "x-auth-method: api-key" \
-H "x-api-key-id: ak_acme_live_1234" \
-H "x-api-secret: sk_live_abcdef..." \
-F "file=@products.xlsx" \
-F "source_id=supplier-weekly-feed"{
"job": {
"job_id": "import_1714142335123_a4f9k2",
"source_id": "supplier-weekly-feed",
"file_name": "products.xlsx",
"file_size": 842517,
"file_url": "https://cdn.vendereincloud.com/vinc-acme/pim-imports/products.xlsx",
"status": "pending",
"created_at": "2026-04-24T12:58:55.123Z"
}
}Poll the job's progress at GET /api/b2b/pim/jobs/:jobId.
Direct JSON import (synchronous)
/api/b2b/pim/import/apiauth: sessionApplies an array of products inline in a single request and returns the full result set. Ideal for ≤ 1 000 products per call; the hard limit is 10 000.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
source_id | string | Required | ImportSource whose field_mappings and auto-publish rules are applied. |
products | Product[] | Required | Array of product payloads. 1 ≤ length ≤ 10000. |
merge_mode | 'replace' | 'partial' | Optional | 'replace' (default) overwrites the product version entirely; 'partial' deep-merges incoming fields onto the existing version.(default: 'replace') |
batch_id | string | Optional | Optional correlation id persisted on each product's source.batch_id. |
batch_metadata | object | Optional | Optional { batch_id, batch_part, batch_total_parts, batch_total_items } — stored on the product for downstream aggregation. |
channel_metadata | object | Optional | Accepted and echoed back in the debug block. Reserved for future routing; currently unused by the pipeline. |
Per-product fields
Each entry in products is mapped through source.field_mappings first,
then the mapped object is persisted. entity_code (or sku) is
required and identifies the product across versions.
| Field | Type | Required | Description |
|---|---|---|---|
entity_code | string | Required | Stable tenant-scoped id. Falls back to `sku` if omitted. |
sku | string | Optional | Defaults to entity_code if omitted. |
name | string | MultiLangString | Optional | Plain strings are auto-wrapped as { <defaultLang>: value }. |
description | string | MultiLangString | Optional | Same auto-wrapping for multilingual fields. |
price | number | Optional | Numeric price; per-channel overrides handled by the PIM. |
images | MediaRef[] | Optional | Replaced wholesale when provided. Kept when omitted in partial mode. |
media | MediaRef[] | Optional | Merged by type in partial mode: incoming types replace existing items of that type; other types are preserved. |
Processing rules
- Versioning — every successful upsert creates a new
PIMProductversion. The priorisCurrent: truerow is demoted and itslocked_fieldsare re-applied to the incoming payload. - Multilingual auto-wrap — multilingual fields (e.g.
name,description,slug) are wrapped as{ <defaultLang>: value }unless they already look like a language-keyed object (2-letter codes matching a language configured for the tenant). - Auto-publish — a version with a sufficient completeness score is
committed as
publishedwithpublished_atset. Otherwise it lands asdraft. - Search sync — when the tenant has a search-enabled default language, successful entity codes are batched (50 per batch) and queued for indexing into the tenant's search surface.
Response
| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Optional | true even when some rows failed — check summary.failed. |
job_id | string | Optional | ImportJob id, prefix 'api_import_'. Persisted for audit. |
summary.total | integer | Optional | Rows attempted. |
summary.successful | integer | Optional | Rows persisted. |
summary.failed | integer | Optional | Rows rejected — see errors[]. |
summary.auto_published | integer | Optional | Rows that met auto-publish criteria. |
summary.duration_seconds | number | Optional | Server-side processing time. |
summary.sync_batches_queued | integer | Optional | Number of search-sync batches queued (50 products per batch). |
errors[] | { row, entity_code, error, raw_data } | Optional | First 100 failures, with the raw row that caused them. |
debug | object | Optional | Echo of batch_id, batch_metadata, channel_metadata, and the first product's source block for inspection. |
Example
curl -X POST https://your-tenant.example.com/api/b2b/pim/import/api \
-H "Content-Type: application/json" \
-H "x-auth-method: api-key" \
-H "x-api-key-id: ak_acme_live_1234" \
-H "x-api-secret: sk_live_abcdef..." \
-d '{
"source_id": "supplier-weekly-feed",
"merge_mode": "partial",
"batch_id": "weekly-2026-17",
"products": [
{
"entity_code": "SKU-0001",
"sku": "SKU-0001",
"name": { "en": "Brass ball valve 1/2\"", "it": "Valvola a sfera 1/2\"" },
"description": "Full-bore brass ball valve, threaded.",
"price": 12.5,
"brand": "acme",
"images": [
{ "url": "https://cdn.example.com/sku-0001-front.jpg", "position": 0 }
]
},
{
"entity_code": "SKU-0002",
"name": "Copper elbow 22mm",
"price": 3.9,
"brand": "acme"
}
]
}'{
"success": true,
"job_id": "api_import_1714142335123_b7x2m1",
"summary": {
"total": 2,
"successful": 2,
"failed": 0,
"auto_published": 1,
"duration_seconds": 0.42,
"sync_batches_queued": 1
},
"errors": [],
"debug": {
"batch_id": "weekly-2026-17",
"batch_metadata": null,
"channel_metadata": null,
"first_product_source": {
"source_id": "supplier-weekly-feed",
"source_name": "Supplier weekly feed",
"imported_at": "2026-04-24T12:58:55.123Z",
"batch_id": "weekly-2026-17"
}
},
"message": "Imported 2 of 2 products successfully, queued 1 search-sync batches"
}Partial vs replace
// Existing product:
// { entity_code: "SKU-0001", name: {...}, description: "old", price: 10, media: [{type:"image",...}] }
// Incoming:
{ "entity_code": "SKU-0001", "price": 12.5 }
// Stored new version:
// { entity_code: "SKU-0001", name: {...}, description: "old", price: 12.5, media: [{type:"image",...}] }// Same existing product, same incoming payload:
// Stored new version:
// { entity_code: "SKU-0001", price: 12.5 }
// (name, description, media dropped unless re-sent — except locked_fields and auto-published flags, which are always re-applied)Queued JSON import (asynchronous)
/api/b2b/pim/import/queueauth: sessionIdentical payload shape to /import/api, but the request returns
immediately after queueing. A background worker applies the same rules
asynchronously. Use this for large batches or when latency matters.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
source_id | string | Required | ImportSource to apply. |
products | Product[] | Required | 1 ≤ length ≤ 10000. |
batch_metadata | object | Optional | Optional { batch_id, batch_part, batch_total_parts, batch_total_items }. Stored on the ImportJob and forwarded to the worker. |
Retry behavior
- Generated job id is returned in the response.
- Transient failures are retried automatically with exponential backoff.
- Persistent failures land on the job as
status: "failed"with per-row errors inimport_errors.
Example
curl -X POST https://your-tenant.example.com/api/b2b/pim/import/queue \
-H "Content-Type: application/json" \
-H "x-auth-method: api-key" \
-H "x-api-key-id: ak_acme_live_1234" \
-H "x-api-secret: sk_live_abcdef..." \
-d '{
"source_id": "supplier-weekly-feed",
"batch_metadata": {
"batch_id": "weekly-2026-17",
"batch_part": 1,
"batch_total_parts": 4,
"batch_total_items": 8000
},
"products": [
{ "entity_code": "SKU-0001", "price": 12.5 },
{ "entity_code": "SKU-0002", "price": 8.9 }
]
}'{
"success": true,
"job_id": "queue_import_1714142335123_m3p7q2",
"status": "queued",
"summary": {
"products_queued": 2,
"source_id": "supplier-weekly-feed",
"batch_metadata": {
"batch_id": "weekly-2026-17",
"batch_part": 1,
"batch_total_parts": 4,
"batch_total_items": 8000
}
},
"message": "Queued 2 products for import. Job ID: queue_import_1714142335123_m3p7q2"
}Poll status at GET /api/b2b/pim/jobs/:jobId.
Queue status
/api/b2b/pim/import/queueauth: sessionSnapshot of the in-flight queue. Combines persisted job state (recent
pending and processing ImportJobs) with live runtime counters
when available.
Response
| Field | Type | Required | Description |
|---|---|---|---|
queue_stats | object | null | Optional | Live counters: waiting, active, completed, failed. May be null when runtime stats are unavailable. |
pending_jobs[] | ImportJob | Optional | Up to 10 most recent pending jobs, newest first. |
processing_jobs[] | ImportJob | Optional | Up to 5 currently processing jobs. |
Example
{
"queue_stats": { "waiting": 2, "active": 1, "completed": 413, "failed": 6 },
"pending_jobs": [
{
"job_id": "queue_import_1714142335123_m3p7q2",
"source_id": "supplier-weekly-feed",
"total_rows": 2000,
"status": "pending",
"batch_id": "weekly-2026-17",
"batch_part": 1,
"batch_total_parts": 4,
"created_at": "2026-04-24T12:58:55.123Z"
}
],
"processing_jobs": [
{
"job_id": "import_1714142000000_a4f9k2",
"source_id": "supplier-weekly-feed",
"total_rows": 1800,
"processed_rows": 1200,
"status": "processing",
"started_at": "2026-04-24T12:55:10.000Z"
}
]
}Supported entities
The import worker is currently product-centric. Other entities are imported through their dedicated endpoints:
- products —
/api/b2b/pim/import/*(this page). - categories —
/api/b2b/pim/categories/:id/import. - brands —
/api/b2b/pim/brands/:id/import. - collections —
/api/b2b/pim/collections/:id/import. - product types —
/api/b2b/pim/product-types/:id/import. - synonym dictionaries —
/api/b2b/pim/synonym-dictionaries/:id/import.
See each entity's page for its specific payload.
Idempotency and retries
Re-uploading the same file or re-sending the same array does not
deduplicate by itself. Rows match existing products by entity_code
(falling back to sku) and create a new version on every successful
apply.
Full product payload reference
The block below is a single POST /api/b2b/pim/import/api request populating
every supported product field. Copy it as a starting template and strip
the sections you don't need.
{
"source_id": "supplier-weekly-feed",
"merge_mode": "replace",
"batch_id": "weekly-2026-17",
"batch_metadata": {
"batch_id": "weekly-2026-17",
"batch_part": 1,
"batch_total_parts": 4,
"batch_total_items": 8000
},
"channel_metadata": {
"b2b": { "tenant_id": "acme" },
"b2c": { "store_id": "store_main" }
},
"products": [
{
"entity_code": "SKU-0001",
"sku": "SKU-0001",
"name": {
"en": "Brass ball valve 1/2\"",
"it": "Valvola a sfera in ottone 1/2\"",
"de": "Messing-Kugelhahn 1/2\"",
"fr": "Robinet à boisseau sphérique en laiton 1/2\""
},
"slug": {
"en": "brass-ball-valve-1-2",
"it": "valvola-sfera-ottone-1-2",
"de": "messing-kugelhahn-1-2",
"fr": "robinet-boisseau-spherique-laiton-1-2"
},
"description": {
"en": "Full-bore threaded brass ball valve with PTFE seals.",
"it": "Valvola a sfera in ottone a passaggio totale con guarnizioni PTFE.",
"de": "Vollbohrungs-Messing-Kugelhahn mit PTFE-Dichtungen.",
"fr": "Robinet à boisseau sphérique en laiton à passage intégral."
},
"short_description": {
"en": "Professional 1/2\" brass ball valve.",
"it": "Valvola a sfera professionale 1/2\"."
},
"long_description": {
"en": "Forged brass body, chrome-plated ball, full-bore, PN40 rated, suitable for water, air and gas installations.",
"it": "Corpo in ottone forgiato, sfera cromata, passaggio totale, PN40, adatta per installazioni acqua, aria e gas."
},
"channels": ["default", "b2b", "b2c"],
"not_visible": false,
"item_creation_date": "2024-03-15T00:00:00.000Z",
"product_kind": "standard",
"is_parent": true,
"include_faceting": true,
"share_images_with_variants": false,
"share_media_with_variants": false,
"product_model": "BV-050-PTFE",
"ean": ["8001234567890", "8001234567891"],
"product_status": "active",
"product_status_description": {
"en": "Available",
"it": "Disponibile",
"de": "Verfügbar"
},
"stock_status": "in_stock",
"quantity": 420,
"sold": 37,
"unit": "pcs",
"weight": 0.23,
"weight_uom": "KG",
"volume": 180,
"volume_uom": "CM3",
"dimension_height": 4.2,
"dimension_width": 5.1,
"dimension_length": 7.8,
"dimension_uom": "CM",
"brand": {
"brand_id": "brand_acme",
"label": "Acme",
"slug": "acme",
"description": "Professional plumbing since 1921.",
"logo_url": "https://cdn.example.com/brands/acme.svg",
"website_url": "https://acme.example.com",
"is_active": true,
"product_count": 1843,
"display_order": 10,
"parent_brand_id": null,
"brand_family": "acme-group",
"level": 0,
"path": ["acme"],
"hierarchy": [
{ "brand_id": "brand_acme", "label": "Acme", "slug": "acme", "logo_url": "https://cdn.example.com/brands/acme.svg", "level": 0 }
]
},
"category": {
"category_id": "cat_plumbing_valves",
"name": { "en": "Ball valves", "it": "Valvole a sfera", "de": "Kugelhähne" },
"slug": { "en": "ball-valves", "it": "valvole-a-sfera", "de": "kugelhaehne" },
"details": { "en": "Shut-off valves for water, air and gas." },
"image": { "id": "img_cat_valves", "thumbnail": "https://cdn.example.com/cats/valves-thumb.jpg", "original": "https://cdn.example.com/cats/valves.jpg" },
"icon": "valve",
"parent_id": "cat_plumbing",
"level": 2,
"path": ["plumbing", "valves", "ball-valves"],
"hierarchy": [
{ "category_id": "cat_plumbing", "name": { "en": "Plumbing", "it": "Idraulica" }, "slug": { "en": "plumbing", "it": "idraulica" }, "level": 0 },
{ "category_id": "cat_valves", "name": { "en": "Valves", "it": "Valvole" }, "slug": { "en": "valves", "it": "valvole" }, "level": 1 }
],
"is_active": true,
"display_order": 5
},
"channel_categories": [
{
"channel_code": "b2c",
"category": {
"category_id": "b2c_cat_diy_plumbing",
"name": { "en": "DIY Plumbing", "it": "Idraulica fai-da-te" },
"slug": { "en": "diy-plumbing", "it": "idraulica-fai-da-te" }
}
}
],
"collections": [
{
"collection_id": "coll_pro_plumber",
"name": { "en": "Pro plumber", "it": "Idraulico professionale" },
"slug": { "en": "pro-plumber", "it": "idraulico-professionale" },
"description": "Curated tools for professionals.",
"is_active": true,
"display_order": 3,
"level": 0,
"path": ["pro-plumber"]
}
],
"product_type": {
"product_type_id": "pt_valve_ball",
"code": "037",
"name": { "en": "Ball valve", "it": "Valvola a sfera" },
"slug": { "en": "ball-valve", "it": "valvola-a-sfera" },
"description": "Quarter-turn shut-off valve.",
"is_active": true,
"technical_specifications": [
{ "technical_specification_id": "ts_body_material", "key": "body_material", "label": { "en": "Body material", "it": "Materiale corpo" }, "type": "select", "options": ["brass", "stainless_steel", "pvc"], "required": true, "display_order": 0 },
{ "technical_specification_id": "ts_pressure", "key": "max_pressure", "label": { "en": "Max pressure", "it": "Pressione max" }, "type": "number", "unit": "bar", "required": true, "display_order": 1 }
]
},
"attributes": {
"en": [
{ "key": "color", "label": "Color", "value": "brass" },
{ "key": "thread", "label": "Thread", "value": "1/2\" BSP" }
],
"it": [
{ "key": "color", "label": "Colore", "value": "ottone" },
{ "key": "thread", "label": "Filetto", "value": "1/2\" BSP" }
]
},
"marketing_features": {
"en": ["Full-bore design", "PN40 rated", "PTFE seals", "Lead-free brass"],
"it": ["Passaggio totale", "PN40", "Guarnizioni PTFE", "Ottone senza piombo"]
},
"technical_specifications": {
"en": [
{ "key": "body_material", "label": "Body material", "value": "brass", "category": "Materials", "order": 0 },
{ "key": "max_pressure", "label": "Max pressure", "value": 40, "uom": "bar", "category": "Performance", "order": 1 },
{ "key": "temp_range", "label": "Temperature range", "value": "-20 to 120", "uom": "°C", "category": "Performance", "order": 2 }
],
"it": [
{ "key": "body_material", "label": "Materiale corpo", "value": "ottone", "category": "Materiali", "order": 0 },
{ "key": "max_pressure", "label": "Pressione max", "value": 40, "uom": "bar", "category": "Prestazioni", "order": 1 }
]
},
"tags": [
{
"tag_id": "tag_bestseller",
"name": { "en": "Bestseller", "it": "Più venduto" },
"slug": "bestseller",
"color": "#ffb400",
"is_active": true,
"display_order": 0,
"tag_category": "marketing",
"tag_group": "highlights"
},
{
"tag_id": "tag_eco",
"name": { "en": "Lead-free", "it": "Senza piombo" },
"slug": "lead-free",
"color": "#3ba55a",
"is_active": true,
"tag_category": "compliance"
}
],
"synonym_keys": ["plumbing_valve_core", "brass_fittings"],
"meta": [
{ "key": "supplier_part_number", "value": "ACM-BV-050" },
{ "key": "warranty_years", "value": "5" }
],
"images": [
{
"url": "https://cdn.example.com/products/sku-0001/front.jpg",
"cdn_key": "products/sku-0001/front.jpg",
"position": 0,
"file_name": "front.jpg",
"file_type": "image/jpeg",
"size_bytes": 184523,
"uploaded_at": "2026-04-01T10:12:33.000Z",
"uploaded_by": "supplier-weekly-feed"
},
{
"url": "https://cdn.example.com/products/sku-0001/side.jpg",
"cdn_key": "products/sku-0001/side.jpg",
"position": 1,
"file_type": "image/jpeg"
}
],
"media": [
{
"type": "document",
"url": "https://cdn.example.com/products/sku-0001/manual-it.pdf",
"s3_key": "products/sku-0001/manual-it.pdf",
"label": { "en": "User manual (Italian)", "it": "Manuale d'uso" },
"language": "it",
"file_type": "application/pdf",
"size_bytes": 420113,
"position": 0,
"is_external_link": false
},
{
"type": "video",
"url": "https://www.youtube.com/watch?v=abc123",
"label": { "en": "Installation video", "it": "Video installazione" },
"language": "en",
"position": 1,
"is_external_link": true
},
{
"type": "3d-model",
"url": "https://cdn.example.com/products/sku-0001/model.glb",
"s3_key": "products/sku-0001/model.glb",
"label": { "en": "3D model" },
"file_type": "model/gltf-binary",
"size_bytes": 1842771,
"position": 2
}
],
"pricing": {
"list": 9.80,
"retail": 12.50,
"sale": 10.90,
"currency": "EUR",
"vat_rate": 22,
"vat_included": false
},
"packaging_options": [
{
"pkg_id": "1",
"code": "PZ",
"label": { "en": "Piece", "it": "Pezzo" },
"qty": 1,
"uom": "PZ",
"is_default": true,
"is_smallest": true,
"is_sellable": true,
"ean": "8001234567890",
"position": 0,
"pricing": {
"list": 9.80,
"retail": 12.50,
"sale": 10.90,
"list_unit": 9.80,
"retail_unit": 12.50,
"sale_unit": 10.90,
"vat_included": false
}
},
{
"pkg_id": "2",
"code": "BOX",
"label": { "en": "Box of 10", "it": "Scatola da 10" },
"qty": 10,
"uom": "PZ",
"is_default": false,
"is_smallest": false,
"is_sellable": true,
"ean": "8001234567891",
"position": 1,
"pricing": {
"price_ref": "PZ",
"list_discount_pct": 5,
"sale_discount_pct": 10,
"vat_included": false
},
"promotions": [
{
"promo_code": "BOX-10",
"promo_row": 1,
"is_active": true,
"promo_type": "STD",
"calc_method": "RPNQMIN",
"label": { "en": "Box discount -10%", "it": "Sconto scatola -10%" },
"language": "en",
"discount_percentage": 10,
"is_stackable": false,
"priority": 5,
"start_date": "2026-04-01T00:00:00.000Z",
"end_date": "2026-06-30T23:59:59.000Z",
"min_quantity": 1
}
]
}
],
"packaging_info": [
{ "packaging_id": "pi_pz", "code": "PZ", "description": "Single piece", "qty": 1, "uom": "PZ", "is_default": true, "is_smallest": true },
{ "packaging_id": "pi_box", "code": "BOX", "description": "Box of 10", "qty": 10, "uom": "PZ", "is_default": false, "is_smallest": false }
],
"promotions": [
{
"promo_code": "SPRING-2026",
"promo_row": 1,
"is_active": true,
"promo_type": "STD",
"calc_method": "RVMSC",
"label": { "en": "Spring promo -12%", "it": "Promo primavera -12%" },
"language": "en",
"discount_percentage": 12,
"is_stackable": true,
"priority": 10,
"start_date": "2026-04-15T00:00:00.000Z",
"end_date": "2026-05-15T23:59:59.000Z",
"min_quantity": 1,
"min_order_value": 50,
"target_pkg_ids": ["1", "2"]
}
],
"promo_code": ["SPRING-2026", "BOX-10"],
"promo_type": ["STD"],
"has_active_promo": true,
"parent_sku": null,
"parent_entity_code": null,
"variants_sku": ["SKU-0001-RED", "SKU-0001-BLU"],
"variants_entity_code": ["SKU-0001-RED", "SKU-0001-BLU"],
"parent_product": null,
"sibling_variants": [],
"meta_title": {
"en": "Brass ball valve 1/2\" - Acme | Your Store",
"it": "Valvola a sfera 1/2\" Acme | Il Tuo Store"
},
"meta_description": {
"en": "Buy the Acme 1/2\" brass ball valve online - PN40, PTFE seals, 5-year warranty.",
"it": "Acquista la valvola a sfera Acme 1/2\" online - PN40, guarnizioni PTFE, garanzia 5 anni."
},
"auto_publish_enabled": true,
"min_score_threshold": 80,
"required_fields": ["name", "sku", "pricing.list", "images"]
}
]
}Field groups at a glance
| Field | Type | Required | Description |
|---|---|---|---|
source_id | string | Required | ImportSource whose field_mappings and auto-publish rules are applied. |
merge_mode | 'replace' | 'partial' | Optional | See Direct JSON import.(default: 'replace') |
batch_id | string | Optional | Persisted on each product's source.batch_id. |
batch_metadata | object | Optional | { batch_id, batch_part, batch_total_parts, batch_total_items } — stored on source.batch_metadata. |
channel_metadata | object | Optional | Reserved. Echoed back in the debug block; not yet consumed by the pipeline. |
products[] | Product | Optional | 1..10000 items. |
| Field | Type | Required | Description |
|---|---|---|---|
entity_code | string | Required | Tenant-unique id. Falls back to sku. |
sku | string | Optional | Defaults to entity_code. |
name / slug / description / short_description / long_description | MultilingualText | Optional | { <lang>: string } map. Plain strings are auto-wrapped as { <defaultLang>: value }. |
meta_title / meta_description | MultilingualText | Optional | SEO metadata per language. |
product_status_description | MultilingualText | Optional | Localized status label. |
| Field | Type | Required | Description |
|---|---|---|---|
brand | BrandEmbedded | Optional | Snapshot of the brand (label, slug, logo_url, hierarchy...). |
category | CategoryEmbedded | Optional | Primary category including path + hierarchy[] for self-containment. |
channel_categories[] | { channel_code, category } | Optional | Per-channel category overrides. |
collections[] | CollectionEmbedded[] | Optional | Marketing collections. |
product_type | ProductTypeEmbedded | Optional | Type + its technical_specifications[] schema. |
tags[] | TagEmbedded[] | Optional | Marketing / SEO tags with groups and colors. |
synonym_keys[] | string[] | Optional | References SynonymDictionary.key for search expansion. |
channels[] | string[] | Optional | Sales channels this product is visible in. Defaults to ['default'] if omitted. |
| Field | Type | Required | Description |
|---|---|---|---|
images[] | { url, cdn_key, position, file_type?, size_bytes?, uploaded_at?, uploaded_by? } | Optional | Gallery. images[0] is the cover. |
media[] | { type, url, s3_key?, label, language?, file_type?, size_bytes?, position, is_external_link? } | Optional | Documents, videos (incl. YouTube/Vimeo), 3D models. |
attributes | { <lang>: { key, label, value }[] } | Optional | Typed per-language attribute lists. |
marketing_features | { <lang>: string[] } | Optional | Bullet-list highlights. |
technical_specifications | { <lang>: { key, label, value, uom?, category?, order? }[] } | Optional | Spec sheet per language. |
meta[] | { key, value }[] | Optional | Free-form key/value metadata. |
| Field | Type | Required | Description |
|---|---|---|---|
weight / weight_uom | number / string | Optional | e.g. 0.23 + 'KG'. |
volume / volume_uom | number / string | Optional | e.g. 180 + 'CM3'. |
dimension_height / _width / _length / _uom | number / string | Optional | Bounding box dimensions. |
| Field | Type | Required | Description |
|---|---|---|---|
quantity | number | Optional | Available stock. |
sold | number | Optional | Lifetime sold counter. |
unit | string | Optional | Primary selling unit (default 'pcs'). |
stock_status | 'in_stock' | 'out_of_stock' | 'pre_order' | Optional | Storefront badge. |
ean[] | string[] | Optional | EAN/GTIN barcodes. |
product_model | string | Optional | Manufacturer model code. |
product_status | string | Optional | ERP status string. |
| Field | Type | Required | Description |
|---|---|---|---|
pricing | ProductPricing | Optional | { list, retail?, sale?, currency, vat_rate?, vat_included? }. |
packaging_options[] | PackagingOption[] | Optional | Sellable packagings with optional per-package pricing and embedded promotions[]. Supports reference-based pricing via price_ref + list_discount_pct / sale_discount_pct. |
packaging_info[] | PackagingInfo[] | Optional | Physical packaging reference; informational only. |
promotions[] | Promotion[] | Optional | Product-level promotions. Source of truth — computed into packagings on GET. |
promo_code[] / promo_type[] / has_active_promo | string[] / string[] / boolean | Optional | Denormalized fields used for storefront faceting and filtering. |
| Field | Type | Required | Description |
|---|---|---|---|
is_parent | boolean | Optional | true for simple + parent products, false for variant children. Default: true. |
include_faceting | boolean | Optional | false for parents that only group variants. Default: true. |
parent_sku / parent_entity_code | string | Optional | Required when the product is a variant. |
variants_sku[] / variants_entity_code[] | string[] | Optional | Children of a parent product. |
parent_product | { entity_code, sku, name, slug, cover_image_url?, price?, brand?, category? } | Optional | Self-contained parent snapshot for variant rows. |
sibling_variants[] | { entity_code, sku, name, variant_attributes?, cover_image_url?, price?, stock_status?, is_active? }[] | Optional | Peer variants (excluding self) for storefront selectors. |
share_images_with_variants | boolean | Optional | Parent only: variants inherit parent images in search results. |
share_media_with_variants | boolean | Optional | Parent only: same for media. |
| Field | Type | Required | Description |
|---|---|---|---|
not_visible | boolean | Optional | Hide from storefront even when published. |
auto_publish_enabled | boolean | Optional | Enable auto-publish eligibility checks on this product. |
min_score_threshold | number | Optional | Completeness score required for auto-publish (default 80). |
required_fields[] | string[] | Optional | Dotted paths that must be populated to qualify for auto-publish. |
item_creation_date | Date (ISO string) | Optional | When the item was originally inserted in the source ERP. |
product_kind | 'standard' | 'bookable' | 'service' | Optional | Topology hint for downstream flows. Default: 'standard'. |
Errors
| Field | Type | Required | Description |
|---|---|---|---|
400 | ValidationError | Optional | Missing file/source_id, file > 50MB, unsupported extension, empty products[] or > 10000 products. |
401 | Unauthenticated | Optional | Missing or invalid credential. |
403 | Forbidden | Optional | API key lacks the 'import' permission (direct JSON endpoint only). |
404 | NotFound | Optional | source_id does not resolve to an existing ImportSource for this tenant. |
500 | ServerError | Optional | CDN not configured, CDN upload failed, or downstream worker error. |