production writes → app.brandsgateway.com Examples on this page target the live marketplace.
BG Vendor Integration
Menu
Step 2

Product contract

One shape, two variants. Everything on this page is enforced — by the JSON Schema at the bottom, by business rules the schema cannot express, or by BrandsGateway itself on write.

The endpoint

upsert
PUT https://vendors.brandsgateway.com/api/v1/products/{yourProductId}

Authorization: Basic <base64(username:application-password)>
X-Vendor-Id:   <your vendor id>
Content-Type:  application/json

{yourProductId} is your identifier — whatever your own system calls this product. Send the same value in meta_data._external_id; the path finds the product, the meta value keeps the link. If the two disagree the request is refused, because guessing which one you meant is how catalogs get duplicated.

One call does both
There is no separate create. The first call for an unseen _external_id returns 201 Created; every call after returns 200 OK and updates in place.

Required on every product

These fields apply whether the product is simple or variable. Fields marked Recommended are not schema-blocking, but products missing them are routinely pulled during catalog review.

type
"simple" | "variable"
Required

Whether the product is one sellable unit or a parent with sized children. Decides which other fields are required.

If it is sold in more than one size it is variable, even when only one size is currently in stock.

name
string
Required

The customer-facing title. Shown in the shop and in search.

Must be English. Vendor titles in Italian or French are a common cause of listings being pulled during review.

sku
string
Required

Your product-level SKU as it appears on BrandsGateway. Must be unique across your catalog.

A collision with an existing SKU fails as woocommerce_rest_product_not_created, which reads like a server error but is not.

fails as woocommerce_rest_product_not_created →
brands
[{ id: integer }]
Required

The designer or label, as a BrandsGateway brand term ID. Exactly one entry.

Brands are curated. A brand that is not already on the platform cannot be created from a feed — request it first.

fails as missing_brand →
categories
[{ id: integer }]
Required

The browsable category tree the product appears under. Usually a parent and a leaf.

Categories drive navigation, not filtering. Filtering comes from groups — send both.

fails as missing_category →
groups
[{ id: integer }]
Required

The merchandising hierarchy. Drives filters, the size axis, and harmonised-code inheritance for customs.

Send the full ancestor chain with the leaf last, e.g. Clothing → Sweaters → Sweatshirts. A container group alone has no size axis.

fails as missing_group_mapping →
genders
[{ id: integer }]
Required

Men (71575), Women (71576) or Unisex (71577). Three fixed terms.

conditions
[{ id: integer }]
Required

Stock condition. Determines whether the item can be returned, so it is never inferred.

New-season stock is New with tags (71354).

attributes
[{ id, variation, options }]
Required

The attribute axes. Colour (id 14) with variation:false, plus — for variable products — the size attribute with variation:true and every size in options.

options holds display labels; variation attributes reference term slugs. They are not interchangeable.

fails as missing_size_mapping →
color
string
Recommended

The mapped BrandsGateway colour name, mirrored from the colour attribute.

fails as missing_color →
images
[{ src: uri }]
Required

Absolute, publicly reachable image URLs. First is the main image; the rest form the gallery.

Only uploaded on create, or on update with force_images=true. Minimum two in practice.

fails as images_below_two →
short_description
string
Recommended

The product copy shown on the listing. Plain text or light HTML.

materials
string
Recommended

Mapped BrandsGateway material name(s). Mirror it into meta key _bgmaterial.

fails as missing_material →
meta_data
[{ key, value }]
Required

The key/value bag carrying identity, origin, cost and traceability. See the meta data reference.

An array of objects, not a plain object. Values must be string, number or boolean — never nested.

fails as missing_meta_key →
vendor_id
string
Recommended

Your vendor ID, echoed in the body. The path parameter is authoritative; this is for your own traceability.

raw_name
string
Recommended

Your original product title, before any renaming. Kept so the mapped `name` can be traced back.

raw_short_description
string
Recommended

Your original description, before any rewriting.

raw_material
string
Recommended

Your original composition string, e.g. "KASHMIR/CAPRA HIRCUS", before it was mapped to a BrandsGateway material.

Send the raw string exactly as it appears in your feed. It is the evidence for the `materials` mapping, not a duplicate of it.

raw_images
string[]
Recommended

Your origin image URLs as plain strings, before BrandsGateway proxies them for resizing.

A flat array of strings — unlike `images`, which is an array of `{ src }` objects.

Additionally, for simple products

A simple product is one sellable unit: one barcode, one price, one stock number, all at the top level.

regular_price
string
Simple only

The retail (RRP) price the item sells for at full price.

A string with at most two decimals: "540.00", never 540.

fails as invalid_price_format →
sale_price
string
Simple only

The price BrandsGateway sells at. Must be strictly below regular_price.

fails as invalid_price_ratio →
stock_quantity
integer
Simple only

Units available. Your feed is authoritative — each sync overwrites this value.

manage_stock
boolean
Simple only

Always true. Without it BrandsGateway cannot track depletion and the item oversells.

global_unique_id
string
Simple only

The EAN/barcode. Unique across the entire marketplace, not just your catalog.

A conflict means another vendor already lists this exact item. That is resolved commercially, not by retrying.

fails as product_invalid_global_unique_id →
complete simple product
{
  "type": "simple",
  "name": "Black Leather Crossbody Bag",
  "sku": "MM4820113077BB",
  "brands":     [{ "id": 71988 }],
  "categories": [{ "id": 19 }, { "id": 31 }],
  "groups":     [{ "id": 71652 }, { "id": 71690 }],
  "genders":    [{ "id": 71576 }],
  "conditions": [{ "id": 71354 }],
  "color": "Black",
  "attributes": [
    { "id": 14, "variation": false, "options": ["Black"] }
  ],
  "images": [
    { "src": "https://cdn.example.com/4820113077_1.jpg" },
    { "src": "https://cdn.example.com/4820113077_2.jpg" }
  ],
  "short_description": "Grained calfskin crossbody with an adjustable strap.",
  "materials": "Calf Leather",

  "regular_price":    "890.00",
  "sale_price":       "445.00",
  "stock_quantity":   3,
  "manage_stock":     true,
  "global_unique_id": "8054802817421",

  "meta_data": [
    { "key": "_external_id",     "value": "4820113077" },
    { "key": "_bgorigincountry", "value": "IT" },
    { "key": "_vendor_cost",     "value": "356.00" },
    { "key": "_vendor_sku",      "value": "MM-4820-113077" },
    { "key": "_bgmaterial",      "value": "Calf Leather" },
    { "key": "_import_source",   "value": "your-vendor-slug" }
  ]
}

Additionally, for variable products

A variable product carries no price or stock of its own — those live on each variation. The parent holds identity, imagery, taxonomy and the attribute axes.

variations
[Variation]
Variable only

One entry per sellable size, each with its own barcode, prices, stock and meta.

A variation whose sale price is not below retail is dropped. If every variation is dropped, the whole product is skipped.

fails as no_valid_variations →

Inside a variation

sku
string
Required

SKU for this specific size. Unique within your catalog.

regular_price
string
Required

Retail price for this size. Sizes may legitimately differ in price.

fails as invalid_price_format →
sale_price
string
Required

BrandsGateway price for this size. Strictly below this variation's regular_price.

fails as invalid_price_ratio →
stock_quantity
integer
Required

Units of this size. Zero is valid and means the size is temporarily unavailable.

global_unique_id
string
Required

The barcode for this size. Every size has its own — they are different sellable units.

Reusing the parent's barcode across sizes is rejected, and is the single most common variable-product mistake.

fails as duplicate_ean_within_product →
attributes
{ [taxonomy_slug]: term_slug }
Required

Which size this is, as an object keyed by attribute slug, e.g. { "pa_italian-size-women": "m" }.

Term slugs, not display labels. The parent's options array carries the labels.

fails as missing_size_mapping →
meta_data
[{ key, value }]
Required

Must contain _variation_external_id, _vendor_cost and _vendor_sku.

fails as missing_meta_key →
variable_global_unique_id
string
Optional

A mirror of this variation's `global_unique_id`, read by some legacy consumers. The pipeline sends both.

weight
number
Optional

Shipping weight in kilograms. Improves carrier rate accuracy.

Accepted, never required

These are stored if you send them and ignored if you do not. Most are produced by our own importer and echoed back on pipeline-managed products — they are listed so that nothing you see in a real payload is unexplained.

groupMappingMatch
object
Optional

How your category string was classified into BrandsGateway groups. Produced by our classifier and echoed back for audit — you supply `groups` directly and can omit this.

Present in pipeline-managed products. Sending it yourself changes nothing; `groups` is what the platform reads.

slug
string
Optional

Send `""` to make BrandsGateway regenerate the URL slug from the product name. Omit it to leave an existing slug untouched.

This is behavioural, not cosmetic. If you rename a product and do not send an empty slug, the old name stays in its URL forever.

The two axes are not the same shape
The parent's attributes[].options holds display labels ("M"). Each variation's attributes object holds term slugs keyed by taxonomy slug ({ "pa_italian-size-women": "m" }). Swapping them is the most frequent cause of missing_size_mapping.
complete variable product
{
  "type": "variable",
  "name": "Beige Cashmere Sweatshirt",
  "sku": "AL2651115145BB",
  "brands":     [{ "id": 72002 }],
  "categories": [{ "id": 19 }, { "id": 26 }],
  "groups":     [{ "id": 71651 }, { "id": 71673 }, { "id": 71675 }],
  "genders":    [{ "id": 71576 }],
  "conditions": [{ "id": 71354 }],
  "color": "Beige",

  "attributes": [
    { "id": 14, "variation": false, "options": ["Beige"] },
    { "id": 10, "variation": true,  "options": ["XS", "S", "M"] }
  ],

  "images": [
    { "src": "https://cdn.example.com/2651115145_1.jpg" },
    { "src": "https://cdn.example.com/2651115145_2.jpg" }
  ],
  "short_description": "Monochromatic long-sleeve cashmere sweater with a crew neckline.",
  "materials": "Cashmere",

  "meta_data": [
    { "key": "_external_id",     "value": "2651115145" },
    { "key": "_bgorigincountry", "value": "CN" },
    { "key": "_bgmaterial",      "value": "Cashmere" },
    { "key": "_import_source",   "value": "your-vendor-slug" },
    { "key": "_season",          "value": "AW26" }
  ],

  "variations": [
    {
      "sku":              "AL-2651115145-M",
      "regular_price":    "540.00",
      "sale_price":       "280.00",
      "stock_quantity":   1,
      "global_unique_id": "4068849368138",
      "attributes": { "pa_italian-size-women": "m" },
      "meta_data": [
        { "key": "_variation_external_id", "value": "2651115145_M" },
        { "key": "_vendor_cost",           "value": "225.45" },
        { "key": "_vendor_sku",            "value": "26511151_45_M" }
      ]
    },
    {
      "sku":              "AL-2651115145-S",
      "regular_price":    "540.00",
      "sale_price":       "280.00",
      "stock_quantity":   2,
      "global_unique_id": "4068849368121",
      "attributes": { "pa_italian-size-women": "s" },
      "meta_data": [
        { "key": "_variation_external_id", "value": "2651115145_S" },
        { "key": "_vendor_cost",           "value": "225.45" },
        { "key": "_vendor_sku",            "value": "26511151_45_S" }
      ]
    }
  ]
}

Meta data keys

meta_data is where identity, provenance and commercial terms live. Values must be a string, a number or a boolean — nested objects and arrays are rejected, so serialise them first. Keys we do not recognise are stored and ignored, so it is safe to carry your own bookkeeping alongside ours.

meta_data
// A complete simple product. The first four are required;
// the rest are what a well-formed feed sends.
"meta_data": [
  { "key": "_external_id",       "value": "338000" },
  { "key": "_bgorigincountry",   "value": "VN" },
  { "key": "_vendor_cost",       "value": "304.80" },
  { "key": "_vendor_sku",        "value": "186036001" },
  { "key": "_bgmaterial",        "value": "Cowhide" },
  { "key": "_import_source",     "value": "yourshop" },
  { "key": "_vendor_size",       "value": "U" },
  { "key": "_season",            "value": "Fall Winter 2026/2027" },
  { "key": "_mpn",               "value": "186036001" },
  { "key": "_client_product_id", "value": "A00279935" },
  { "key": "_size_fit_notes",    "value": "W 18.5 x H 9.4 x D 7 cm" }
]

// NOT this — meta_data is an array of {key, value} objects,
// never a plain object:
"meta_data": { "_external_id": "338000" }   // rejected
The three that matter most
_external_id is how the product is identified across syncs. _vendor_cost is the commercial value every settlement figure is derived from. _bgorigincountry is what customs paperwork is built from. None of the three can be inferred, corrected downstream, or safely omitted.

Product-level keys

Key Requirement What it is
_external_id Required Your own permanent product identifier. This is the upsert key — it is what `?use_eid=true` matches on.
_bgorigincountry Required ISO-3166-1 alpha-2 country of manufacture.
_vendor_cost Simple only Your cost price, as a string, at full precision. How it is used depends on your agreement with BrandsGateway — never omit or round it.
_vendor_sku Simple only Your own SKU for the sellable unit, so support can trace an order back to your system.
_bgmaterial Recommended Mapped BrandsGateway material name(s), comma-separated. Powers material filtering.
_import_source Recommended Your vendor slug. Keeps the product traceable to its feed.
_mpn Optional Manufacturer part number.
_vendor_size Recommended Your own, unmapped size label for this unit — "U", "31", "42,5". Kept verbatim alongside the mapped size so a mis-map can be traced back to what you actually sent.
_client_product_id Optional Your internal record ID for the product, when it differs from `_external_id`. Support uses it to find the row in your system.
_size_fit_notes Optional Free-text sizing or dimensions shown on the product page — "true to size fit", "W 18.5 x H 9.4 x D 7 cm".
_season Optional Season code, e.g. "AW26".
_collection Optional Collection name, e.g. "MAIN COLLECTION".
wc_ai_prompt Optional The prompt used when a product group and title were assigned by BrandsGateway's classifier. Only set by the managed pipeline — a self-integrating vendor supplies `groups` and `name` directly and omits this.
wc_ai_response Optional The classifier's raw response. Audit trail for how `groups` was chosen.
wc_ai_response_last_modified Optional When that classification was last produced.
_dimensions_measurements Optional Free-text measurements shown on the product page.
_heel_height Optional Numeric heel height; pair with `_heel_height_unit`.

Variation-level keys

Each variation carries its own meta bag. Cost and SKU repeat per size because a size is an independently sellable, independently priced unit.

Key Requirement What it is
_vendor_cost Simple only Your cost price, as a string, at full precision. How it is used depends on your agreement with BrandsGateway — never omit or round it.
_vendor_sku Simple only Your own SKU for the sellable unit, so support can trace an order back to your system.
_variation_external_id Variable only Your permanent identifier for this specific size. Must be unique within the product.
_mpn Optional Manufacturer part number.
_vendor_size Recommended Your own, unmapped size label for this unit — "U", "31", "42,5". Kept verbatim alongside the mapped size so a mis-map can be traced back to what you actually sent.
_global_unique_id Optional Mirror of the variation EAN. Sent alongside the top-level `global_unique_id` field for legacy consumers.
_sku Optional Mirror of the variation's own `sku` field. The pipeline sends both; harmless to omit.

On _external_id

The most consequential value in the payload. It is the join key between your catalog and ours, it is what use_eid=true resolves, and it survives the product being archived and later restored.

  • Permanent. Reusing an ID for a different product silently overwrites the old listing, including its history.
  • Unique within your catalog. Two feed rows sharing one ID are both dropped — the platform cannot tell which is authoritative.
  • Yours. Never derive it from a BrandsGateway product ID, or it will not survive an archive-and-restore cycle.

On _vendor_cost

Your cost price is read as a commercial figure rather than a hint. Send it as a string with full precision. On variable products it belongs on every variation, because sizes can legitimately cost different amounts.

What it is used for depends on your agreement. There is no single company-wide arrangement: each vendor has its own terms, and those terms are dated, so an order settles under whatever was in force when it was placed. Broadly there are two live shapes — some vendors are paid this cost directly, and some are paid on the order line with a commission charged separately. Your account manager can tell you which applies to you; the portal does not know, and this page will not guess.

An omitted cost does not fail — it reports as nothing
Whichever arrangement you are on, send the value and send it accurately. Where a cost is missing, the payout reporting records the line as zero payout and zero profit rather than raising an error, so the gap is invisible until someone reconciles a settlement. Rounding it in your feed rounds your own figures.

Responses

  • 201 Created — new product, now live. The body is the full product including BrandsGateway's own id.
  • 200 OK — matched on _external_id and updated.
  • 422 — not accepted. sent: false means we stopped it and nothing reached the shop; sent: true with accepted: false means BrandsGateway refused it. Either way .code is stable and documented in the error reference.
  • 401 — bad credential, or a vendor ID that is not yours.

The schema, verbatim

This is the exact JSON Schema the platform validates against — the same document the validator and POST /api/v1/validate compile. Wire it into your own test suite so a breaking feed change fails on your side, not ours.

bg-product.schema.json
{
  "type": "object",
  "required": [
    "type",
    "categories",
    "brands",
    "attributes",
    "meta_data",
    "images",
    "name",
    "sku",
    "genders",
    "conditions",
    "groups"
  ],
  "properties": {
    "type": {
      "type": "string",
      "enum": [
        "simple",
        "variable"
      ]
    },
    "categories": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          }
        }
      }
    },
    "brands": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          }
        }
      }
    },
    "color": {
      "type": "string",
      "minLength": 1
    },
    "attributes": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "id",
          "variation",
          "options"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "variation": {
            "type": "boolean"
          },
          "options": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          }
        }
      }
    },
    "meta_data": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "key",
          "value"
        ],
        "properties": {
          "key": {
            "type": "string"
          },
          "value": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              }
            ]
          }
        }
      }
    },
    "images": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "src"
        ],
        "properties": {
          "src": {
            "type": "string",
            "format": "uri"
          }
        }
      }
    },
    "short_description": {
      "type": "string"
    },
    "name": {
      "type": "string",
      "minLength": 1
    },
    "sku": {
      "type": "string",
      "minLength": 1
    },
    "genders": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          }
        }
      }
    },
    "conditions": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          }
        }
      }
    },
    "groups": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          }
        }
      }
    },
    "global_unique_id": {
      "type": "string"
    },
    "regular_price": {
      "type": "string",
      "pattern": "^[0-9]+(\\.[0-9]{1,2})?$"
    },
    "sale_price": {
      "type": "string",
      "pattern": "^[0-9]+(\\.[0-9]{1,2})?$"
    },
    "stock_quantity": {
      "type": "integer",
      "minimum": 0
    },
    "manage_stock": {
      "type": "boolean"
    },
    "vendor_id": {
      "type": "string"
    },
    "materials": {
      "type": "string"
    },
    "variations": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "sale_price",
          "regular_price",
          "stock_quantity",
          "global_unique_id",
          "attributes",
          "sku",
          "meta_data"
        ],
        "properties": {
          "sale_price": {
            "type": "string",
            "pattern": "^[0-9]+(\\.[0-9]{1,2})?$"
          },
          "regular_price": {
            "type": "string",
            "pattern": "^[0-9]+(\\.[0-9]{1,2})?$"
          },
          "stock_quantity": {
            "type": "integer",
            "minimum": 0
          },
          "global_unique_id": {
            "type": "string",
            "minLength": 1
          },
          "attributes": {
            "type": "object",
            "minProperties": 1
          },
          "sku": {
            "type": "string",
            "minLength": 1
          },
          "variable_global_unique_id": {
            "type": "string"
          },
          "meta_data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key",
                "value"
              ],
              "properties": {
                "key": {
                  "type": "string"
                },
                "value": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "boolean"
                    }
                  ]
                }
              }
            },
            "allOf": [
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_variation_external_id"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _variation_external_id"
              },
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_vendor_cost"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _vendor_cost"
              },
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_vendor_sku"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _vendor_sku"
              }
            ]
          }
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "type": {
            "const": "simple"
          }
        }
      },
      "then": {
        "required": [
          "regular_price",
          "sale_price",
          "stock_quantity",
          "manage_stock",
          "global_unique_id"
        ],
        "properties": {
          "meta_data": {
            "allOf": [
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_bgorigincountry"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _bgorigincountry"
              },
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_external_id"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _external_id"
              },
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_vendor_cost"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _vendor_cost"
              },
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_vendor_sku"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _vendor_sku"
              }
            ]
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "variable"
          }
        }
      },
      "then": {
        "required": [
          "variations"
        ],
        "properties": {
          "variations": {
            "minItems": 1
          },
          "meta_data": {
            "allOf": [
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_bgorigincountry"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _bgorigincountry"
              },
              {
                "contains": {
                  "properties": {
                    "key": {
                      "const": "_external_id"
                    }
                  }
                },
                "errorMessage": "Missing required meta_data key: _external_id"
              }
            ]
          }
        }
      }
    }
  ]
}
Next — step 3: test and monitor
You know what to send. Now check a payload before you send it, confirm your first product landed, and watch what happens to every product after that.