Products Guide - Styles, Variants & SKUs

How to create, read, update, and archive products through the Retraced public API. Everything goes through one unified resource - /styles - which serves all three levels of the product hierarchy. If you only read one section, read The Mental Model: once you understand how the API detects the product level and how field inheritance works, every endpoint behaves predictably.

Setup

BASE_URL="https://publicapi.retraced.com/api/v2"
API_KEY="your-api-key"        # provided by Retraced; wrap in single quotes if it contains $

Requests are authenticated with the API key header:

curl "$BASE_URL/styles?level=style" \
  -H "companyapikey: $API_KEY"

Important: If your API key contains $ characters, use single quotes around its value to prevent the shell from interpreting it as a variable:

-H 'companyapikey: $2b$10$...'   # correct - single quotes
-H "companyapikey: $2b$10$..."   # wrong - shell expands $2b to empty string → 401

All responses use the standard envelope:

{
  "metadata": { "success": true },
  "data": { }
}

Errors come back in the same envelope with metadata.success: false plus a machine-readable code:

{
  "metadata": { "success": false },
  "statusCode": 400,
  "code": "mixed_levels",
  "message": "All IDs must be the same product level. Found mixed levels: style, variant"
}

The Mental Model

One endpoint, three levels

Products form a 3-level hierarchy:

Style    "Summer Dress"            ── the product definition
  └── Variant  "Summer Dress - Blue"     ── a colorway
        └── SKU  "Summer Dress - Blue - M"     ── a sellable size/dimension

There are no separate /variants or /skus endpoints. All three levels live under /styles, and the API determines the level for you:

Operation How the level is determined
POST /styles By parentId: omitted → style; a style ID → variant; a variant ID → SKU; a SKU ID → 400 invalid_parent_level
GET /styles/:id, PUT /styles/:id Looked up from the ID - pass any level's ID, the right handler is chosen automatically
GET /styles (list) By the level query parameter (style is the default)
POST /styles/archive, /styles/unarchive From the IDs in the body - all IDs must be the same level (400 mixed_levels otherwise)

Field inheritance: null means "inherit"

For these fields, variants and SKUs inherit the parent's value whenever their own value is null:

description, gender, brands, departments, seasons, collections,
suppliers, mainSupplier, vendors, claimIntentions

Inheritance is resolved at read time - there is no copying. Consequences:

Materials and BOMs follow a different inheritance model (shared links with copy-on-write) - see the Materials Guide and BOM Guide.

Codes identify products across systems

Every product carries one or more codes - external identifiers like ERP numbers or EAN barcodes (codes: [{ "codeId": "ERP_ID", "codeValue": "DRS-001" }]). At least one code is required at every level, and code values must be unique per code type within your catalog. See Code Types.

externalId: address products by your own ID

Codes describe a product; externalId addresses it. Set externalId when you create a product and you can then use your own identifier anywhere this API expects a product ID - no mapping table on your side:

# Create with your own ID
curl -X POST "$BASE/styles" -H "companyapikey: $KEY" -H 'Content-Type: application/json' -d '{
  "name": "Summer Dress 2024",
  "externalId": "DRS-2024-001",
  "styleTypeId": "DRESSES",
  "isComponent": false,
  "codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-2024-001" }]
}'

# Then read, update, archive or reference it by that ID
curl "$BASE/styles/DRS-2024-001" -H "companyapikey: $KEY"

# ...including as a parent
curl -X POST "$BASE/styles" -H "companyapikey: $KEY" -H 'Content-Type: application/json' -d '{
  "name": "Summer Dress 2024 - Blue",
  "parentId": "DRS-2024-001",
  "externalId": "DRS-2024-001-BLUE",
  "color": "Blue",
  "codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-2024-001-BLUE" }]
}'

Rules:


Endpoint Reference

Endpoint Purpose
GET /styles?level=... List products at one hierarchy level
GET /styles/:id Get one product (any level, auto-detected)
POST /styles Create a style, variant, or SKU (level from parentId)
PUT /styles/:id Update a product (any level, auto-detected)
POST /styles/archive Archive products (cascades to children)
POST /styles/unarchive Unarchive products (cascade optional)
GET /style-types Look up styleTypeId values (product categorization)
GET /style-properties?type=... Look up brand / department / season / collection IDs
GET /facility-processes Look up facility process IDs (used on orders and supply chains)
GET /production-processes Look up production process IDs (the parent stage of a facility process)

Before You Create: Look Up Reference IDs

Creating a style requires a styleTypeId, and fields like brands take IDs - not names. Fetch the valid IDs first:

# Product type hierarchy → styleTypeId
curl "$BASE_URL/style-types" \
  -H "companyapikey: $API_KEY"

# Brands, departments, seasons, collections → property IDs
curl "$BASE_URL/style-properties?type=brands" \
  -H "companyapikey: $API_KEY"

suppliers, mainSupplier, and vendors take company IDs from your Retraced network.

Two more ID lists are shared platform-wide rather than owned by your account - you'll need them when working with orders and supply chains:

# Facility process IDs → buyerFacilityProcess / supplierFacilityProcess / receiverFacilityProcess on orders
curl "$BASE_URL/facility-processes?limit=100" \
  -H "companyapikey: $API_KEY"

# Production process IDs → the parent stage of a facility process
curl "$BASE_URL/production-processes" \
  -H "companyapikey: $API_KEY"

See Reference data for the full list of ID lookup endpoints.


Creating Products

Create a style (top level)

Required: name, codes, styleTypeId, isComponent. Omit parentId.

curl -X POST "$BASE_URL/styles" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Dress",
    "externalId": "DRS-001",
    "styleTypeId": "YOUR_STYLE_TYPE_ID",
    "isComponent": false,
    "codes": [
      { "codeId": "ERP_ID", "codeValue": "DRS-001" }
    ],
    "description": "Lightweight summer dress in organic cotton",
    "gender": "FEMALE",
    "brands": ["YOUR_BRAND_ID"],
    "seasons": ["YOUR_SEASON_ID"],
    "suppliers": ["SUPPLIER_COMPANY_ID"],
    "mainSupplier": "SUPPLIER_COMPANY_ID",
    "weight": { "value": 250, "unit": "GRAM" }
  }'

Response (201): the created product, including its generated id:

{
  "metadata": { "success": true },
  "data": {
    "id": "style_01KMZMMMW8JA3S1RVX1PS8E19S",
    "externalId": "DRS-001",
    "name": "Summer Dress",
    "codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-001" }],
    "isComponent": false,
    "isArchived": false,
    "createdAt": "2026-06-01T08:00:00.000Z"
  }
}
STYLE_ID="style_01KMZMMMW8JA3S1RVX1PS8E19S"

Because this style was created with "externalId": "DRS-001", every request below could equally use DRS-001 in place of $STYLE_ID - see externalId.

Field notes:

Field Notes
isComponent true marks a component / raw material (e.g. a button) rather than a finished good - components are what you reference in BOM lines
gender Free text (≤ 50 chars); convention: MALE, FEMALE, UNISEX
weight { "value": number, "unit": "GRAM" | "KILOGRAM" | ... } - both parts required; sending either as null stores null
customProperties A JSON string of arbitrary key-value metadata, e.g. "{\"fit\":\"slim\"}"
imagesFileIds File IDs from the Files API - see the File Upload Guide
ecoFootPrint Recyclability and hazardous-substance data (hazardousMaterials, recyclabilityRate, …)
externalId Your own identifier for this product, so you can address it later without storing our ID. Unique per company across all three levels; kept on a PUT that omits it, cleared only by "externalId": null
forCompanyId Create the product on behalf of another company in your network
isStoryActive Enables the public traceability page - requires a supply chain attached to the product

Create a variant

Set parentId to the style ID. Required: name, codes, color.

curl -X POST "$BASE_URL/styles" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Dress - Blue",
    "parentId": "'"$STYLE_ID"'",
    "externalId": "DRS-001-BLU",
    "codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-001-BLU" }],
    "color": "Blue"
  }'

parentId also accepts the parent's externalId, so with the style above you can skip $STYLE_ID entirely and never store a Retraced ID:

curl -X POST "$BASE_URL/styles" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Dress - Red",
    "parentId": "DRS-001",
    "externalId": "DRS-001-RED",
    "codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-001-RED" }],
    "color": "Red"
  }'

Fields you omit (or set null) - description, gender, brands, … - are inherited from the style at read time. Provide values only where the variant genuinely differs.

VARIANT_ID="variant_01KN6YZNVKNBN1TJ4MR9T7FFD0"   # from the response

Create a SKU

Set parentId to the variant ID. Required: name, codes, dimension.

curl -X POST "$BASE_URL/styles" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Dress - Blue - M",
    "parentId": "'"$VARIANT_ID"'",
    "externalId": "DRS-001-BLU-M",
    "codes": [{ "codeId": "EAN_GTIN_13", "codeValue": "4006381333931" }],
    "dimension": "M"
  }'

Set externalId at every level and your whole catalogue stays addressable by your own IDs - GET /styles/DRS-001-BLU-M, GET /bom-headers?styleId=DRS-001, POST /orders with lines[].styleId = "DRS-001-BLU-M".

Required fields by level

Level parentId Required fields Level-specific field
Style (omitted) name, codes, styleTypeId, isComponent -
Variant a style ID or its externalId name, codes, color color
SKU a variant ID or its externalId name, codes, dimension dimension

externalId is optional at every level. Reusing one that another of your products already holds fails with 409 external_id_already_exists.

Missing styleTypeId or isComponent on a style fails with 400 missing_style_type_id / 400 missing_is_component. An unknown parentId fails with 404 parent_not_found; a SKU parentId with 400 invalid_parent_level.


Reading Products

Get by ID (any level)

curl "$BASE_URL/styles/$VARIANT_ID" \
  -H "companyapikey: $API_KEY"

The level is auto-detected; inherited fields come back resolved (the variant's description shows the style's text if not overridden). Unknown IDs return 404 not_found.

List by level

# All styles (level=style is the default)
curl "$BASE_URL/styles?level=style&page=1&limit=50" \
  -H "companyapikey: $API_KEY"

# Variants of one style
curl "$BASE_URL/styles?level=variant&styleId=$STYLE_ID" \
  -H "companyapikey: $API_KEY"

# SKUs of one variant
curl "$BASE_URL/styles?level=sku&variantId=$VARIANT_ID" \
  -H "companyapikey: $API_KEY"

List responses are paginated:

{
  "metadata": {
    "success": true,
    "pagination": { "page": 1, "limit": 50, "total": 7, "totalPages": 1 }
  },
  "data": [ { "id": "style_01KMZ...", "name": "Summer Dress", "...": "..." } ]
}

Query parameters:

Parameter Type Default Description
level string style style, variant, sku
page number 1 1-based
limit number 20 Max 100
sort string createdAt createdAt, updatedAt, name, gender, typeName, color, styleId, dimension, variantId
order string desc asc, desc
name string - Substring match on name
nameOrCode string - Substring match (case-insensitive) on the name or any code value - ABC also matches ABCD. Works at every level, e.g. with level=sku to look up a SKU by its code
codeId string - Filter by code type/ID, e.g. codeId=ERP_ID
codeValue string - Filter by code value, e.g. codeValue=DRS-001. Combine: ?codeId=ERP_ID&codeValue=DRS-001
description string - Substring match
gender string - e.g. FEMALE
styleTypeId, styleLevelId string - Exact match
brands, departments, seasons, collections, suppliers, vendors string - Comma-separated ID lists
mainSupplier, companyId string - Exact match
isComponent, isStoryActive, createdForMe boolean - true or false - see note below
isArchived boolean false true returns archived products only; false (the default) non-archived only
updatedAfterUnixMs, updatedBeforeUnixMs number - Updated-time window (Unix ms) - useful for incremental syncs
styleId string - Parent filter when level=variant
color string - When level=variant
variantId string - Parent filter when level=sku
dimension string - When level=sku
include string - Comma-separated related data - see below

Boolean filters accept true or false. Pass isComponent=false / isStoryActive=false to filter for the false value, or omit the parameter to skip the filter. Any other value (e.g. 1, yes) is rejected with a 400.

include pulls related records into each list/get result. Availability differs by level:

Value style variant sku What you get
type The style type (categorization)
material Material composition headers
bom BOM headers
styleProperties Resolved brand / department / season / collection objects
tracingStory The active supply-chain story attached to the product
images - Product images on list queries (not available at style level - see note below)
style - The parent style
variant - - The parent variant
skuCount - - Number of SKUs under the variant
styleLink - - Linked styles

Images: include=images only affects list queries, and only at variant/SKU level - style lists silently ignore it. You still get images for styles two ways: every list result carries featureImageUrl (the product's first image), and GET /styles/{id} returns the full images array at every level without any include.

curl "$BASE_URL/styles?level=sku&variantId=$VARIANT_ID&include=images,style,tracingStory" \
  -H "companyapikey: $API_KEY"

Updating Products

PUT /styles/:id works on any level (auto-detected). Two rules matter:

  1. codes is replaced wholesale. The codes array you send becomes the product's complete set of codes - send the full desired list every time, not just additions.
  2. null re-enables inheritance on inheritable fields (variants/SKUs); omitting a field leaves it unchanged.
# Update the style's description and codes
curl -X PUT "$BASE_URL/styles/$STYLE_ID" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Dress",
    "description": "Updated description",
    "codes": [
      { "codeId": "ERP_ID", "codeValue": "DRS-001" },
      { "codeId": "PLM_ID", "codeValue": "PLM-9999" }
    ]
  }'

Override and re-inherit

# Variant: override brands (stop inheriting from the style)
curl -X PUT "$BASE_URL/styles/$VARIANT_ID" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Dress - Blue",
    "brands": ["DIFFERENT_BRAND_ID"],
    "codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-001-BLU" }]
  }'

# Variant: reset to inherit brands from the style again
curl -X PUT "$BASE_URL/styles/$VARIANT_ID" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Dress - Blue",
    "brands": null,
    "codes": [{ "codeId": "ERP_ID", "codeValue": "DRS-001-BLU" }]
  }'

parentId and isComponent cannot be changed after creation (they are not part of the update body). styleLevelId can be set on update.


Archiving & Unarchiving

Both endpoints take up to 100 IDs, all of the same level, and return 204 No Content.

# Archive a style - ALWAYS cascades to its variants and SKUs
curl -X POST "$BASE_URL/styles/archive" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["'"$STYLE_ID"'"] }'

# Unarchive the style and all its children (default)
curl -X POST "$BASE_URL/styles/unarchive" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["'"$STYLE_ID"'"], "shouldUnarchiveChildren": true }'

# Unarchive only the style - children stay archived
curl -X POST "$BASE_URL/styles/unarchive" \
  -H "companyapikey: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["'"$STYLE_ID"'"], "shouldUnarchiveChildren": false }'

Full Lifecycle Script

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://publicapi.retraced.com/api/v2"
API_KEY="your-api-key"
STYLE_TYPE_ID="your-style-type-id"   # from GET /style-types

api_post() { curl -s -X POST "$BASE_URL$1" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" -d "$2"; }
api_put()  { curl -s -X PUT  "$BASE_URL$1" -H "companyapikey: $API_KEY" -H "Content-Type: application/json" -d "$2"; }
api_get()  { curl -s "$BASE_URL$1" -H "companyapikey: $API_KEY"; }
extract_id() { echo "$1" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4; }

echo "=== Step 1: Create Style ==="
STYLE_RESP=$(api_post "/styles" '{
  "name": "Demo Jacket",
  "styleTypeId": "'"$STYLE_TYPE_ID"'",
  "isComponent": false,
  "codes": [{"codeId": "ERP_ID", "codeValue": "JKT-001"}],
  "gender": "UNISEX",
  "weight": {"value": 500, "unit": "GRAM"}
}')
STYLE_ID=$(extract_id "$STYLE_RESP")
echo "Created style: $STYLE_ID"

echo "=== Step 2: Create Variant (Red) ==="
VARIANT_RESP=$(api_post "/styles" '{
  "name": "Demo Jacket - Red",
  "parentId": "'"$STYLE_ID"'",
  "codes": [{"codeId": "ERP_ID", "codeValue": "JKT-001-RED"}],
  "color": "Red"
}')
VARIANT_ID=$(extract_id "$VARIANT_RESP")
echo "Created variant: $VARIANT_ID"

echo "=== Step 3: Create SKUs (S, M, L) ==="
for SIZE in S M L; do
  SKU_RESP=$(api_post "/styles" '{
    "name": "Demo Jacket - Red - '"$SIZE"'",
    "parentId": "'"$VARIANT_ID"'",
    "codes": [{"codeId": "ERP_ID", "codeValue": "JKT-001-RED-'"$SIZE"'"}],
    "dimension": "'"$SIZE"'"
  }')
  echo "Created SKU ($SIZE): $(extract_id "$SKU_RESP")"
done

echo "=== Step 4: Update Style ==="
api_put "/styles/$STYLE_ID" '{
  "name": "Demo Jacket (Updated)",
  "description": "Premium demo jacket for integration testing",
  "codes": [{"codeId": "ERP_ID", "codeValue": "JKT-001"}]
}' > /dev/null

echo "=== Step 5: Read back ==="
api_get "/styles/$STYLE_ID" | python3 -m json.tool
api_get "/styles?level=variant&styleId=$STYLE_ID" | python3 -m json.tool
api_get "/styles?level=sku&variantId=$VARIANT_ID" | python3 -m json.tool

echo "=== Step 6: Archive (cascades to all children) ==="
api_post "/styles/archive" '{"ids": ["'"$STYLE_ID"'"]}' > /dev/null

echo "=== Step 7: Unarchive everything ==="
api_post "/styles/unarchive" '{"ids": ["'"$STYLE_ID"'"], "shouldUnarchiveChildren": true}' > /dev/null

echo "=== Done ==="

Code Types

Single-entry (max one per product):

Code ID Meaning
ERP_ID ERP system identifier
PLM_ID Product Lifecycle Management ID
MAIN_SUPPLIERS_ID Main supplier's internal ID
PIM_ID Product Information Management ID
UPC_GTIN_12 12-digit barcode
EAN_GTIN_13 13-digit barcode
ZALANDO_ID Zalando marketplace ID
FASHIONCLOUD_ID FashionCloud ID

Multi-entry (several per product, with an explicit numeric suffix you assign):

Code ID Example
ALTERNATIVE_SUPPLIERS_ID<N> ALTERNATIVE_SUPPLIERS_ID1, ALTERNATIVE_SUPPLIERS_ID2, …
OTHER_ID<N> OTHER_ID1, OTHER_ID2, …

Rules:


Error Reference

Status / code Where Meaning
401 token_or_api_key_invalid All endpoints Missing or invalid companyapikey
404 parent_not_found POST /styles parentId doesn't exist in your catalog
400 invalid_parent_level POST /styles parentId is a SKU - SKUs cannot have children
400 missing_style_type_id POST /styles Creating a style without styleTypeId
400 missing_is_component POST /styles Creating a style without isComponent
400 invalid_code_ids Create & update Unknown code ID (check spelling and multi-entry suffix)
400 codes_not_unique Create & update A code value is already used by another product
404 not_found GET/PUT /styles/:id Product ID doesn't exist
404 not_found Archive / unarchive None of the provided IDs were found
400 mixed_levels Archive / unarchive IDs span multiple product levels
409 external_id_already_exists POST/PUT /styles externalId is already used by another of your products

Field limits: name ≤ 255 characters, description ≤ 4000, gender ≤ 50, color / dimension ≤ 255.


Gotchas Checklist

  1. PUT replaces codes wholesale. Always send the complete desired codes array - sending only the new code deletes the others.
  2. null ≠ omitted in updates. Omitting a field keeps its value; sending null clears it - and on variants/SKUs that means re-inherit from the parent.
  3. Inheritance is live. A variant that never overrode description reflects style edits immediately. Check overriddenFlag on variant/SKU responses to see what is overridden.
  4. Archive always cascades; unarchive is optional. shouldUnarchiveChildren only affects /styles/unarchive.
  5. Boolean query filters accept only true or false (isComponent, isStoryActive, isArchived, createdForMe) - any other value is rejected with a 400. Omit the parameter to skip the filter.
  6. styleTypeId and isComponent are required for styles only - and isComponent cannot be changed later.
  7. Multi-entry code IDs need a numeric suffix (OTHER_ID1, not OTHER_ID).
  8. weight is all-or-nothing - value without unit (or vice versa) is stored as null.
  9. include=images works on variant/SKU lists only - style lists silently ignore it. Use featureImageUrl (present on every list result) or GET /styles/{id}, which returns the full images array at every level without any include.
  10. List limit is capped at 100; archive/unarchive bodies at 100 IDs.
  11. Use updatedAfterUnixMs for incremental syncs instead of re-pulling the whole catalog.
  12. externalId survives a PUT that omits it - unlike every other field on that endpoint. Clear it deliberately with "externalId": null.