Download OpenAPI specification:
How a brand's own systems publish content to any OBX node.
Normative. The OBX Ingest API lets a brand's automation (PIM/ERP/DAM connector,
spreadsheet export, the obx CLI, a GitHub Action, n8n/Make/Zapier) publish to ANY
conforming node. It is keyed by the brand's own ids, so every write is an idempotent
upsert: send the same thing twice and nothing changes (and no change entry is written).
See docs/ingest.md for the full semantics, including exactly how writes appear in the
OBX Protocol /changes feed, and docs/import-format.md for the canonical CSV and
image filename pattern.
Authentication: Authorization: Bearer <ingest token> (tokens start with obxi_),
created in the node admin. Scopes: ingest:read, ingest:write, ingest:delete.
A token MAY be restricted to some brands; writes outside them fail with 403 forbidden.
Errors are application/problem+json with obx_error (see docs/protocol.md#errors).
Unknown fields in request bodies MUST be ignored by nodes (forward compatibility) and
reported as a warning in the result.
Any scope. Clients SHOULD call this first; renditions.server_generated=false means the client must upload web/square/thumb itself.
{- "token": {
- "id": "tok_1",
- "name": "PIM nightly",
- "scopes": [
- "ingest:read",
- "ingest:write"
], - "brand_external_ids": null,
- "expires_at": null
}, - "limits": {
- "max_upload_bytes": 104857600,
- "max_object_bytes": 2147483648,
- "part_bytes": 8388608,
- "max_batch_operations": 1000,
- "max_import_bytes": 2147483648
}, - "renditions": {
- "server_generated": false,
- "required": [
- "original",
- "web",
- "square",
- "thumb"
]
}, - "accepted_content_types": [
- "image/jpeg",
- "image/png",
- "image/webp",
- "image/tiff"
], - "default_status": "published",
- "brands": [
- {
- "external_id": "kestrel",
- "name": "Kestrel"
}
], - "languages": [
- "nl",
- "en"
]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "kind": "asset",
- "id": "ast_1",
- "external_id": "KO-1001_NVY_1",
- "status": "published",
- "visibility": "pending_files",
- "etag": "\"c3d4\"",
- "created_at": "2026-09-28T10:00:00Z",
- "updated_at": "2026-09-28T10:00:00Z",
- "data": {
- "product_external_id": "KO-1001",
- "color_code": "NVY",
- "role": "packshot",
- "view": "front",
- "position": 1,
- "files": {
- "original": {
- "sha256": "2222222222222222222222222222222222222222222222222222222222222222",
- "width": 1200,
- "height": 1500
}
}
}, - "missing_files": [
- "web",
- "square",
- "thumb"
]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
| name required | string [ 1 .. 200 ] characters |
Url (string) or null | |
Url (string) or null | |
LocalizedText (object) or null | |
| status | string (PublicationStatus) Enum: "published" "unpublished" Brand-side publication state. Only 'published' objects (with published parents) are visible through the OBX Protocol. |
| ^x-[a-z0-9]+-[a-z0-9_-]+$ pattern property | any |
{- "name": "Kestrel Trail",
- "description": {
- "en": "Trail running gear.",
- "nl": "Trailrunningkleding."
}
}{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
{ }{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}Requires scope ingest:delete. Fails with 409 conflict while the brand still has products (delete or move them first). Idempotent: deleting an unknown id returns 200 with result unchanged.
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "kind": "asset",
- "id": "ast_1",
- "external_id": "KO-1001_NVY_1",
- "status": "published",
- "visibility": "pending_files",
- "etag": "\"c3d4\"",
- "created_at": "2026-09-28T10:00:00Z",
- "updated_at": "2026-09-28T10:00:00Z",
- "data": {
- "product_external_id": "KO-1001",
- "color_code": "NVY",
- "role": "packshot",
- "view": "front",
- "position": 1,
- "files": {
- "original": {
- "sha256": "2222222222222222222222222222222222222222222222222222222222222222",
- "width": 1200,
- "height": 1500
}
}
}, - "missing_files": [
- "web",
- "square",
- "thumb"
]
}Full replacement: omitted optional fields become null. Exception: an omitted status
keeps the current status (new objects get the node's default status, normally
published), so a nightly sync never re-publishes what someone unpublished by hand
unless it sends status explicitly.
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
ExternalId (string) or null | |
| style_number required | string [ 1 .. 200 ] characters |
required | object (LocalizedText) non-empty Free text per language, keyed by BCP 47 tag. At least one language. Example: {"nl": "Trui", "en": "Sweater"}. |
LocalizedText (object) or null | |
| item_type | string or null^[a-z][a-z0-9_]{0,63}$ |
| gpc_brick | string or null^[0-9]{8}$ |
| target_group | string or null^[a-z][a-z0-9_]{0,63}$ |
| season | string or null <= 64 characters |
| collection | string or null <= 200 characters |
Array of Materials (objects) or null | |
Array of Care (objects) or null | |
CountryCode (string) or null | |
Url (string) or null | |
object or null | |
| status | string (PublicationStatus) Enum: "published" "unpublished" Brand-side publication state. Only 'published' objects (with published parents) are visible through the OBX Protocol. |
| ^x-[a-z0-9]+-[a-z0-9_-]+$ pattern property | any |
{- "style_number": "KO-1007",
- "name": {
- "en": "Linen Shirt"
}
}{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
{ }{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}Requires ingest:delete. Emits tombstones for the product and each of its visible variants and assets. Idempotent.
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}Assigns positions 1..n to the listed assets (all non-deleted assets of this product
and color_code MUST be listed; 422 validation_failed otherwise). This is how you
swap or reorder images without position_conflict. Emits one asset upsert per asset
whose position changed.
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| color_code required | string or null null = the product-wide (all colours) assets. |
| asset_external_ids required | Array of strings (ExternalId) [ 1 .. 999 ] items unique [ items [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ ] |
{- "color_code": "string",
- "asset_external_ids": [
- "KO-1001"
]
}[- {
- "index": 0,
- "op": "upsert",
- "kind": "brand",
- "external_id": "KO-1001",
- "result": "created",
- "id": "prd_01J9ZK3Q7T5W",
- "visibility": "visible",
- "etag": "string",
- "error": {
- "title": "string",
- "status": 400,
- "detail": "string",
- "instance": "string",
- "obx_error": "invalid_request",
- "errors": [
- {
- "pointer": "string",
- "obx_error": "invalid_request",
- "detail": "string"
}
], - "retry_after": 0
}, - "warnings": [
- "string"
]
}
]| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "kind": "asset",
- "id": "ast_1",
- "external_id": "KO-1001_NVY_1",
- "status": "published",
- "visibility": "pending_files",
- "etag": "\"c3d4\"",
- "created_at": "2026-09-28T10:00:00Z",
- "updated_at": "2026-09-28T10:00:00Z",
- "data": {
- "product_external_id": "KO-1001",
- "color_code": "NVY",
- "role": "packshot",
- "view": "front",
- "position": 1,
- "files": {
- "original": {
- "sha256": "2222222222222222222222222222222222222222222222222222222222222222",
- "width": 1200,
- "height": 1500
}
}
}, - "missing_files": [
- "web",
- "square",
- "thumb"
]
}The product (product_external_id) MUST exist, else 422 unknown_parent. GTIN check digit MUST be valid (invalid_gtin) and unique in the node (409 gtin_conflict).
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
| product_external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ The brand's own identifier for an object (from its PIM/ERP/spreadsheet). 1-200 characters, no control characters and no '/'. Leading/trailing whitespace is trimmed by nodes. Compared case-sensitively. |
GtinInput (string) or null | |
| sku | string or null <= 200 characters |
required | object (Color) |
| size | string or null <= 64 characters |
| size_system | string or null <= 16 characters |
| status | string (PublicationStatus) Enum: "published" "unpublished" Brand-side publication state. Only 'published' objects (with published parents) are visible through the OBX Protocol. |
| ^x-[a-z0-9]+-[a-z0-9_-]+$ pattern property | any |
{- "product_external_id": "KO-1007",
- "gtin": "2000100701010",
- "color": {
- "code": "SND",
- "name": {
- "en": "Sand"
}
}, - "size": "M",
- "size_system": "INT"
}{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
{ }{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "kind": "asset",
- "id": "ast_1",
- "external_id": "KO-1001_NVY_1",
- "status": "published",
- "visibility": "pending_files",
- "etag": "\"c3d4\"",
- "created_at": "2026-09-28T10:00:00Z",
- "updated_at": "2026-09-28T10:00:00Z",
- "data": {
- "product_external_id": "KO-1001",
- "color_code": "NVY",
- "role": "packshot",
- "view": "front",
- "position": 1,
- "files": {
- "original": {
- "sha256": "2222222222222222222222222222222222222222222222222222222222222222",
- "width": 1200,
- "height": 1500
}
}
}, - "missing_files": [
- "web",
- "square",
- "thumb"
]
}The product MUST exist (unknown_parent). position MUST be free among the product's
other non-deleted assets of the same color_code (409 position_conflict; replacing an
image = reuse its external_id). files attaches blobs uploaded earlier (422
blob_missing if a sha256 is unknown); this is the atomic way to replace an original
together with client-generated renditions. The asset becomes visible once its original
and all required renditions exist (visibility: pending_files until then).
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
| product_external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ The brand's own identifier for an object (from its PIM/ERP/spreadsheet). 1-200 characters, no control characters and no '/'. Leading/trailing whitespace is trimmed by nodes. Compared case-sensitively. |
| color_code | string or null <= 64 characters |
| variant_external_ids | Array of strings or null (ExternalId) [ items [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ ] |
| media_type | string Value: "image" |
| role required | string (Role) Enum: "packshot" "model" "detail" "ghost" "flatlay" "scene" "swatch" "size_chart" "other" What the image shows. Stable key; labels are presentation. |
View (string) or null | |
| position required | integer [ 1 .. 999 ] |
| filename | string or null <= 255 characters Download filename; default |
LocalizedText (object) or null | |
object or null Partial Rights; omitted fields take the node's default rights (Settings). null = all defaults. | |
object or null Omitted fields default to ai_generated=false, ai_edited=false, source=photo (generated when ai_generated), c2pa=none (nodes MAY detect embedded C2PA). | |
object or null Attach previously uploaded blobs atomically. Omit to keep current files (or upload via PUT .../files/{rendition}). | |
| status | string (PublicationStatus) Enum: "published" "unpublished" Brand-side publication state. Only 'published' objects (with published parents) are visible through the OBX Protocol. |
| ^x-[a-z0-9]+-[a-z0-9_-]+$ pattern property | any |
{- "product_external_id": "KO-1007",
- "color_code": "SND",
- "role": "packshot",
- "view": "front",
- "position": 1,
- "rights": {
- "channels": [
- "webshop"
], - "embargo_until": "2027-02-01T00:00:00Z"
}, - "provenance": {
- "ai_generated": false,
- "ai_edited": true,
- "source": "composite"
}, - "files": {
- "original": {
- "sha256": "6666666666666666666666666666666666666666666666666666666666666666"
}, - "web": {
- "sha256": "7777777777777777777777777777777777777777777777777777777777777777",
- "width": 1600,
- "height": 2000
}, - "square": {
- "sha256": "8888888888888888888888888888888888888888888888888888888888888888",
- "width": 2000,
- "height": 2000
}, - "thumb": {
- "sha256": "9999999999999999999999999999999999999999999999999999999999999999",
- "width": 480,
- "height": 600
}
}
}{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| If-Match | string Optional optimistic concurrency with the record's ETag. |
{ }{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}Raw body, Content-Type of the file. OBX-Content-SHA256 is RECOMMENDED; when present
the node verifies it (422 checksum_mismatch). The asset metadata MUST exist (404).
Uploading the same bytes again is a no-op (unchanged).
Replacing original on a node without server-side renditions: the node keeps serving
the previous original and renditions until all required renditions uploaded AFTER the
new original are present, then swaps atomically (one asset upsert in /changes).
Renditions MUST follow the rendition rules (web/thumb: JPEG, longest side <= 2000/600,
never upscaled; square: web on a white square canvas); nodes MAY reject (422
validation_failed) renditions whose size violates them.
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ Examples: KO-1001 The brand's own id, percent-encoded. |
| rendition required | string (RenditionName) Enum: "original" "web" "square" "thumb" Rendition name. |
| OBX-Content-SHA256 | string (Sha256) ^[a-f0-9]{64}$ Lowercase hex sha256 of the body; verified when present. |
{- "index": 0,
- "op": "upsert",
- "kind": "product",
- "external_id": "KO-1001",
- "result": "unchanged",
- "id": "prd_1",
- "visibility": "visible",
- "etag": "\"a1b2\"",
- "error": null,
- "warnings": [ ]
}Dedupe check before uploading. 200 = present (skip the upload), 404 = upload it.
| sha256 required | string (Sha256) ^[a-f0-9]{64}$ Lowercase hex SHA-256 of the exact bytes. |
{- "title": "string",
- "status": 400,
- "detail": "string",
- "instance": "string",
- "obx_error": "invalid_request",
- "errors": [
- {
- "pointer": "string",
- "obx_error": "invalid_request",
- "detail": "string"
}
], - "retry_after": 0
}Raw body. The node verifies the sha256 (422 checksum_mismatch). Blobs not referenced by any asset MAY be garbage-collected after 24 h. Bodies larger than limits.max_upload_bytes use /uploads.
| sha256 required | string (Sha256) ^[a-f0-9]{64}$ Lowercase hex SHA-256 of the exact bytes. |
{- "sha256": "3333333333333333333333333333333333333333333333333333333333333333",
- "bytes": 48211,
- "content_type": "image/jpeg",
- "width": 1200,
- "height": 1500,
- "created": true
}For files above max_upload_bytes. If the node already has the sha256 it answers 200 with the Blob and no upload is needed.
| sha256 required | string (Sha256) ^[a-f0-9]{64}$ Lowercase hex SHA-256 of the exact bytes. |
| bytes required | integer >= 1 |
| content_type required | string |
{- "sha256": "string",
- "bytes": 1,
- "content_type": "string"
}{- "sha256": "3333333333333333333333333333333333333333333333333333333333333333",
- "bytes": 48211,
- "content_type": "image/jpeg",
- "width": 1200,
- "height": 1500,
- "created": true
}| id required | string (Id) ^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$ Examples: prd_01J9ZK3Q7T5W Opaque, node-assigned identifier. Stable for the life of the object. Consumers MUST treat it as opaque. |
{- "id": "upl_1",
- "sha256": "4444444444444444444444444444444444444444444444444444444444444444",
- "bytes": 83886080,
- "content_type": "image/tiff",
- "part_bytes": 8388608,
- "parts_total": 10,
- "received_parts": [
- 1,
- 2,
- 3
], - "status": "open",
- "expires_at": "2026-09-29T10:00:00Z"
}| id required | string (Id) ^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$ Examples: prd_01J9ZK3Q7T5W Opaque, node-assigned identifier. Stable for the life of the object. Consumers MUST treat it as opaque. |
{- "title": "string",
- "status": 400,
- "detail": "string",
- "instance": "string",
- "obx_error": "invalid_request",
- "errors": [
- {
- "pointer": "string",
- "obx_error": "invalid_request",
- "detail": "string"
}
], - "retry_after": 0
}Idempotent per part number; re-sending a part replaces it. Every part except the last MUST be exactly part_bytes.
| id required | string (Id) ^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$ Examples: prd_01J9ZK3Q7T5W Opaque, node-assigned identifier. Stable for the life of the object. Consumers MUST treat it as opaque. |
| number required | integer >= 1 |
{- "id": "upl_1",
- "sha256": "4444444444444444444444444444444444444444444444444444444444444444",
- "bytes": 83886080,
- "content_type": "image/tiff",
- "part_bytes": 8388608,
- "parts_total": 10,
- "received_parts": [
- 1,
- 2,
- 3
], - "status": "open",
- "expires_at": "2026-09-29T10:00:00Z"
}Verifies size and sha256. Missing parts give 409 upload_incomplete; a hash mismatch gives 422 checksum_mismatch (the upload is aborted).
| id required | string (Id) ^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$ Examples: prd_01J9ZK3Q7T5W Opaque, node-assigned identifier. Stable for the life of the object. Consumers MUST treat it as opaque. |
{- "sha256": "3333333333333333333333333333333333333333333333333333333333333333",
- "bytes": 48211,
- "content_type": "image/jpeg",
- "width": 1200,
- "height": 1500,
- "created": true
}Up to limits.max_batch_operations (>= 1000) operations, processed in order, each one
atomic on its own; a failing operation does not stop the rest. Body: a JSON array of
IngestOperation, or NDJSON (one operation per line). Binary data is referenced by
sha256 (upload blobs first). The node answers 200 with all results when it processed
the batch within the request, or 202 with a job (always when the client sends
Prefer: respond-async). Clients MUST handle both.
| Idempotency-Key | string <= 200 characters Repeating a request with the same key within 24 h returns the original response (same job). |
| Prefer | string Value: "respond-async" |
| op required | string Enum: "upsert" "patch" "delete" "publish" "unpublish" upsert = PUT (create or replace); patch = JSON merge patch; publish/unpublish = patch of status. |
| kind required | string Enum: "brand" "product" "variant" "asset" |
| external_id required | string (ExternalId) [ 1 .. 200 ] characters ^[^\x00-\x1F\x7F/]+$ The brand's own identifier for an object (from its PIM/ERP/spreadsheet). 1-200 characters, no control characters and no '/'. Leading/trailing whitespace is trimmed by nodes. Compared case-sensitively. |
| data | object upsert: BrandInput|ProductInput|VariantInput|AssetInput by kind. patch: merge patch of that. Absent for delete/publish/unpublish. |
| if_match | string or null Optional ETag precondition. |
| ^x-[a-z0-9]+-[a-z0-9_-]+$ pattern property | any |
[- {
- "op": "upsert",
- "kind": "brand",
- "external_id": "KO-1001",
- "data": { },
- "if_match": "string"
}
]{- "results": [
- {
- "index": 0,
- "op": "upsert",
- "kind": "brand",
- "external_id": "KO-1001",
- "result": "created",
- "id": "prd_01J9ZK3Q7T5W",
- "visibility": "visible",
- "etag": "string",
- "error": {
- "title": "string",
- "status": 400,
- "detail": "string",
- "instance": "string",
- "obx_error": "invalid_request",
- "errors": [
- {
- "pointer": "string",
- "obx_error": "invalid_request",
- "detail": "string"
}
], - "retry_after": 0
}, - "warnings": [
- "string"
]
}
], - "summary": {
- "created": 0,
- "updated": 0,
- "unchanged": 0,
- "deleted": 0,
- "errors": 0
}
}multipart/form-data with any of: package (zip with products.csv, optional assets.csv
and images), csv (products.csv), assets_csv (assets.csv), images (one zip, or
repeated image files), images_manifest (JSON: images already uploaded as blobs,
referenced by file name + sha256, optionally with client-generated renditions) and
options (JSON). The manifest is how the obx CLI and the admin UI avoid re-uploading
unchanged images: HEAD /blobs/{sha256}, upload only what is missing, then import with
the manifest. Always asynchronous: 202 + job. Row and file problems are reported as
ImportRowError results, never as a failed request. See docs/import-format.md.
| Idempotency-Key | string <= 200 characters Repeating a request with the same key within 24 h returns the original response (same job). |
| package | string <application/zip> |
| csv | string <text/csv> |
| assets_csv | string <text/csv> |
| images | Array of strings |
Array of objects (ImagesManifest) <= 100000 items Images already uploaded as blobs. | |
object (ImportOptions) |
{- "id": "job_1",
- "kind": "import",
- "status": "succeeded",
- "created_at": "2026-09-28T10:00:00Z",
- "finished_at": "2026-09-28T10:00:00Z",
- "progress": {
- "done": 67,
- "total": 67
}, - "summary": {
- "created": 75,
- "updated": 0,
- "unchanged": 0,
- "deleted": 0,
- "errors": 1
}, - "import_run": {
- "id": "run_1",
- "source_id": null,
- "trigger": "api",
- "status": "succeeded",
- "mode": "upsert",
- "dry_run": false,
- "created_at": "2026-09-28T10:00:00Z",
- "started_at": "2026-09-28T10:00:00Z",
- "finished_at": "2026-09-28T10:00:00Z",
- "files": [
- {
- "name": "products.csv",
- "bytes": 10240,
- "sha256": "5555555555555555555555555555555555555555555555555555555555555555"
}
], - "counts": {
- "rows": 40,
- "files": 27,
- "created": 75,
- "updated": 0,
- "unchanged": 0,
- "unpublished": 0,
- "errors": 1,
- "warnings": 0
}, - "error": null,
- "errors_preview": [
- {
- "file": "products.csv",
- "row": 12,
- "column": "gtin",
- "severity": "error",
- "obx_error": "invalid_gtin",
- "detail": "Check digit should be 8.",
- "value": "2000100101019"
}
]
},
}| status | string Enum: "queued" "running" "succeeded" "failed" |
| cursor | string (Cursor) [ 1 .. 4096 ] characters Opaque pagination or feed cursor. Clients MUST NOT parse or construct it. |
| limit | integer [ 1 .. 100 ] Default: 25 |
{- "items": [
- {
- "id": "prd_01J9ZK3Q7T5W",
- "kind": "batch",
- "status": "queued",
- "created_at": "2026-09-28T10:00:00Z",
- "finished_at": "2026-09-28T10:00:00Z",
- "progress": {
- "done": 0,
- "total": 0
}, - "summary": {
- "created": 0,
- "updated": 0,
- "unchanged": 0,
- "deleted": 0,
- "errors": 0
}, - "import_run": {
- "id": "prd_01J9ZK3Q7T5W",
- "source_id": "prd_01J9ZK3Q7T5W",
- "trigger": "upload",
- "status": "queued",
- "mode": "upsert",
- "dry_run": true,
- "created_at": "2026-09-28T10:00:00Z",
- "started_at": "2026-09-28T10:00:00Z",
- "finished_at": "2026-09-28T10:00:00Z",
- "files": [
- {
- "name": "string",
- "bytes": 0,
- "sha256": "string"
}
], - "counts": {
- "rows": 0,
- "files": 0,
- "created": 0,
- "updated": 0,
- "unchanged": 0,
- "unpublished": 0,
- "errors": 0,
- "warnings": 0
}, - "error": {
- "title": "string",
- "status": 400,
- "detail": "string",
- "instance": "string",
- "obx_error": "invalid_request",
- "errors": [
- {
- "pointer": "string",
- "obx_error": "invalid_request",
- "detail": "string"
}
], - "retry_after": 0
}, - "errors_preview": [
- {
- "file": "string",
- "row": 1,
- "column": "string",
- "severity": "error",
- "obx_error": "invalid_request",
- "detail": "string",
- "value": "string"
}
]
},
}
], - "next_cursor": "string"
}| id required | string (Id) ^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$ Examples: prd_01J9ZK3Q7T5W Opaque, node-assigned identifier. Stable for the life of the object. Consumers MUST treat it as opaque. |
{- "id": "job_1",
- "kind": "import",
- "status": "succeeded",
- "created_at": "2026-09-28T10:00:00Z",
- "finished_at": "2026-09-28T10:00:00Z",
- "progress": {
- "done": 67,
- "total": 67
}, - "summary": {
- "created": 75,
- "updated": 0,
- "unchanged": 0,
- "deleted": 0,
- "errors": 1
}, - "import_run": {
- "id": "run_1",
- "source_id": null,
- "trigger": "api",
- "status": "succeeded",
- "mode": "upsert",
- "dry_run": false,
- "created_at": "2026-09-28T10:00:00Z",
- "started_at": "2026-09-28T10:00:00Z",
- "finished_at": "2026-09-28T10:00:00Z",
- "files": [
- {
- "name": "products.csv",
- "bytes": 10240,
- "sha256": "5555555555555555555555555555555555555555555555555555555555555555"
}
], - "counts": {
- "rows": 40,
- "files": 27,
- "created": 75,
- "updated": 0,
- "unchanged": 0,
- "unpublished": 0,
- "errors": 1,
- "warnings": 0
}, - "error": null,
- "errors_preview": [
- {
- "file": "products.csv",
- "row": 12,
- "column": "gtin",
- "severity": "error",
- "obx_error": "invalid_gtin",
- "detail": "Check digit should be 8.",
- "value": "2000100101019"
}
]
},
}| id required | string (Id) ^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$ Examples: prd_01J9ZK3Q7T5W Opaque, node-assigned identifier. Stable for the life of the object. Consumers MUST treat it as opaque. |
| errors_only | boolean Default: false |
| cursor | string (Cursor) [ 1 .. 4096 ] characters Opaque pagination or feed cursor. Clients MUST NOT parse or construct it. |
| limit | integer [ 1 .. 1000 ] Default: 100 |
{- "items": [
- {
- "index": 0,
- "op": "upsert",
- "kind": "brand",
- "external_id": "KO-1001",
- "result": "created",
- "id": "prd_01J9ZK3Q7T5W",
- "visibility": "visible",
- "etag": "string",
- "error": {
- "title": "string",
- "status": 400,
- "detail": "string",
- "instance": "string",
- "obx_error": "invalid_request",
- "errors": [
- {
- "pointer": "string",
- "obx_error": "invalid_request",
- "detail": "string"
}
], - "retry_after": 0
}, - "warnings": [
- "string"
]
}
], - "next_cursor": "string"
}