Upload items to a dataset

post/v1/datasets/{datasetId}/items/upload

Queues up to 5,000 products for processing in one request. Nothing is searchable yet when this call returns: each product becomes a pending queue entry, and a background worker turns it into a DatasetItem — downloading and storing its images, extracting style metadata and computing embeddings. Track progress with the queue endpoints; the itemId of a queue entry is the itemId of the item it produces.

disableGenerativeAssets is accepted for compatibility but ignored: generative thumbnails are disabled platform-wide and every entry is stored with the value true.

Validation

Validation is all-or-nothing. Every product is checked before any is queued; if one fails, the whole request is rejected and nothing is written. Failures come back in details as { index, path, message } where index is the product's position in items (or -1 for request-wide SKU problems) and path is the offending field. The per-field messages are:

  • Product must be an object.
  • Expected string, got <type> — one of name, handle, description, publishedAt, createdAt, updatedAt, productType, sku is not a string
  • Price must be a non-negative number.
  • SKU must not be empty. — sku is blank or only whitespace
  • Unparseable date: <value> — publishedAt, createdAt or updatedAt cannot be parsed by Date
  • Expected an array. — tags, images, options or variants is not an array
  • Expected <type>, got <type> — an element of those arrays has the wrong type (tags must hold strings; the others objects)
  • Expected string. — a variant's sku, name, createdAt, updatedAt or productId is not a string
  • Price must be non-negative number. — a variant's price
  • Expected boolean. — a variant's available
  • Expected one of mens, womens, unisex, or null. — gender is present and not one of those

Dates are normalised to ISO 8601 UTC before they are stored. An empty publishedAt is accepted and means the product is unpublished. Fields outside the documented shape are dropped.

A few rules are enforced only by the database when the batch is written, and break the whole request with the shared 500 Internal server error. (nothing is queued): name and handle must be non-empty; each variant's name and productId must be non-empty; every images entry needs a src; tags and option values must not contain empty strings. A variants value that is neither an array nor omitted, or a null element inside variants, also returns the shared 500 Internal server error.

Re-uploading a product

SKUs are matched ignoring case and surrounding whitespace, the same way a data source sync matches them. A SKU must appear only once in the request. When it already has a queue entry in the dataset (in any status except archived), the upload updates that entry instead of creating a second one:

  • updatedAt differs from the stored product → the entry is re-queued. If the image URLs changed it goes through full processing again (pending); otherwise only name, description, price and sizes are refreshed (partial_update).
  • The entry is failed → it is re-queued for full processing.
  • Otherwise nothing is written; the product is counted as unchanged.

A SKU whose only entry is archived brings that entry back, with the same itemId, and is counted as revived. If it was fully processed and its image URLs, name and product type are unchanged, it returns through partial_update; otherwise it is processed again (pending).

This makes the endpoint safe to retry, and usable for price and stock refreshes as long as updatedAt moves with the change.

Private keyapi.v1.datasets.items.uploadNo demo

Path parameters

1
datasetIdstring (uuid)required

The dataset's id, as returned when it was created.

Request body

application/jsonrequired
array<IngestProduct>required

Products to queue. Between 1 and 5,000 entries.

minItems: 1maxItems: 5000
namestringrequired

Product title. Must be non-empty.

e.g. "Classic Crew Tee"
handlestringrequired

URL slug. The product URL is built as <store URL>/products/<handle>. Must be non-empty.

e.g. "classic-crew-tee"
descriptionstringrequired

Product description; HTML is allowed. May be an empty string.

e.g. "<p>A heavyweight organic cotton tee with a relaxed fit.</p>"
publishedAtstringrequired

Any string Date can parse (ISO 8601 recommended).

e.g. "2024-11-02T09:15:00Z"
createdAtstringrequired

Any string Date can parse.

e.g. "2024-11-02T09:15:00Z"
updatedAtstringrequired

Any string Date can parse.

e.g. "2025-02-10T12:00:00Z"
productTypestringrequired

Merchant category. May be an empty string.

e.g. "T-Shirts"
skustringrequired

Product-level SKU. Must be non-empty, unique within the request, and not already present in the dataset's queue.

e.g. "A1A2P-BB2J"
pricenumberrequired

Product price. Must be a non-negative number.

min: 0e.g. 49
tagsarray<string>required

Free-form tags. Every element must be a non-empty string.

array<IngestProductImage>required

Images to download. Every element must be an object with src.

array<IngestProductOption>required

Product options. Every element must be an object.

array<IngestProductVariant>required

Purchasable variants. Every element must be an object; a null element fails the request with the shared 500.

genderstringnullable

Optional. Who you sell the product to, when you know. It is stored as the item's metadata.gender in place of the one the labeller would choose, ahead of any department the title or tags name and of the dataset's targetGender. Products that are not worn (home goods, beauty, gift cards) are always unisex. Omit it or send null to let the labeller decide. Changing it on an existing SKU relabels the product.

One ofmenswomensunisexnull
e.g. "womens"
disableGenerativeAssetsboolean

Must be a boolean when present (null is rejected). Currently ignored — every entry is stored with true.

default: true

Response

200Every product was queued.
successbooleanrequired

Always true on this response.

e.g. true
uploadedintegerrequired

Number of products accepted — equal to the length of items, and to added + updated + revived + unchanged.

e.g. 1
addedinteger

Products queued as new entries.

e.g. 1
updatedinteger

Existing queue entries re-queued with the new product data.

e.g. 0
revivedinteger

Archived entries brought back with the new product data, keeping their itemId.

e.g. 0
unchangedinteger

Products whose queue entry was already up to date; nothing was written for them.

e.g. 0
array<object>

Non-blocking notices about individual products. Reserved; the current validator emits none, so the key is absent.

indexinteger

Position of the product in items.

pathstring

Field the notice refers to.

messagestring

The notice.

Errors

26

Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.

400
Request body must be valid JSON.

The request body could not be parsed as JSON.

400
Request body must be a JSON object.

The request body is empty, or parses to something other than a JSON object (for example an array or null).

401
Organization not found. Please check the organizationId.

The organization encoded in the API key no longer exists.

404
Dataset not found for this organization.

No dataset with this datasetId belongs to the key's organization.

413
Upload limit exceeded (<count> > 5000).

items holds more than 5,000 products. Split the upload into batches.

422
The 'disableGenerativeAssets' field must be either true or false when provided.

disableGenerativeAssets is present but not a boolean (null counts as invalid).

422
`items` must be a non-empty array.

items is missing, not an array, or empty.

422
Duplicate SKUs in payload.

The same sku (ignoring case and surrounding whitespace) appears on more than one product in items. Checked after per-product validation passes. details names each repeat by its position in items.

{
  "error": "Duplicate SKUs in payload.",
  "details": [
    {
      "index": 3,
      "path": "sku",
      "message": "A1A2P-BB2J appears earlier in this request."
    }
  ]
}
422
Validation failed.

One or more products failed field validation. details lists every failure as { index, path, message }; nothing was queued.

{
  "error": "Validation failed.",
  "details": [
    {
      "index": 0,
      "path": "price",
      "message": "Price must be a non-negative number."
    },
    {
      "index": 2,
      "path": "variants[0].available",
      "message": "Expected boolean."
    }
  ]
}
curl -X POST "https://stylor.ai/api/v1/datasets/YOUR_DATASET_ID/items/upload" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "name": "Classic Crew Tee",
      "handle": "classic-crew-tee",
      "description": "<p>A heavyweight organic cotton tee with a relaxed fit.</p>",
      "publishedAt": "2024-11-02T09:15:00Z",
      "createdAt": "2024-11-02T09:15:00Z",
      "updatedAt": "2025-02-10T12:00:00Z",
      "productType": "T-Shirts",
      "sku": "A1A2P-BB2J",
      "price": 49,
      "tags": [
        "organic",
        "basics"
      ],
      "images": [
        {
          "src": "https://cdn.shopify.com/s/files/1/0001/products/tee-front.jpg"
        },
        {
          "src": "https://cdn.shopify.com/s/files/1/0001/products/tee-back.jpg"
        }
      ],
      "options": [
        {
          "name": "Size",
          "values": [
            "Medium",
            "Large"
          ]
        }
      ],
      "variants": [
        {
          "sku": "A1A2P-BB2J-M",
          "name": "Medium",
          "available": true,
          "price": 49,
          "createdAt": "2024-11-02T09:15:00Z",
          "updatedAt": "2025-02-10T12:00:00Z",
          "productId": "8412345678901"
        },
        {
          "sku": "A1A2P-BB2J-L",
          "name": "Large",
          "available": false,
          "price": 49,
          "createdAt": "2024-11-02T09:15:00Z",
          "updatedAt": "2025-02-10T12:00:00Z",
          "productId": "8412345678901"
        }
      ]
    }
  ]
}'
Response · 200
{
  "success": true,
  "uploaded": 1,
  "added": 1,
  "updated": 0,
  "unchanged": 0
}