RECIPE: Shop and Buy a Product

When to use this recipe

Any visitor request about a Wix Stores catalog or a purchase. Examples:

  • "What do you sell?" / "Do you have journals?" / "Show me only the Wear items"
  • "What colours does the journal come in? Is every colour in stock?"
  • "Is the Breathing Cards deck available?" / "Is anything on sale?"
  • "Add a Focus Candle in Fig and two mugs to my cart" / "Make it one mug, remove the candle"
  • "I'm ready to pay" / "Do you ship or can I pick up?"

Inputs you need before STEP 4

InputHow to get it
Visitor tokenSTEP 1 (GenerateVisitorToken, or reuse one already in the conversation)
Product id, slug, price range, stock status, optionsSTEP 2 (POST /stores/v3/products/query or /search)
Category id (only for "show me the X category")STEP 3 (POST /categories/v1/categories/query)
variantId for the chosen size/colour/scent (mandatory for any product with options; use the single variant otherwise)STEP 4 (POST /stores/v3/products/query-variants)
Cart line ids (for quantity changes / removal)STEP 5 response (cart.lineItems[].id)
checkoutIdSTEP 7 (POST /ecom/v1/carts/current/create-checkout)

Catalog version — check it first

Stores run on Catalog V3 or the older Catalog V1, and each answers only its own endpoints. The <site-context> block from WixREADME names it (Stores Catalog Version: V1_CATALOG). Without that block, a 428 from any /stores/v3/* call means the store is V1. On a V1 store, replace STEP 2–4 with STEP 2-V1 and build the cart line as STEP 2-V1 shows; STEP 5 onward is the same.

Decision tree — pick the shortest path

  • "What do you sell?" / "Do you have X?" → STEP 1 + STEP 2. Match client-side, case-insensitively, over name, plainDescription, option names. Offer to add to cart.
  • Requirements like "SPF 50 or higher", "under €20", "size 42" → read each candidate's name and description yourself and decide; do not extract the value with a regex. Names put numbers on either side ("Sunscreen 50 SPF", "SPF 30 Day Cream"), and a pattern that misses one makes you answer "none found" for a product the store sells.
  • "Show me only the " → STEP 1 + STEP 3 (find the category) + STEP 2b (search by category).
  • "What sizes/colours/scents?" / "Is in stock?" → STEP 2 for the product, then STEP 4 (variants carry per-choice stock).
  • "Is X available?" / "Anything on sale?" → STEP 2 only: inventory.availabilityStatus and compareAtPriceRange.
  • "Add X to my cart" → STEP 2 → STEP 4 (resolve variantId) → STEP 5 → STEP 6 (totals).
  • "Change quantity / remove" → STEP 6 with the line ids from the last cart response.
  • "I'm ready to pay" → STEP 7 (create checkout) → STEP 8 (redirect session). Never build /checkout?checkoutId= yourself (see Common errors #7).

All calls below run with the visitor token through CallWixSiteAPI / ExecuteWixAPI. Prefer CallWixSiteAPI for any write whose failure you need to explain — ExecuteWixAPI currently hides the real HTTP error (see Common errors #1).


STEP 1: Get a visitor token

Call GenerateVisitorToken once and reuse the access_token for every step. The cart is owned by the visitor identity: a new token means an empty cart and a 404 OWNED_CART_NOT_FOUND on checkout.


STEP 2: List or find products

POST https://www.wixapis.com/stores/v3/products/query

Copy

Filterable fields on this endpoint: id, slug, visible, handle, options.id, created/updated dates. name is not filterable — fetch up to 50–100 and match client-side. For one product by URL slug: "filter": { "slug": "breathing-cards" }.

What you need from the response (verified 2026-09-16)

Copy
  • Price to show: actualPriceRange.minValue.formattedAmount (already in the site currency). If min ≠ max say "from …".
  • "On sale" = compareAtPriceRange.minValue.amount > actualPriceRange.minValue.amount.
  • inventory.availabilityStatus === "OUT_OF_STOCK" → say "sold out"; still list it.
  • options[] tells you what the visitor must choose before STEP 5.

STEP 2b: Products in a category

POST https://www.wixapis.com/stores/v3/products/search — the only product endpoint that can filter by category (query rejects directCategoriesInfo).

Copy

Body is wrapped in search (a top-level filter is ignored). Operator is $matchItems with the key id; $hasSome is rejected. Response is { "products": [ … same shape as STEP 2 … ] }.

STEP 2-V1: Products on a Catalog V1 store

POST https://www.wixapis.com/stores-reader/v1/products/query

Copy
  • filter is a JSON string, not an object. Unlike V3, name is filterable: $contains and $startsWith both work and ignore case. Drop the filter to list the whole catalog (totalResults gives the count).
  • Search for the product noun the visitor used ("sunscreen"), not for the requirement ("SPF 50"); then check the requirement against the returned names.
Copy

Cart line for STEP 5 (same endpoint and appId), by product type:

ProductcatalogReference.options
No options (productOptions: [])omit options
manageVariants: true{ "variantId": "<VARIANT_ID from variants[].id>" }
manageVariants: false with options{ "options": { "Size": "Medium", "Color": "Red" } } (option name → choice description)

Verified 2026-10-25 on a V1 store: the name query returned "Sunscreen 50 SPF" and "Sunscreen 30 SPF", and add-to-cart with { "catalogItemId": "<id>", "appId": "215238eb-…" } and no options added it.


STEP 3: Categories (only when the visitor names one)

POST https://www.wixapis.com/categories/v1/categories/query

Copy
  • treeReference is top-level and mandatory. An empty filter is rejected (INVALID_FILTER), hence the $exists condition.
  • Response: { "categories": [{ "id": "8f01c2bf-…", "name": "Wear", "slug": "wear", "visible": true }] }. Match the visitor's word to name client-side ("clothes" → Wear).

STEP 4: Resolve the variant (size / colour / scent + per-choice stock)

POST https://www.wixapis.com/stores/v3/products/query-variants — not /stores/v3/variants/query (404).

Copy
Copy

Match the visitor's words to optionChoices[].optionChoiceNames.choiceName (case-insensitive, accept synonyms like "grey" for "Charcoal" only if you say so). A product without options has exactly one variant — use it. If the requested choice has inStock: false, offer the choices that are in stock.


STEP 5: Add to the cart

POST https://www.wixapis.com/ecom/v1/carts/current/add-to-cart

Copy
Copy

Same call works for Restaurants dishes with the Orders app id — a visitor can hold shop and café items in one cart (verified: Flat White + planner, ₪105).


STEP 6: Change quantities, remove lines, read totals

Copy

estimate-totals → { "cart": {…}, "priceSummary": { "subtotal": { "amount": "70.00", "formattedAmount": "₪70.00" }, "total": {…} } }. Quote formattedAmount; never hardcode a currency symbol.


STEP 7: Create the checkout

POST https://www.wixapis.com/ecom/v1/carts/current/create-checkout

Copy

→ { "checkoutId": "ca727402-…" }. GET https://www.wixapis.com/ecom/v1/checkouts/<checkoutId> then shows lineItems, priceSummary.total and shippingInfo.carrierServiceOptions[].shippingOptions[] (title, cost.price.formattedAmount, logistics.pickupDetails for pickup) — use it to answer "do you ship or can I pick up?".


STEP 8: Send the visitor to pay

Do not use the checkoutUrl returned by POST /ecom/v2/carts/<checkoutId>/get-checkout-url or GET /ecom/v1/checkouts/<id>/checkout-url on a headless site: it is https://<site>/checkout?checkoutId=…, a Wix-editor page that a headless frontend does not have (404). Create a redirect session instead:

POST https://www.wixapis.com/_api/redirects-api/v1/redirect-session

Copy

→ redirectSession.fullUrl is the hosted checkout. Say "pay here to complete the order" and stop: payment cannot be completed through the API, and on a site without a payment provider the hosted page will show that payment is unavailable.


Common errors and how to avoid them

1. ExecuteWixAPI says "Visitor token rejected (HTTP 403)" for every failure

The sandbox reports any non-2xx (400 ticketReservation must not be empty, 403 No payment method configured, 404) as a token problem. Do not mint a new token (you would lose the cart). Re-issue the single failing request through CallWixSiteAPI to read the real status and message.

2. POST /stores/v3/variants/query → 404

The variants endpoint is POST /stores/v3/products/query-variants. Docs search for "product variants" lists it as Query Variants.

3. 400 catalogItemId has size 0

You built the cart body before the product query resolved (or read _id off a REST response). Product ids are products[].id in REST.

4. 400 lineItems[0].id is not a valid GUID

Quantity updates need the cart line id from the last cart response, not the product id. Read the cart first if you no longer have it.

5. 404 OWNED_CART_NOT_FOUND on create-checkout

The cart belongs to a different visitor token (you minted a new one) or nothing was added. Reuse the token from STEP 1 and re-check GET /ecom/v1/carts/current.

6. Category filter returns nothing / 400 not declared as filterable

Use /products/search with "search": { "filter": { "directCategoriesInfo.categories": { "$matchItems": [{ "id": … }] } } }. query cannot filter by category, and $hasSome is not accepted.

7. Checkout URL points to /checkout?checkoutId=

That page exists only on Wix-editor sites. Use the redirect session (STEP 8).

8. Adding a product with options without variantId

Cart V1/V2 rejects the line. Always resolve the variant in STEP 4 first; a "no options" product still has one variant.

9. Treating checkoutId as an order

A checkout is unpaid. Only the hosted page (or POST /ecom/v1/checkouts/{id}/create-order after payment) produces an order. Never say "your order is placed".

10. 428 from /stores/v3/*

The store is on Catalog V1. Switch to STEP 2-V1; do not retry V3 endpoints or fall back to guessing.


Constants

ConstantValue
Wix Stores catalog appId (cart catalogReference.appId)215238eb-22a5-4c36-9e7b-e7c08025e04e
Categories tree reference{ "appNamespace": "@wix/stores", "treeKey": null }
Product read fields worth requesting["CURRENCY", "PLAIN_DESCRIPTION"]

Product, variant, category, line and checkout ids are per site and per visitor — discover them during the conversation.


References

Last updated: 25 October 2026

Did this help?