RECIPE: Business Recipe – Initial Setup for a Wix Online Store (Catalog V3)
Standard call shape (every curl below). The
<AUTH>placeholder is shorthand forAuthorization: Bearer <TOKEN>only. Body-bearing requests also needContent-Type: application/json.
A concise checklist for preparing any new Wix site that uses the Online Stores app with Catalog V3. Notice that this recipe is NOT meant for coding purposes and is ONLY meant for initial catalog setup.
This recipe is the how, not the what. What to seed — how many products, which are in/out of stock, which categories — is determined by the request you're fulfilling. This recipe only specifies the calls and the request format; it does not decide quantities or which items to create.
API surfaces: products use Catalog V3 (
https://www.wixapis.com/stores/v3/...). The Categories API is the exception — it lives on av1endpoint (https://www.wixapis.com/categories/v1/categories). Don't mix in any pre-V3 product endpoints.
Article: Steps for Setting Up a Wix Online Store
YOU MUST complete the following steps in the given order (1-4) and without requiring additional user input. One conditional: the category steps (3-4) run only when the request names categories — if intent.stores.categoriesNamed is empty, create none (skill policy, per SEED.md § "What to seed per capability") and skip the category steps. Products (steps 1-2) always run; the Attach images step runs last, only when imagery is on.
⚠️ CRITICAL ORDER REQUIREMENT: Do the product operations FIRST (clean + create, Steps 1-2), then categories (Steps 3-4). Categories API might take some time to be fully available after Stores installation, so always finish products before attempting category operations.
STEP 1: Clean the store — remove the default sample products
A freshly provisioned Wix Stores app comes pre-seeded with demo/sample products. Only remove products that are obviously the install's own demo/sample data on a fresh install. Do not assume the existing products are samples: the site may already hold the owner's real catalog (a connect/iterate run, or an owner-populated store). If what's there isn't obviously install demo data, or you're unsure, do not delete it — ask the user first (SEED.md: seeding is additive; deleting real content needs the owner's approval). When they clearly are the install's samples, remove them before creating yours so the storefront shows only the intended catalog.
- List the existing products —
POST https://www.wixapis.com/stores/v3/products/querywith body{"query": {"paging": {"limit": 50}}}. Collect everyproduct.idfrom the response. - Bulk-delete them in one call —
POST https://www.wixapis.com/stores/v3/bulk/products/deletewith body{"productIds": ["<id1>", "<id2>", …]}(the ids from step 1; up to 100 per call). On a fresh install the query returns only the install's sample products — delete those. If it returns anything that could be the owner's real catalog, stop and ask first (above).
STEP 2: Bulk-create the products (with options)
Create the products in a single bulk request to POST https://www.wixapis.com/stores/v3/bulk/products-with-inventory/create. How many products, and which are in or out of stock, are set by the request you're fulfilling — this step only gives the call and the required format. Each product needs a real web image URL relevant to it, and price via actualPrice (+ optional compareAtPrice).
First decide each product's type by what the buyer receives: shipped → PHYSICAL (the JSON below); a file the buyer downloads and keeps → DIGITAL (the digital block below — carries a file, no quantity). Access — a membership or an online course/program the buyer enrolls in — is Pricing Plans, not a Stores product.
⚠️ CRITICAL: PRODUCT & VARIANT VISIBILITY — set "visible": true explicitly.
Storefront product queries (searchProducts / queryProducts) return only visible products to site visitors. A product created without "visible": true is created successfully but will NOT appear on the live site — the catalog looks empty. So:
- Set
"visible": trueon every product (product level). - Set
"visible": trueon every variant. A variant withinventoryItem.quantity > 0is buyable; a variant that should be out-of-stock stays"visible": truewith"quantity": 0. - Never rely on a default — always include
visible.
Request body shape (one representative product shown — repeat product objects inside the products array):
{
"products": [
{
"name": "Pro Basketball Sneaker",
"productType": "PHYSICAL",
"physicalProperties": {},
"visible": true,
"visibleInPos": true,
"description": {
"nodes": [
{
"type": "PARAGRAPH",
"id": "desc1",
"nodes": [{ "type": "TEXT", "textData": { "text": "High-performance basketball sneaker with superior ankle support." } }],
"paragraphData": { "textStyle": { "textAlignment": "AUTO" } }
}
],
"metadata": { "version": 1, "id": "basketball-desc-001" }
},
"options": [
{
"name": "Size",
"optionRenderType": "TEXT_CHOICES",
"choicesSettings": { "choices": [ { "choiceType": "CHOICE_TEXT", "name": "8" }, { "choiceType": "CHOICE_TEXT", "name": "9" } ] }
},
{
"name": "Color",
"optionRenderType": "SWATCH_CHOICES",
"choicesSettings": { "choices": [ { "choiceType": "ONE_COLOR", "name": "Red", "colorCode": "#FF0000" }, { "choiceType": "ONE_COLOR", "name": "Blue", "colorCode": "#0000FF" } ] }
}
],
"variantsInfo": {
"variants": [
{
"choices": [
{ "optionChoiceNames": { "optionName": "Size", "choiceName": "8", "renderType": "TEXT_CHOICES" } },
{ "optionChoiceNames": { "optionName": "Color", "choiceName": "Red", "renderType": "SWATCH_CHOICES" } }
],
"price": { "actualPrice": { "amount": "159.99" }, "compareAtPrice": { "amount": "200.00" } },
"visible": true,
"inventoryItem": { "quantity": 25, "preorderInfo": { "enabled": false } },
"physicalProperties": {}
}
]
}
}
],
"returnEntity": true
}⚠️ CRITICAL FORMAT REQUIREMENTS:
Description MUST be rich-text nodes, not a plain string — a plain string causes an
"Expected an object"error. Use the{ "nodes": [...], "metadata": {...} }shape shown.Media — gated by the
imagerypolicy (SEED.md§ "Entity images"), no exception for stores. Always create products text-only here — omitmedia. Whenimageryis on, the Attach images step below writes generated brand images in a second pass; it does not happen at create time.Choose the product type from what the buyer receives: a shipped item is
PHYSICAL(default); a downloadable file they keep — ebook, PDF, template, preset pack, track, downloadable video — isDIGITAL. Selling access rather than a file — a membership, subscription, or an online course/program the buyer enrolls in — is Pricing Plans, not a Stores product.Physical products MUST set
"productType": "PHYSICAL"and an empty"physicalProperties": {}(on the product and on each variant).Digital products (
"productType": "DIGITAL") dropphysicalPropertiesand are sellable only when each variant carries both a file and stock — miss either and the product is created fine, reads backvisible: true, and is rejected at add-to-cart. Upload the file first (POST /site-media/v1/files/generate-upload-url→PUTthe bytes →file.id):{ "productType": "DIGITAL", "visible": true, "variantsInfo": { "variants": [ { "price": { "actualPrice": { "amount": "12.00" } }, "visible": true, "inventoryItem": { "inStock": true }, "digitalProperties": { "digitalFile": { "id": "<file.id>" } } } ] } }Options: text options use
"optionRenderType": "TEXT_CHOICES"+"choiceType": "CHOICE_TEXT"; color options use"optionRenderType": "SWATCH_CHOICES"+"choiceType": "ONE_COLOR"+ acolorCode.Variants = the full Cartesian product of all option choices; each variant references all options via
optionChoiceNames, setsprice.actualPrice.amount(+ optionalcompareAtPrice.amount) as strings, andinventoryItem.quantity.If part of the bulk request fails, retry the failed products once with the exact same format; do not loop.
⚠️ CRITICAL: options/variants are for things the buyer selects and buys — not for attributes you only filter or display by. Make something an option (and thus a variant) only if the buyer picks it to purchase a distinct SKU (Size, Color, Format). An attribute you merely filter, badge, or display by — roast level, material, genre, "new arrival" — is not a variant: encode it in the product name, its category, or description, and never as an option. Modeling a display-only attribute as an option multiplies the variant Cartesian product for nothing (slower seeding, larger payload) and produces a buyer-facing selector that shouldn't exist. (A single-variant product is fine — give it one variant with no options.) Default cardinality — keep it small: unless the request names specific options, give each product at most one option with ≤3 choices (≤3 variants); many products legitimately have none (a single variant). This is a floor, not a cap — honor a larger/explicit option set when intent names one. The two-option example above shows the multi-option format; it is not the default shape to reach for.
⚠️ Reading the response — created products are under productResults.results[], NOT a top-level results. A successful bulk create returns 200 with this shape:
{ "productResults": { "results": [
{ "itemMetadata": { "id": "<productId>", "originalIndex": 0, "success": true },
"item": { "id": "<productId>", "slug": "<slug>", "name": "…", "visible": true, … } }
] } }Read each product's id (→ the catalogItemId you'll assign to categories in STEP 4 and the frontend will use) and slug from productResults.results[].item. There is no top-level results key — reading response.results finds nothing and makes a successful create look like it returned zero products. Check productResults.results first; do not re-create on an empty top-level results.
STEP 3: Create the store's categories
Create the categories for the store — the categories that fit the user's request. Which categories (and how many) are set by the request you're fulfilling; this step only gives the call and format.
Use the Create Category endpoint: POST https://www.wixapis.com/categories/v1/categories. There is no bulk-create endpoint for /categories/v1/ (the events/v1/bulk/categories/create URL is for the Events product, not Stores).
⚠️ CRITICAL: create the categories SEQUENTIALLY — one call at a time, never concurrently. Every category lives in the single shared @wix/stores category tree, which carries one revision. Firing the creates concurrently makes them race on that revision: all but one fail with 409 INVALID_REVISION ("Outdated revision for entity id @wix/stores"), and the failed-then-retried calls can leave duplicate categories behind. So issue each create, wait for its response, then issue the next. (There are only a handful of categories — the serial cost is negligible.)
The request body must include a top-level treeReference field — it must not be nested inside the category object:
{
"category": {
"name": "Drinkware",
"description": "The Drinkware category includes a wide range of containers designed for holding beverages",
"visible": true
},
"treeReference": {
"appNamespace": "@wix/stores",
"treeKey": null
}
}STEP 4: Add Each Product to a Category
Add each product to the category it belongs to (per the request you're fulfilling). Acquire the product ids (from STEP 2's response) first. Use the Bulk Add Items To Category endpoint — products are the items: POST https://www.wixapis.com/categories/v1/bulk/categories/{categoryId}/add-items (one call per categoryId — the path parameter is single).
Issue these add-items calls SEQUENTIALLY too — one call at a time, waiting for each response before the next. They mutate the same shared @wix/stores tree as the creates above, so concurrent calls risk the same 409 INVALID_REVISION race. Serial is the safe, deterministic choice (the category count is small).
The request body is items (each with the product's catalogItemId and the Stores appId) plus a treeReference at the same level as items (not nested):
{
"items": [
{ "catalogItemId": "<productId>", "appId": "215238eb-22a5-4c36-9e7b-e7c08025e04e" }
],
"treeReference": {
"appNamespace": "@wix/stores",
"treeKey": null
}
}Attach images (imagery ON only — skip otherwise)
Only when imagery is on (SEED.md § "Entity images"). Products were created text-only above; this pass-2 step writes a generated brand image onto each. Generate + import per references/IMAGE_GENERATION.md, keep each image's url.
Attach every product in ONE call — POST https://www.wixapis.com/stores/v3/bulk/products/update (up to 100 products). You already hold each product's id and revision from STEP 2's bulk-create response (productResults.results[].item), so no per-product GET is needed. Each entry nests under a product wrapper carrying just id, revision, and media — options, variants, and every other field are left untouched (it's a partial update):
{
"products": [
{ "product": {
"id": "<product id>",
"revision": "<from the create response>",
"media": { "itemsInfo": { "items": [ { "url": "<static.wixstatic.com/… or Wix Media id>", "altText": "…" } ] } }
} }
]
}revisionis required on every entry — omitting it fails400 "revision must not be empty". Use the revision from the create response (or a fresh query); a stale one fails the optimistic-concurrency check.Everything nests under
product—id/revision/mediaat the entry root fails validation. Send no field mask (the validator runs before masking →428).Write the image via
media.itemsInfo.items; the server derivesmedia.mainfrom it. One primary image per product (a larger gallery is out of scope for the seed). The image may be referenced by astatic.wixstatic.comurlor by its Wix Mediaid(<hash>~mv2.png) — either works.Success shape —
bulkActionMetadatatotals the run, and each result carries anitemMetadata:{ "results": [ { "itemMetadata": { "id": "<product id>", "originalIndex": 0, "success": true } } ], "bulkActionMetadata": { "totalSuccesses": 1, "totalFailures": 0, "undetailedFailures": 0 } }Read
itemMetadata.successper product (the result carries no product body). Retry only the failed ids once, then move on.Confirm by requery —
POST …/stores/v3/products/querywith"fields": ["MEDIA_ITEMS_INFO"]; the attached image is atmedia.main.image.url. Wix re-hosts the image on attach, so that URL is a newwixstatic.comid — verify it's a wixstatic URL, never string-equality against the one you sent. The read-back can lag a beat; one empty read right after success isn't a failure.Never block on image failure — a product that won't attach stays text-only.
One-off fix (single product). To (re)attach a single image later, PATCH https://www.wixapis.com/stores/v3/products/{id} with the same { "product": { "id", "revision", "media": {…} } } body — same rules apply (revision required, no field mask, media-only is a partial update that leaves options/variants intact). Read the current revision from GET …/products/{id} → response.product.revision (the GET is not flat); the 200 response carries the image at media.main.url.
Conclusion
Following these steps in order sets up a new V3 Wix Online Store site:
- Starts from a clean catalog — the install's default sample products are removed first.
- Contains the products and categories called for by the request, all created
visible: trueso they appear on the live site. - Each product is connected to the category it belongs to.
- All products follow the correct Catalog V3 API format specified in STEP 2.