API ReferenceEndpointsProductsCreate a new product

Create a new product

Supports Idempotency-Key header for safe retries.

POST
/v1/products
AuthorizationBearer <token>

Admin API key (server-to-server). Issue via the dashboard at Settings → API keys. Plain-text key is shown once — store in a secret manager. Format: Authorization: Bearer brainerce_xxxxxxxxx.

In: header

Header Parameters

Idempotency-Key?string

Client-supplied key (a UUID v4 is the recommended form, max 255 characters) that makes this mutation safe to retry.

  • Replay window: 24 hours. The first response for a key is cached; a retry inside the window with the same request body returns the original status and body without re-running the handler.
  • Reusing a key with a different body returns 409 Conflict with code: "IDEMPOTENCY_KEY_REUSED". The request fingerprint (method + path + body hash) is compared against the stored one; a mismatch is refused rather than served the old response.
  • Keys are scoped to the calling credential and store — two API keys may safely use the same key value.
  • Error responses are cached too, so a retry of a request that failed validation returns the same 400 immediately.
  • Sending this header on a GET returns 400 with code: "IDEMPOTENCY_KEY_NOT_SUPPORTED" — GETs are already idempotent.
  • Support is per-route and this parameter is the authoritative signal: a key sent to a route that does not declare it is accepted and silently ignored.

See /docs/api/idempotency.

Lengthlength <= 255
namestring

Display name of the product. Required, 1 to 500 characters.

Lengthlength <= 500
slug?string

URL slug. Letters, numbers, and hyphens only. Auto-generated from name when omitted.

Match^[\p{L}\p{N}-]+$
Lengthlength <= 500
sku?string

Stock keeping unit, meaning your internal product code.

Lengthlength <= 191
gtin?string

Global Trade Item Number (universal barcode: EAN/UPC/ISBN). Required by Google Shopping, Facebook Catalog, and TikTok Shop on most categories. Validated against the checksum digit.

mpn?string

Manufacturer Part Number, paired with brand when no GTIN exists. Max 70 chars.

Lengthlength <= 70
externalId?string

Your own stable identifier for this product in the SOURCE system (supplier feed, legacy store, ERP). Unique per store. Its purpose is safe retries on bulk imports: re-sending a batch after a timeout matches on this value and skips the product instead of creating a duplicate. Not a marketplace id: those are managed per-connection and are not settable here.

Lengthlength <= 191
salePriceStartsAt?string

Sale-price effective window start (ISO 8601). Enforced: before the start and after salePriceEndsAt the storefront, cart and checkout use the regular price. Variants without dates of their own follow this window. Also the Google Merchant Center sale_price_effective_date. Both this and salePriceEndsAt must be set together, or neither applies.

salePriceEndsAt?string

Sale-price effective window end (ISO 8601). See salePriceStartsAt.

shippingWeightValue?number

Shipping weight numeric value. Paired with shippingWeightUnit.

Range0 <= value
shippingWeightUnit?string

Shipping weight unit.

Value in"kg" | "lb" | "g" | "oz"
shippingLengthValue?number

Shipping package length. Paired with shippingDimensionUnit.

Range0 <= value
shippingWidthValue?number

Shipping package width. Paired with shippingDimensionUnit.

Range0 <= value
shippingHeightValue?number

Shipping package height. Paired with shippingDimensionUnit.

Range0 <= value
shippingDimensionUnit?string

Unit shared by shippingLengthValue/shippingWidthValue/shippingHeightValue.

Value in"cm" | "in"
metaDescription?string

SEO meta description (≤160 chars). Used by Google Search snippets, social link previews (WhatsApp/Telegram/Twitter), email previews, and schema.org JSON-LD. Falls back to a stripped/truncated description when omitted.

Lengthlength <= 160
description?string

Long-form product description. Accepts HTML, and the backend strips dangerous tags/attributes (<script>, inline event handlers, <iframe>, …) before saving. Hard cap of 65 535 characters.

Lengthlength <= 65535
descriptionFormat?string

How description is written. Omit it and the platform decides: a description that contains real HTML tags is stored as html, anything else keeps its current format (text for a new product). Send it to override that, for example text for a description that only quotes a tag. A description marked html is sanitized on write and rendered as HTML by the storefront; the SEO audit does not penalize it.

Value in"text" | "html" | "markdown"
basePricenumber

Base list price in the store currency. Must be ≥ 0.

Range0 <= value
salePrice?number

Sale price: greater than 0 and lower than basePrice, otherwise the request is refused with a 400 naming the field (a sale price is never silently dropped). When present, the storefront displays it as the active price with basePrice shown struck-through. Omit it for a product that is not on sale.

Range0 <= value
costPrice?number

Internal cost (what you pay your supplier). Used for margin reporting only, and never shown to shoppers.

Range0 <= value
status?string

Publish state. active = visible to shoppers; draft = only visible in the dashboard.

Value in"active" | "draft"
typestring

Product structure. SIMPLE = single SKU; VARIABLE = parent of multiple variants (size, color, …); KIT = a bundle priced from component products. A KIT may be created with no components yet — it is simply not purchasable until it has at least one.

Value in"SIMPLE" | "VARIABLE" | "KIT"
kitPricingMode?string

How a KIT is priced. FIXED (default) — you set basePrice and it stays put. SUM — the kit costs exactly what its components cost, recomputed on every read, so a component going on sale lowers the kit price on its own. SUM_MINUS_PERCENT — that sum less kitDiscountValue percent. Ignored on SIMPLE and VARIABLE products.

Default"FIXED"
Value in"FIXED" | "SUM" | "SUM_MINUS_PERCENT"
kitDiscountValue?number

Percent off the component sum, 0-100. Required when kitPricingMode is SUM_MINUS_PERCENT; ignored otherwise.

Range0 <= value <= 100
isDownloadable?boolean

If true, the product is a digital download, so checkout issues a signed URL after payment instead of triggering shipping.

downloadData?object

Digital-download configuration. Required when isDownloadable=true. files[] are uploaded via /v1/media. downloadLimit = max downloads per order (null = unlimited). downloadExpiry = hours until the signed URL expires (null = no expiry).

inventory?object

Inventory configuration for SIMPLE products. total = current stock count; trackingMode: TRACKED (default) decrements on each sale and blocks oversells; UNLIMITED accepts any order; DISABLED hides stock UI. Ignored for VARIABLE products, whose variants carry their own inventory.

variants?array<CreateVariantDto>

Array of variants. Required (and non-empty) for VARIABLE products, at most 500. Each variant carries its own price, SKU, and inventory.

Itemsitems <= 500
categories?array<string>

Array of existing category IDs (from GET /categories / list_categories). Unknown/cross-store IDs are rejected with 400. To assign by name, creating the category if it does not exist, use categoryNames instead.

Itemsitems <= 100
categoryNames?array<string>

Category names to assign. Each name is matched case-insensitively to an existing category and auto-created if none exists, so there is no need to look up IDs first. Merged with any IDs passed in categories.

Itemsitems <= 100
brands?array<string>

Array of existing brand IDs. Unknown/cross-store IDs are rejected with 400. To assign by name, creating the brand if it does not exist, use brandNames instead.

Itemsitems <= 50
brandNames?array<string>

Brand names to assign. Each name is matched case-insensitively to an existing brand and auto-created if none exists. Merged with any IDs passed in brands.

Itemsitems <= 50
tags?array<string>

Free-form tags for filtering and segmentation.

Itemsitems <= 250
images?array<string | object>

Product images. Each entry is an image URL string or an object { url, position, isMain }. External URLs are imported into your media library; to reuse a library asset, send its URL and key from /v1/media.

Itemsitems <= 250
platforms?array<array<unknown>>

Where to publish this product: platform codes of installed connector apps (for example google-merchant-center), or vc:<salesChannelId> for one of your sales channels. Each platform syncs the product separately and can have per-channel overrides (see channels). SHOPIFY and WOOCOMMERCE are retired: they are still accepted and ignored.

Itemsitems <= 50
channels?object

Per-platform field overrides. Keyed by platform code, value is a partial product object that overrides the canonical fields for that channel only.

taxBehavior?string

Tax category. taxable is the default; exempt skips all tax calculation.

Default"taxable"
Value in"taxable" | "exempt"
translations?object

Multi-language overrides. Keyed by ISO locale code (he, ar, es, …). Each value is a partial product with translated name, description, seoTitle, seoDescription and slug. Falls back to the canonical fields for missing locales.

curl -X POST "https://api.brainerce.com/api/v1/products" \  -H "Idempotency-Key: string" \  -H "Content-Type: application/json" \  -d '{    "name": "Premium Wireless Headphones",    "basePrice": 299.99,    "type": "SIMPLE"  }'
{
  "id": "clx1a2b3c4d5e6f7g8h9",
  "name": "Classic T-Shirt",
  "slug": "classic-t-shirt",
  "localeSlugs": {},
  "description": "A soft cotton t-shirt.",
  "descriptionFormat": "text",
  "basePrice": "49.90",
  "salePrice": "24.90",
  "costPrice": "12.00",
  "priceMin": "24.90",
  "priceMax": "34.90",
  "priceVaries": true,
  "displayPrice": "29.90",
  "displaySalePrice": "24.90",
  "displayPriceMin": "24.90",
  "displayPriceMax": "34.90",
  "displayCurrency": "USD",
  "gtin": "0123456789012",
  "mpn": "TS-BLK-M",
  "salePriceStartsAt": "2019-08-24T14:15:22Z",
  "salePriceEndsAt": "2019-08-24T14:15:22Z",
  "shippingWeightValue": 0.3,
  "shippingWeightUnit": "kg",
  "shippingLengthValue": 30,
  "shippingWidthValue": 20,
  "shippingHeightValue": 5,
  "shippingDimensionUnit": "cm",
  "metaDescription": "Soft cotton t-shirt in five colors",
  "status": "active",
  "sku": "TSHIRT-BLK-M",
  "type": "SIMPLE",
  "kitPricingMode": "FIXED",
  "kitDiscountValue": 10,
  "isDownloadable": true,
  "downloadData": {
    "files": [
      {
        "id": "clx1a2b3c4d5e6f7g8h9",
        "name": "photos.zip",
        "url": "https://cdn.example.com/downloads/photos.zip",
        "storageKey": "downloads/photos.zip",
        "size": 5242880,
        "mimeType": "application/zip"
      }
    ],
    "downloadLimit": 3,
    "downloadExpiry": 30
  },
  "needsSync": true,
  "lastSyncedAt": "2019-08-24T14:15:22Z",
  "menuOrder": 0,
  "taxBehavior": "taxable",
  "createdAt": "2019-08-24T14:15:22Z",
  "updatedAt": "2019-08-24T14:15:22Z",
  "inventory": {
    "trackingMode": "TRACKED",
    "total": 42,
    "reserved": 3,
    "available": 39,
    "inStock": true,
    "canPurchase": true,
    "lastInventorySyncAt": "2019-08-24T14:15:22Z"
  },
  "variants": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "productId": "clx1a2b3c4d5e6f7g8h9",
      "sku": "TSHIRT-BLK-M",
      "name": "Black / M",
      "attributes": {
        "size": "S",
        "color": "Red"
      },
      "price": "19.90",
      "salePrice": "24.90",
      "costPrice": "12.00",
      "salePriceStartsAt": "2019-08-24T14:15:22Z",
      "salePriceEndsAt": "2019-08-24T14:15:22Z",
      "displayPrice": "29.90",
      "displaySalePrice": "24.90",
      "displayCurrency": "USD",
      "image": "string",
      "position": 0,
      "status": "active",
      "inventory": {
        "trackingMode": "TRACKED",
        "total": 42,
        "reserved": 3,
        "available": 39,
        "inStock": true,
        "canPurchase": true,
        "lastInventorySyncAt": "2019-08-24T14:15:22Z"
      },
      "isDownloadable": true,
      "downloadData": {
        "files": [
          {
            "id": "clx1a2b3c4d5e6f7g8h9",
            "name": "photos.zip",
            "url": "https://cdn.example.com/downloads/photos.zip",
            "storageKey": "downloads/photos.zip",
            "size": 5242880,
            "mimeType": "application/zip"
          }
        ],
        "downloadLimit": 3,
        "downloadExpiry": 30
      },
      "shippingClassId": "clx1a2b3c4d5e6f7g8h9",
      "createdAt": "2019-08-24T14:15:22Z",
      "updatedAt": "2019-08-24T14:15:22Z"
    }
  ],
  "images": [
    {
      "url": "https://cdn.example.com/products/tshirt.jpg",
      "position": 0,
      "isMain": true,
      "alt": "Black t-shirt on a hanger",
      "id": "clx1a2b3c4d5e6f7g8h9",
      "key": "products/tshirt.jpg",
      "thumbnailUrl": "https://cdn.example.com/products/tshirt-thumb.jpg",
      "width": 1200,
      "height": 800,
      "size": 245123,
      "mimeType": "image/jpeg",
      "createdAt": "2019-08-24T14:15:22Z",
      "source": "local",
      "importStatus": "completed",
      "importError": "HTTP 404 from the source server",
      "src": "https://cdn.example.com/products/tshirt.jpg",
      "altSource": "asset",
      "altTranslations": {},
      "originalFilename": "tshirt.jpg",
      "sourceUrl": "https://source.example.com/img/tshirt.jpg",
      "sourcePlatform": "SHOPIFY",
      "importedAt": "2026-01-15T10:30:00.000Z",
      "platformImageIds": {}
    }
  ],
  "channels": {},
  "translations": {},
  "categories": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "name": "Shoes",
      "taxBehavior": "taxable"
    }
  ],
  "brands": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "name": "Summer Collection"
    }
  ],
  "attributes": [
    "string"
  ],
  "productAttributeOptions": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "attributeId": "clx1a2b3c4d5e6f7g8h9",
      "attributeOptionId": "clx1a2b3c4d5e6f7g8h9",
      "platform": "GLOBAL",
      "attribute": {
        "id": "clx1a2b3c4d5e6f7g8h9",
        "name": "Color",
        "displayType": "COLOR_SWATCH"
      },
      "attributeOption": {
        "id": "clx1a2b3c4d5e6f7g8h9",
        "name": "Black",
        "value": "black",
        "swatchColor": "#000000",
        "swatchColor2": "#FFFFFF",
        "swatchImageUrl": "https://cdn.example.com/swatches/black.png"
      }
    }
  ],
  "tags": [
    "string"
  ],
  "metafields": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "definitionId": "clx1a2b3c4d5e6f7g8h9",
      "definitionKey": "material",
      "definitionName": "Material",
      "type": "TEXT",
      "value": "Cotton",
      "variantId": "clx1a2b3c4d5e6f7g8h9"
    }
  ],
  "customizationFields": [
    {
      "definitionId": "clx1a2b3c4d5e6f7g8h9",
      "name": "Engraving text",
      "key": "engraving",
      "description": "Up to 20 characters",
      "type": "TEXT",
      "required": true,
      "minLength": 1,
      "maxLength": 20,
      "minValue": 0,
      "maxValue": 100,
      "enumValues": [
        "string"
      ],
      "defaultValue": "Happy birthday",
      "position": 0
    }
  ],
  "channelPublishes": [
    {
      "salesChannel": {
        "id": "clx1a2b3c4d5e6f7g8h9",
        "name": "My storefront",
        "connectionId": "vc_9f3a2b1c8d7e6f5a"
      },
      "connection": {
        "id": "clx1a2b3c4d5e6f7g8h9",
        "name": "My storefront",
        "connectionId": "vc_9f3a2b1c8d7e6f5a"
      }
    }
  ],
  "vibeCodedPublishes": [
    {
      "salesChannel": {
        "id": "clx1a2b3c4d5e6f7g8h9",
        "name": "My storefront",
        "connectionId": "vc_9f3a2b1c8d7e6f5a"
      },
      "connection": {
        "id": "clx1a2b3c4d5e6f7g8h9",
        "name": "My storefront",
        "connectionId": "vc_9f3a2b1c8d7e6f5a"
      }
    }
  ],
  "channelSync": {},
  "shippingClassId": "clx1a2b3c4d5e6f7g8h9",
  "shippingClass": {
    "id": "clx1a2b3c4d5e6f7g8h9",
    "name": "Heavy items"
  },
  "crossSells": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "name": "Summer Collection"
    }
  ],
  "upsells": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "name": "Summer Collection"
    }
  ],
  "related": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9",
      "name": "Summer Collection"
    }
  ],
  "bundleOfferCount": 1,
  "orderBumpCount": 1,
  "avgRating": 4.6,
  "reviewCount": 18,
  "seoScore": {
    "score": 82,
    "band": "excellent",
    "hasUnresolvedError": true
  },
  "modifierGroups": [
    {
      "attachmentId": "clx1a2b3c4d5e6f7g8h9",
      "groupId": "clx1a2b3c4d5e6f7g8h9",
      "variantId": "clx1a2b3c4d5e6f7g8h9",
      "position": 0,
      "minOverride": 0,
      "maxOverride": 3,
      "freeQuantityOverride": 1,
      "requiredOverride": true,
      "freeAllocationPolicyOverride": "EXPENSIVE_FREE",
      "defaultModifierIds": [
        "string"
      ],
      "name": "Toppings",
      "internalName": "toppings-pizza",
      "description": "Choose up to 3",
      "translations": {},
      "selectionType": "SINGLE",
      "minSelections": 0,
      "maxSelections": 3,
      "freeQuantity": 1,
      "required": true,
      "freeAllocationPolicy": "EXPENSIVE_FREE",
      "status": "active",
      "modifiers": [
        {
          "id": "clx1a2b3c4d5e6f7g8h9",
          "name": "Extra cheese",
          "description": "Double portion",
          "translations": {},
          "priceDelta": "2.50",
          "sku": "TSHIRT-BLK-M",
          "image": "http://example.com",
          "position": 0,
          "isDefault": true,
          "available": true,
          "referencedProductId": "clx1a2b3c4d5e6f7g8h9",
          "status": "active"
        }
      ]
    }
  ]
}

{
  "statusCode": 400,
  "code": "VALIDATION_FAILED",
  "message": "name should not be empty, price must be a positive number",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products"
}

{
  "statusCode": 401,
  "code": "UNAUTHORIZED",
  "message": "Invalid or expired credential",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products"
}

{
  "statusCode": 403,
  "code": "INSUFFICIENT_SCOPE",
  "message": "API key does not have the required scope: products:write",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products"
}

{
  "statusCode": 429,
  "code": "RATE_LIMITED",
  "message": "Too many requests",
  "details": {
    "retryAfterSeconds": 12
  },
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/store"
}