> Portal Navigation:
> 
> - Append `.md` to any URL under `https://dev.wix.com/docs/` to get its markdown version.
> - Pages are either content pages (article or reference text) or menu pages (a list of links to child pages).
> - To get a menu page, truncate any URL to a parent path and append `.md` (e.g. `https://dev.wix.com/docs/sdk.md`, `https://dev.wix.com/docs/sdk/core-modules.md`).
> - Top-level index of all portals: https://dev.wix.com/docs/llms.txt
> - Full concatenated docs: https://dev.wix.com/docs/llms-full.txt

## Resource: Shop and Buy a Product

## Article: Shop and Buy a Product

## Article Link: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/skills/shop-and-buy-a-product.md

## Article Content:

# 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

| Input | How to get it |
|---|---|
| Visitor token | STEP 1 (`GenerateVisitorToken`, or reuse one already in the conversation) |
| Product `id`, `slug`, price range, stock status, options | STEP 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 `id`s (for quantity changes / removal) | STEP 5 response (`cart.lineItems[].id`) |
| `checkoutId` | STEP 7 (`POST /ecom/v1/carts/current/create-checkout`) |

## 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.
- **"Show me only the <category>"** → STEP 1 + STEP 3 (find the category) + STEP 2b (search by category).
- **"What sizes/colours/scents?" / "Is <colour> 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 `id`s 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`

```bash
curl -X POST 'https://www.wixapis.com/stores/v3/products/query' \
-H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{
  "query": { "filter": { "visible": true }, "paging": { "limit": 50 } },
  "fields": ["CURRENCY", "PLAIN_DESCRIPTION"]
}'
```

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)

```jsonc
{
  "products": [{
    "id": "f5007949-2d28-48e4-b0b2-6431b2badc77",   // ← productId for STEP 4 and catalogItemId for STEP 5
    "name": "Focus Candle",
    "slug": "focus-candle",
    "visible": true,
    "plainDescription": "A slow-burning soy candle…",  // only with PLAIN_DESCRIPTION
    "currency": "ILS",                                 // only with CURRENCY
    "actualPriceRange":    { "minValue": { "amount": "65.00", "formattedAmount": "₪65.00" }, "maxValue": { … } },
    "compareAtPriceRange": { "minValue": { "amount": "140.00", "formattedAmount": "₪140.00" } },  // present → "on sale"
    "inventory": { "availabilityStatus": "IN_STOCK" },  // IN_STOCK | OUT_OF_STOCK | PARTIALLY_OUT_OF_STOCK
    "options": [{ "id": "…", "name": "Scent", "optionRenderType": "TEXT_CHOICES",
                  "choicesSettings": { "choices": [{ "name": "Cedar & Sage" }, { "name": "Fig" }, { "name": "Sea Salt" }] } }],
    "media": { "main": { "image": "wix:image://v1/…" } }   // may be absent
  }],
  "pagingMetadata": { "count": 8 }
}
```

- 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`).

```bash
curl -X POST 'https://www.wixapis.com/stores/v3/products/search' \
-H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{
  "search": { "filter": { "directCategoriesInfo.categories": { "$matchItems": [{ "id": "<CATEGORY_GUID>" }] } } },
  "fields": ["CURRENCY"]
}'
```

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 3: Categories (only when the visitor names one)

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

```bash
curl -X POST 'https://www.wixapis.com/categories/v1/categories/query' \
-H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{
  "query": { "filter": { "name": { "$exists": true } } },
  "treeReference": { "appNamespace": "@wix/stores", "treeKey": null }
}'
```

- `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).

```bash
curl -X POST 'https://www.wixapis.com/stores/v3/products/query-variants' \
-H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{ "query": { "filter": { "productData.productId": "<PRODUCT_GUID>" }, "paging": { "limit": 50 } } }'
```

```jsonc
{
  "variants": [{
    "variantId": "…",                                       // ← options.variantId in STEP 5 (NOT "id")
    "optionChoices": [{ "optionChoiceNames": { "optionName": "Scent", "choiceName": "Fig" } }],
    "price": { "actualPrice": { "amount": "65.00", "formattedAmount": "₪65.00" } },
    "inventoryStatus": { "inStock": true }                  // ← per-choice stock
  }]
}
```

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`

```bash
curl -X POST 'https://www.wixapis.com/ecom/v1/carts/current/add-to-cart' \
-H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{
  "lineItems": [
    { "catalogReference": { "catalogItemId": "<PRODUCT_GUID>", "appId": "215238eb-22a5-4c36-9e7b-e7c08025e04e", "options": { "variantId": "<VARIANT_ID>" } }, "quantity": 1 },
    { "catalogReference": { "catalogItemId": "<PRODUCT_GUID_2>", "appId": "215238eb-22a5-4c36-9e7b-e7c08025e04e", "options": { "variantId": "<VARIANT_ID_2>" } }, "quantity": 2 }
  ]
}'
```

```jsonc
{ "cart": {
  "id": "…", "currency": "ILS",
  "lineItems": [{
    "id": "4f4dc9f5-…",                                   // ← line id for STEP 6
    "productName": { "original": "Focus Candle" },
    "quantity": 1,
    "price": { "amount": "65.00", "formattedAmount": "₪65.00" },
    "descriptionLines": [{ "name": { "original": "Scent" }, "plainText": { "original": "Fig" } }],
    "availability": { "status": "AVAILABLE" }
  }]
} }
```

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

```bash
# quantity
curl -X POST 'https://www.wixapis.com/ecom/v1/carts/current/update-line-items-quantity' -H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{ "lineItems": [{ "id": "<LINE_ID>", "quantity": 1 }] }'
# remove
curl -X POST 'https://www.wixapis.com/ecom/v1/carts/current/remove-line-items' -H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{ "lineItemIds": ["<LINE_ID>"] }'
# totals (the cart entity itself has no totals)
curl -X POST 'https://www.wixapis.com/ecom/v1/carts/current/estimate-totals' -H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' -d '{}'
# read
curl 'https://www.wixapis.com/ecom/v1/carts/current' -H 'Authorization: <VISITOR_TOKEN>'
```

`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`

```bash
curl -X POST 'https://www.wixapis.com/ecom/v1/carts/current/create-checkout' \
-H 'Authorization: <VISITOR_TOKEN>' -H 'Content-Type: application/json' \
-d '{ "channelType": "WEB" }'
```

→ `{ "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`

```json
{ "ecomCheckout": { "checkoutId": "<CHECKOUT_ID>" },
  "callbacks": { "postFlowUrl": "https://<site>/cart", "thankYouPageUrl": "https://<site>/order-confirmation" } }
```

→ `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".

---

## Constants

| Constant | Value |
|---|---|
| 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

- [Query Products (V3)](https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/query-products.md)
- [Search Products (V3)](https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/search-products.md)
- [Query Variants (V3)](https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/read-only-variants-v3/query-variants.md)
- [Query Categories](https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/categories/query-categories.md)
- [Add To Current Cart](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart/current-cart/add-to-current-cart.md)
- [Estimate Current Cart Totals](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart/current-cart/estimate-current-cart-totals.md)
- [Create Checkout From Current Cart](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart/current-cart/create-checkout-from-current-cart.md)
- [Get Checkout](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/checkout/checkout/get-checkout.md)
- [Create Redirect Session](https://dev.wix.com/docs/api-reference/business-management/redirects/redirect-session/create-redirect-session.md)