RECIPE: Business Recipe - Updating a Wix Store Product (Catalog V3)
Use this recipe to update an existing Catalog V3 product: storefront visibility, description, media, options, variants, prices, or stock-related inventory records.
Before Any Product Update
Every Catalog V3 product update is revision-based:
- If the user gives a product name instead of a product ID, use Search Products and choose the exact product name match.
- Use Get Product to retrieve the current product, its
product.revision, and its existing variants. Search Products and Query Products responses do not includevariantsInfo.variants, so a variant or price update assembled from a search result sends an empty variants array and is rejected. Re-read the product before every variant-level update. - Include
product.idand the currentproduct.revisionin every Update Product PATCH body. - Update Product is a partial update: only
product,product.id, andproduct.revisionare required, and top-level fields you omit (for examplename,ribbon,brand) are left unchanged. The full-array overwrite rule applies only to the repeated fieldsoptions,modifiers, andvariantsInfo.variants. - For simple text/HTML description updates, prefer
plainDescription. Usedescriptiononly when sending a Rich Content object.
Find the product by name
curl -X POST "https://www.wixapis.com/stores/v3/products/search" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"search": {
"expression": "Product name"
}
}'For product-name lookup, prefer Search Products before retrieving the product by ID. Search only resolves the product ID; it does not replace the Get Product call.
Get the current revision
curl -X GET "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Authorization: <AUTH>"Common Update Patterns
Hide or Show a Product
"Hide this product", "make it not show in my store", "unhide it", "put it back in the store" are all product-level visibility changes. Set the visible boolean on the product in an Update Product PATCH. Do not delete the product, and do not change variant visibility to hide the product.
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"product": {
"id": "{productId}",
"revision": "{currentRevision}",
"visible": false
}
}'Send "visible": true to show it again. Nothing else needs to be in the body — name, options, variantsInfo and the other top-level fields you omit are left unchanged. Confirm the result from product.visible in the response.
Visibility behaviour to report back accurately:
visibledefaults totrue.- For a product without options, updating
product.visibleautomatically updates the default variant'svisibleto match. - For a product with options, product and variant visibility are independent: setting
product.visibletofalseleaves eachvariantsInfo.variants[].visibleas it was. - Point-of-sale visibility is a separate field,
visibleInPos. Only change it when the user asks about POS. It is alwaysfalseforproductType: DIGITAL.
Update Description Only
For a normal user request like "set the product description to X", use plainDescription with valid HTML. The API converts it to rich content.
Do not send a plain string in description. description is a Rich Content object.
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"product": {
"id": "{productId}",
"revision": "{currentRevision}",
"plainDescription": "<p>A great product for everyone.</p>"
}
}'Use description only when you intentionally need to send Rich Content:
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"product": {
"id": "{productId}",
"revision": "{currentRevision}",
"description": {
"nodes": [
{
"type": "PARAGRAPH",
"id": "description",
"nodes": [
{
"type": "TEXT",
"textData": {
"text": "Updated product description."
}
}
],
"paragraphData": {
"textStyle": {
"textAlignment": "AUTO"
}
}
}
],
"metadata": {
"version": 1
}
}
}
}'Update Options and Variants
When adding or changing options and variants, send the full option definitions and one variant for each option-choice combination. Use optionChoiceNames to reference choices.
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"product": {
"id": "{productId}",
"revision": "{currentRevision}",
"options": [
{
"name": "Color",
"optionRenderType": "SWATCH_CHOICES",
"choicesSettings": {
"choices": [
{
"name": "White",
"choiceType": "ONE_COLOR",
"colorCode": "#FFFFFF"
},
{
"name": "Red",
"choiceType": "ONE_COLOR",
"colorCode": "#FF0000"
},
{
"name": "Black",
"choiceType": "ONE_COLOR",
"colorCode": "#000000"
}
]
}
}
],
"variantsInfo": {
"variants": [
{
"choices": [
{
"optionChoiceNames": {
"optionName": "Color",
"choiceName": "White",
"renderType": "SWATCH_CHOICES"
}
}
],
"price": {
"actualPrice": {
"amount": "270.00"
}
}
},
{
"choices": [
{
"optionChoiceNames": {
"optionName": "Color",
"choiceName": "Red",
"renderType": "SWATCH_CHOICES"
}
}
],
"price": {
"actualPrice": {
"amount": "270.00"
}
}
},
{
"choices": [
{
"optionChoiceNames": {
"optionName": "Color",
"choiceName": "Black",
"renderType": "SWATCH_CHOICES"
}
}
],
"price": {
"actualPrice": {
"amount": "270.00"
}
}
}
]
}
}
}'When updating existing variants, include each existing variant id. If no GUID is passed, a variant is created with a new GUID. Each variant object is replaced whole rather than merged, so carry over the fields you are not changing: rebuilding a variant from just its id plus the field you want to set drops everything else and is rejected on the first required field it lost (price must not be empty). Start from the variant as returned by Get Product and override only what the user asked to change.
Renaming an Existing Choice Is Not Supported
options[].choicesSettings.choices is immutable once a choice exists — this includes the choice's own name. If the user asks to rename, fix a typo in, or standardize the name of an existing option choice (for example a shared/reusable customization like "Color" or "Patch Design" that already has variants), do not attempt an Update Product or Update Customization PATCH: it returns 200, product.revision/customization.revision increments, but the name is silently left unchanged — confirm this yourself with a fresh Get Product/Get Customization call and you will still see the old name. There is currently no endpoint that renames an existing choice, even when you pass back the same choiceId and preserve every other choice, option, and variant exactly.
Do not try workarounds that risk the existing variant matrix — do not delete and recreate the choice/option (this destroys all variants using it, along with their prices, SKUs, and inventory), and do not use Set Customization Choices (it fails once the choice is assigned to any product). Tell the user this specific rename isn't possible via the API today and that manually renaming the choice in the Wix dashboard (Site → Products → the product's options) is the only way, or point them to SupportAndFeedback to report the gap.
This only blocks renaming an existing choice. Adding brand-new choices to an option (Add/Bulk Add Customization Choices, or including a new choice name in a full Update Product options payload) works normally.
Convert a Simple Product to Color Variants
When adding the first option to a simple product, do not preserve a choice-less default variant unchanged. A simple product often has one existing variant with price or stock but no choices. After you add a Color option, every variant in variantsInfo.variants must include choices that match the product options.
Use the existing default variant as source data only. For example, copy its price if the user did not ask to change price, then send a complete optioned variants list where each variant has:
{
"choices": [
{
"optionChoiceNames": {
"optionName": "Color",
"choiceName": "Red",
"renderType": "SWATCH_CHOICES"
}
}
],
"price": {
"actualPrice": {
"amount": "{existingOrRequestedPrice}"
}
}
}After the product update returns the new variant IDs, use those IDs to set inventory.
Set Stock for New Variants
Inventory is handled separately from product updates. After the product update returns variant IDs, use Bulk Create Inventory Items with productId, variantId, and quantity.
If the store has multiple inventory locations, include locationId; otherwise the store's default location is used.
After bulk inventory create, check bulkActionMetadata.totalSuccesses and results[].itemMetadata.success. Returned inventory entities are under results[].item, not a top-level inventoryItems field; confirm stock from results[].item.quantity.
curl -X POST "https://www.wixapis.com/stores/v3/bulk/inventory-items/create" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"inventoryItems": [
{
"productId": "{productId}",
"variantId": "{redVariantId}",
"quantity": 10
},
{
"productId": "{productId}",
"variantId": "{blueVariantId}",
"quantity": 10
}
],
"returnEntity": true
}'Update Media Only
Sets the product-level gallery (media.itemsInfo.items). For an image shown per option choice (a swatch's photo), see Per-choice media below.
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"product": {
"id": "{productId}",
"revision": "{currentRevision}",
"media": {
"itemsInfo": {
"items": [
{
"url": "https://static.wixstatic.com/media/your-image.jpg",
"altText": "Product image"
}
]
}
}
}
}'Choice & variant fields (what you can set)
Send these inside the options / variantsInfo.variants arrays — both arrays complete, in the
same PATCH, with the current revision, and identity-preserving (see Update Options and
Variants): keep the option's id and every choiceId, or the ids are re-minted and the existing
variants no longer resolve (400 "Variant choice not found in product options"). Each variant needs
its id, its choices (by optionChoiceIds — or optionChoiceNames, see Gotchas) and a
price — omitting price is 400 "price must not be empty".
Option choice (options[].choicesSettings.choices[]):
name,choiceType,colorCode— the choice's identity and swatch colour.media={ "items": [ { "mediaId" } | { "url" } ] }— the product photos shown when this choice is picked.urlis write-only; on read you getmediaId(request thePRODUCT_CHOICES_MEDIA_REFERENCESmask). Usemedia, notlinkedMedia(a separate display-filter, returned empty when you readmedia).Gallery-first; the id you link is the gallery item's, not the upload's. Add the
urlto the gallery, then read the product back and link the id you see there:GET /stores/v3/products/{id}?fields=MEDIA_ITEMS_INFO # → media.itemsInfo.items[].id = "abc~mv2.jpg" ← link THIS as choice media.items[].mediaId # the id from POST /site-media/v1/files/import or /files/get-file-by-id 400s as a choice mediaId{ "url" }works in place of{ "mediaId" }too, but only once the image is already in the gallery.
displayImage— the image shown on the swatch itself (distinct frommedia).- Read-only, don't send:
inStock,visible,key.
Variant (variantsInfo.variants[], by its id from Get Product; always send choices + price with it):
price={ "actualPrice": { "amount" }, "compareAtPrice": { "amount" } }— setcompareAtPriceaboveactualPricefor a strikethrough sale; omit it for full price.sku,barcode— per combination.visible— hide a single variant.revenueDetails— cost / profit tracking.- Read-only, don't send:
media(derived from the choices' media),inventoryStatus(stock — use the Inventory API, see Set Stock for New Variants),subscriptionPricesInfo.
# A choice image + a variant's sale price, one PATCH. Both arrays are complete and keep their ids —
# this exact shape is the one that succeeds; the two shortcuts (choices by name only, or a variant
# without its choices/price) each 400.
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" -H "Authorization: <AUTH>" \
-d '{ "product": { "id": "{productId}", "revision": "{currentRevision}",
"options": [ { "id": "{optionId}", "name": "Color", "optionRenderType": "COLOR_CHOICES",
"choicesSettings": { "choices": [
{ "choiceId": "{choiceId1}", "name": "Red", "choiceType": "ONE_COLOR", "colorCode": "#C0392B",
"media": { "items": [ { "mediaId": "abc~mv2.jpg" } ] } },
{ "choiceId": "{choiceId2}", "name": "Blue", "choiceType": "ONE_COLOR", "colorCode": "#2C3E50" } ] } } ],
"variantsInfo": { "variants": [
{ "id": "{variantId1}", "sku": "TEE-RED-L",
"price": { "actualPrice": { "amount": "20" }, "compareAtPrice": { "amount": "30" } },
"choices": [ { "optionChoiceIds": { "optionId": "{optionId}", "choiceId": "{choiceId1}" } } ] },
{ "id": "{variantId2}", "price": { "actualPrice": { "amount": "20" } },
"choices": [ { "optionChoiceIds": { "optionId": "{optionId}", "choiceId": "{choiceId2}" } } ] } ] } } }'Update Variant Price Only
Read {existingVariantId} off the Get Product response; a Search or Query Products result does not carry it.
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"product": {
"id": "{productId}",
"revision": "{currentRevision}",
"variantsInfo": {
"variants": [
{
"id": "{existingVariantId}",
"price": {
"actualPrice": {
"amount": "29.99"
}
}
}
]
}
}
}'Attach a Digital File
A DIGITAL product is sellable only when its variant carries both a digital file and stock. Upload the file first (Upload Media to Wix → Generate Upload URL, then PUT the bytes), then send its file.id on the variant — digitalProperties is a variant field, never a product field.
curl -X PATCH "https://www.wixapis.com/stores/v3/products/{productId}" \
-H "Content-Type: application/json" \
-H "Authorization: <AUTH>" \
-d '{
"product": {
"id": "{productId}",
"revision": "{currentRevision}",
"variantsInfo": {
"variants": [
{
"id": "{existingVariantId}",
"price": { "actualPrice": { "amount": "9.99" } },
"visible": true,
"inventoryItem": { "inStock": true },
"digitalProperties": { "digitalFile": { "id": "{fileId}" } }
}
]
}
}
}'Confirm from product.variantsInfo.variants[].digitalProperties.digitalFile in the response.
Important Notes
- A request to hide a product is a
visible: falseupdate on the product, never a Delete Product call and never a variant-only change. - To update array fields like
options,modifiers,variantsInfo.variants, and any others, pass the entire existing array. Passing only the changed item overwrites the whole array. - To update
variantsInfo.variants, also passoptions, and vice versa. Variants and options are mutually dependent and must stay aligned. - When converting a simple product to an optioned product, rebuild the variants list so every variant has
choices; do not keep an existing choice-less default variant unchanged. - Always include
choicesSettingswith the complete list of choices when updating a product with options. - An existing choice's
namecan't be changed via Update Product or Update Customization. The request succeeds and revision increments, but the rename is silently dropped — see "Renaming an Existing Choice Is Not Supported" above. - Use
optionChoiceNamesrather thanoptionChoiceIdsin variants for more reliable updates. Reading them back is not symmetric: Get Product returns each variant'schoiceswithoptionChoiceIdsonly, and fills inoptionChoiceNamesjust when the request'sfieldsarray includes"VARIANT_OPTION_CHOICE_NAMES". So to find the variant for a named choice such asLarge, either pass that field and match on the name, or take the choice GUID fromoptions[].choicesSettings.choices[].choiceIdand match it againstvariants[].choices[].optionChoiceIds.choiceId. Matching on a name the response never carried raises nothing — it just selects no variant. - Include the
renderTypeinoptionChoiceNames.
Error Message Reference
| Error Message | Meaning | Fix |
|---|---|---|
revision must not be empty |
Missing optimistic lock | GET product first and include product.revision in PATCH |
revision mismatch |
Stale revision | Re-GET product and retry with the new revision |
Expected an object for description |
Sent description as a string |
Use plainDescription for HTML strings, or send description as Rich Content |
choicesSettings must not be empty |
Missing choices array | Include full choicesSettings.choices array |
Missing product option choices |
Variant references non-existent option | Use optionChoiceNames with exact option and choice names |
price must not be empty |
A variant was sent without a price — including an existing variant rebuilt from only its id and the field being changed |
Carry price.actualPrice.amount on every variant you send, not just new ones; copy it from the Get Product response for variants you are not repricing |
variantsInfo is invalid: variants has size 0, expected 1 or more |
Variants were read from a Search or Query Products response, which does not return them | Re-read the product with Get Product and send its variantsInfo.variants |
Missing option choices or INVALID_DEFAULT_VARIANT |
Product has options but at least one variant has no matching choices | Rebuild variantsInfo.variants so every variant includes choices for all product options |
DIGITAL_PRODUCT_CANNOT_BE_VISIBLE_IN_POS |
Sent visibleInPos: true on a digital product |
Digital products can't be visible in POS; leave visibleInPos out of the body |
ITEM_NOT_FOUND_IN_CATALOG at add-to-cart, product exists |
A DIGITAL variant has no digitalProperties.digitalFile |
Attach a file — see Attach a Digital File |
exceeds available inventory at add-to-cart, product exists |
The variant has no stock (DIGITAL products included) |
Set inventoryItem.inStock: true on the variant |