Products, variants & translations
Products are the heart of the API. Every endpoint, export, feed, webhook and channel works with the same CanonicalProduct shape.
The product shape
{
"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"
}attributesholds your custom attributes by code — see Custom attributes.translationsholds content per extra locale;pricesholds prices in extra currencies.images[].urlis absolute and publicly fetchable — marketplaces can download it directly.costPriceis internal; public feeds leave it out unless selected.completenessis 0–100 against your workspace's required fields.
Create and update
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:
| Field | PATCH behaviour |
|---|---|
attributes | Merged into the existing values; null removes one |
translations | Merged per locale; "" clears a field, a locale set to null is removed |
images, collections | Replace the whole list |
bulletPoints, tags, prices | Replace the whole list |
seo, dimensions | Merged per key |
everything else | Replaced 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 -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=variantsnests the variants invariants.GET /products?include=variantslists top-level products with their variants nested; addtop_level=falseto 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 "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
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 -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
}
]
}'{
"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
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.