API ReferenceEndpointsProductsUpdate a product

Update a product

Supports Idempotency-Key header for safe retries.

PATCH
/v1/products/{id}
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

Path Parameters

idstring

Product ID

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
name?string

Display name of the product, 1 to 500 characters.

Lengthlength <= 500
slug?string

URL slug. Letters, numbers, and hyphens only. Changing the slug breaks existing shopper bookmarks for this product.

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). Validated against the checksum digit. Pass an empty string to clear.

mpn?string

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

Lengthlength <= 70
salePriceStartsAt?string

Sale-price effective window start (ISO 8601). Enforced at checkout: outside the window the regular price is charged. Pass null to clear.

salePriceEndsAt?string

Sale-price effective window end (ISO 8601). Pass null to clear.

shippingWeightValue?number

Shipping weight numeric value. Pass null to clear.

Range0 <= value
shippingWeightUnit?string

Shipping weight unit.

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

Shipping package length. Pass null to clear.

Range0 <= value
shippingWidthValue?number

Shipping package width. Pass null to clear.

Range0 <= value
shippingHeightValue?number

Shipping package height. Pass null to clear.

Range0 <= value
shippingDimensionUnit?string

Unit shared by the dimension fields.

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

SEO meta description (≤160 chars). Used by Google Search snippets and social link previews. 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"
basePrice?number

Base list price in the store currency.

Range0 <= value
salePrice?number | null

Sale price: greater than 0 and lower than basePrice (the one sent with it, or the stored one), otherwise the request is refused with a 400 naming the field. 0 is not a sale price. When present, the storefront displays it as the active price with basePrice shown struck-through. Pass null to take the product OFF sale. Omitting the field leaves the existing sale price untouched, which is not the same thing (D76: the dashboard form sent undefined for an emptied box, so a sale could be started and never ended).

Range0 <= value
costPrice?number | null

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

status?string

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

Value in"active" | "draft"
type?string

Product structure. SIMPLE = single SKU; VARIABLE = parent of multiple variants.

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.

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.

needsSync?boolean

When true, the product is flagged for re-sync to connected platforms on the next sync job.

channels?object

Per-platform field overrides. Keyed by platform code of an installed connector app (for example TIKTOK) or by sales-channel ID (for example vc_abc123); value is a partial product object that overrides the canonical fields for that channel only.

tags?array<string>

Free-form tags for filtering and segmentation.

Itemsitems <= 250
categories?array<string>

Existing category IDs to assign (replaces the current set). Pass [] to remove all categories. Unknown/cross-store IDs are rejected with 400. To assign by name, creating the category if missing, use categoryNames.

Itemsitems <= 100
categoryNames?array<string>

Category names to assign (case-insensitive match, auto-created if missing). Additive: names are ADDED to the product, joining the set in categories when that is also sent, otherwise to the product's current categories (they are not replaced).

Itemsitems <= 100
brands?array<string>

Existing brand IDs to assign (replaces the current set). Pass [] to remove all brands. Unknown/cross-store IDs are rejected with 400. To assign by name, creating the brand if missing, use brandNames.

Itemsitems <= 50
brandNames?array<string>

Brand names to assign (case-insensitive match, auto-created if missing). Additive: names are ADDED to the set in brands when that is also sent, otherwise to the product's current brands (they are not replaced).

Itemsitems <= 50
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
menuOrder?number

Display order within the catalog/category listing. Lower values render first. Mirrors WooCommerce menu_order.

shippingClassId?string | null

ID of the shipping class this product belongs to (used by shipping-rate rules). Pass null to clear.

taxClassId?string | null

ID of the tax class this product belongs to (drives differential tax rates at checkout). Pass null to clear (falls back to category/store-default/Standard).

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).

taxBehavior?string

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

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.

curl -X PATCH "https://api.brainerce.com/api/v1/products/string" \  -H "Idempotency-Key: string" \  -H "Content-Type: application/json" \  -d '{}'
{
  "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/{id}"
}

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

{
  "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/{id}"
}

{
  "statusCode": 404,
  "code": "RESOURCE_NOT_FOUND",
  "message": "Resource not found",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products/{id}"
}

{
  "statusCode": 409,
  "code": "CONFLICT",
  "message": "The request conflicts with the current state of the resource",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products/{id}"
}

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