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
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.
_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
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
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
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
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
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
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
Men (71575), Women (71576) or Unisex (71577). Three fixed terms.
conditions
Stock condition. Determines whether the item can be returned, so it is never inferred.
New-season stock is New with tags (71354).
attributes
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
The mapped BrandsGateway colour name, mirrored from the colour attribute.
fails as missing_color →images
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
The product copy shown on the listing. Plain text or light HTML.
materials
Mapped BrandsGateway material name(s). Mirror it into meta key _bgmaterial.
fails as missing_material →meta_data
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
Your vendor ID, echoed in the body. The path parameter is authoritative; this is for your own traceability.
raw_name
Your original product title, before any renaming. Kept so the mapped `name` can be traced back.
raw_short_description
Your original description, before any rewriting.
raw_material
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
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
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
The price BrandsGateway sells at. Must be strictly below regular_price.
fails as invalid_price_ratio →stock_quantity
Units available. Your feed is authoritative — each sync overwrites this value.
manage_stock
Always true. Without it BrandsGateway cannot track depletion and the item oversells.
global_unique_id
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 →{
"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
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
SKU for this specific size. Unique within your catalog.
regular_price
Retail price for this size. Sizes may legitimately differ in price.
fails as invalid_price_format →sale_price
BrandsGateway price for this size. Strictly below this variation's regular_price.
fails as invalid_price_ratio →stock_quantity
Units of this size. Zero is valid and means the size is temporarily unavailable.
global_unique_id
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
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
Must contain _variation_external_id, _vendor_cost and _vendor_sku.
fails as missing_meta_key →variable_global_unique_id
A mirror of this variation's `global_unique_id`, read by some legacy consumers. The pipeline sends both.
weight
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
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
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.
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.
{
"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.
// 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
_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.
Responses
-
201 Created — new product, now live. The body is the
full product including BrandsGateway's own
id. -
200 OK — matched on
_external_idand updated. -
422 — not accepted.
sent: falsemeans we stopped it and nothing reached the shop;sent: truewithaccepted: falsemeans BrandsGateway refused it. Either way.codeis 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.
{
"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"
}
]
}
}
}
}
]
}