Skip to content

Products, variants & translations

Products are the heart of the API. Every endpoint, export, feed, webhook and channel works with the same CanonicalProduct shape.

GET/products
List and filter
POST/products
Create
GET/products/{idOrSku}
Get one (by id or SKU)
PATCH/products/{idOrSku}
Update the fields you send
PUT/products/{idOrSku}
Replace (or create under that SKU)
DELETE/products/{idOrSku}
Delete (a parent deletes its variants)
POST/products/bulk
Create or update up to 500 products
GET/products/{idOrSku}/versions
Change history
GET/products/{idOrSku}/channels
Readiness on every channel

The product shape

CanonicalProduct
json
{
  "id": "cm1x0p3k20001",
  "sku": "FR-OUD-100",
  "type": "simple",
  "parentId": null,
  "parentSku": null,
  "status": "approved",
  "name": "Oud Noir Eau de Parfum 100 ml",
  "shortDescription": "A rich oriental perfume.",
  "description": "<p>Smoky oud over warm amber.</p>",
  "brand": "Acme",
  "category": {
    "id": "cm1…",
    "code": "edp",
    "name": "Eau de Parfum",
    "path": "Fragrance > Eau de Parfum",
    "pathCodes": [
      "fragrance",
      "edp"
    ]
  },
  "gtin": "6291041500213",
  "mpn": null,
  "hsCode": "3303.00",
  "countryOfOrigin": "AE",
  "price": 349,
  "compareAtPrice": null,
  "costPrice": 120,
  "currency": "AED",
  "prices": [
    {
      "currency": "SAR",
      "amount": 359,
      "compareAt": null
    }
  ],
  "stock": 42,
  "weight": 0.45,
  "weightUnit": "kg",
  "dimensions": {
    "length": 8,
    "width": 5,
    "height": 14,
    "unit": "cm"
  },
  "bulletPoints": [
    "Long-lasting eau de parfum",
    "Notes of oud and amber"
  ],
  "tags": [
    "bestseller"
  ],
  "seo": {
    "title": "Oud Noir EDP 100 ml",
    "description": "",
    "keywords": "oud, perfume"
  },
  "attributes": {
    "volume_ml": 100,
    "fragrance_family": "woody-oriental"
  },
  "translations": {
    "ar": {
      "name": "عطر عود نوار ١٠٠ مل",
      "shortDescription": "عطر شرقي فاخر بنفحات العود والعنبر."
    }
  },
  "images": [
    {
      "assetId": "cm1…",
      "url": "https://library.retailcommerceai.com/files/…/oud-noir.jpg",
      "thumbUrl": "https://library.retailcommerceai.com/files/…/oud-noir.webp",
      "alt": "Front",
      "position": 0,
      "role": "image",
      "width": 1600,
      "height": 1600,
      "mimeType": "image/jpeg"
    }
  ],
  "variantAxes": [],
  "collections": [
    "summer-edit"
  ],
  "completeness": 92,
  "version": 7,
  "createdAt": "2026-03-03T09:00:00.000Z",
  "updatedAt": "2026-09-24T08:12:45.000Z"
}
  • attributes holds your custom attributes by code — see Custom attributes.
  • translations holds content per extra locale; prices holds prices in extra currencies.
  • images[].url is absolute and publicly fetchable — marketplaces can download it directly.
  • costPrice is internal; public feeds leave it out unless selected. completeness is 0–100 against your workspace's required fields.

Create and update

cURL
bash
curl -X POST "https://library.retailcommerceai.com/api/v1/products" \
  -H "Authorization: Bearer cl_live_YOUR_API_KEY" \
  -H "Idempotency-Key: 8f14e45f-ea3b-4c0f-9b1e-2f3d5c7a9e11" \
  -H "Content-Type: application/json" \
  -d '{
  "sku": "FR-OUD-100",
  "name": "Oud Noir Eau de Parfum 100 ml",
  "brand": "Acme",
  "categoryCode": "edp",
  "price": 349,
  "currency": "AED",
  "attributes": {
    "volume_ml": 100
  },
  "translations": {
    "ar": {
      "name": "عطر عود نوار ١٠٠ مل"
    }
  },
  "images": [
    {
      "url": "https://cdn.example.com/oud-noir-front.jpg",
      "alt": "Front"
    }
  ]
}'

Set the category by categoryId or categoryCode, the parent by parentId or parentSku, and images by assetId (see assets) or url (registered as an asset). PATCH changes only what you send:

FieldPATCH behaviour
attributesMerged into the existing values; null removes one
translationsMerged per locale; "" clears a field, a locale set to null is removed
images, collectionsReplace the whole list
bulletPoints, tags, pricesReplace the whole list
seo, dimensionsMerged per key
everything elseReplaced when present

PUT replaces the product: content you omit — attributes, translations, images, collections included — is cleared;sku, type, the parent, variantAxes, status, currency and units keep their value when omitted. A PUT to an unknown SKU creates the product (201). HTML in descriptions is sanitized on write.

Concurrency: ETag and If-Match

Every product response carries an ETag such as "v7-2f4c…" (the number is the product version). Send it back as If-Match on PUT, PATCH or DELETE and the write only happens if nobody changed the product since — otherwise you get 412 precondition_failed. Alternatively put "expectedVersion": 7 in the body (a mismatch answers 409).

cURL
bash
curl -X PATCH "https://library.retailcommerceai.com/api/v1/products/FR-OUD-100" \
  -H "Authorization: Bearer cl_live_YOUR_API_KEY" \
  -H "If-Match: "v7-2f4c9a1b0d3e5f67"" \
  -H "Content-Type: application/json" \
  -d '{
  "price": 359,
  "stock": 40
}'

Variants

A parent product lists its variantAxes (attribute codes such as color and size); each variant points to its parent with parentSku and sets the axis values. Variants store only what differs: empty fields — name, description, images, price, category … — are inherited from the parent and already resolved in responses. Clearing a variant field ("" or null) makes it inherit again.

  • GET /products/{parent}?include=variants nests the variants in variants.
  • GET /products?include=variants lists top-level products with their variants nested; add top_level=false to list variants as rows too.
  • Deleting a parent deletes its variants. Create parents before their variants (the bulk endpoint keeps item order).

Translations

The default locale lives in the top-level fields; every other workspace locale (Arabic by default) lives in translations.<locale> with name, shortDescription, description, bulletPoints, seoTitle, seoDescription, keywords and localizable attributes. Arabic text is plain UTF-8; render it with dir="rtl".

Pass ?locale=ar on product reads to get a localized view: the content fields show the Arabic values, falling back to the default language field by field, and localeFallbacks lists the fields that fell back — handy for storefronts and to spot missing translations.

cURL
bash
curl "https://library.retailcommerceai.com/api/v1/products/FR-OUD-100?locale=ar" \
  -H "Authorization: Bearer cl_live_YOUR_API_KEY"

Edit the default view, not a localized one

A localized view is read-only: sending it back with PUT, PATCH, POST or in a bulk item answers 422 (its locale is not the default), because saving it would put Arabic text in the default-language fields. Read the product without ?locale= before editing, and write Arabic under translations.ar.

Bulk upserts

POST /products/bulk creates or updates up to 500 products by SKU (mode: create_update, create_only or update_only). Items are saved one by one, so a bad item never blocks the others: the response has a result per item (created, updated, unchanged, skipped or error) and a summary.

cURL
bash
curl -X POST "https://library.retailcommerceai.com/api/v1/products/bulk" \
  -H "Authorization: Bearer cl_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "mode": "create_update",
  "items": [
    {
      "sku": "AP-LRS",
      "type": "parent",
      "name": "Linen Relaxed Shirt",
      "price": 189,
      "currency": "AED",
      "variantAxes": [
        "color",
        "size"
      ]
    },
    {
      "sku": "AP-LRS-WHT-S",
      "parentSku": "AP-LRS",
      "type": "variant",
      "attributes": {
        "color": "white",
        "size": "s"
      }
    },
    {
      "sku": "AP-LRS-WHT-M",
      "parentSku": "AP-LRS",
      "type": "variant",
      "attributes": {
        "color": "white",
        "size": "m"
      },
      "stock": 12
    }
  ]
}'
200 OK
json
{
  "data": {
    "summary": {
      "total": 3,
      "created": 2,
      "updated": 1,
      "unchanged": 0,
      "skipped": 0,
      "failed": 0
    },
    "results": [
      {
        "index": 0,
        "sku": "AP-LRS",
        "status": "updated",
        "id": "cm1…",
        "version": 4,
        "error": null
      },
      {
        "index": 1,
        "sku": "AP-LRS-WHT-S",
        "status": "created",
        "id": "cm1…",
        "version": 1,
        "error": null
      },
      {
        "index": 2,
        "sku": "AP-LRS-WHT-M",
        "status": "created",
        "id": "cm1…",
        "version": 1,
        "error": null
      }
    ]
  }
}

Idempotency

POST endpoints accept an Idempotency-Key header (any unique string, such as a UUID). If a network error leaves you unsure whether a request went through, repeat it with the same key and body: you get the original response back with Idempotent-Replayed: true instead of a second product.

Limitation

Replays are remembered for 24 hours in the memory of the server instance that handled the first request (up to 1 MB per response). On a serverless or multi-instance deployment a retry can land on another instance and run again. For critical writes prefer calls that are safe to repeat anyway — PUT /products/{sku} and bulk upserts by SKU.

Versions

Every change creates a version. GET /products/{sku}/versions lists them (source, actor, changed fields) and GET /products/{sku}/versions/{n} returns the product as it was.