# Wantly catalog guide for AI agents

How a Wantly company account (tenant) stores its product catalog, and how to change it safely with the catalog actions. Read this before making bulk changes.

The same actions are available four ways. They behave identically:

| Way | Who uses it | How |
|-----|-------------|-----|
| MCP | External AI models (Claude, Cursor, ChatGPT connectors…) | `POST https://{tenant}.wantly.eu/api/mcp`, `Authorization: Bearer <key>` |
| API v2 | Integrations, scripts | `POST https://{tenant}.wantly.eu/api/v2/{action}` with a JSON body, same key |
| In-app AI | AI tools inside Wantly, as the logged-in user | `POST /ai/actions/{action}` (browser session, no key) |
| Server CLI | AI tools running on the server | `php artisan wantly:action {action} --input='{json}' [--dry-run]` |

Machine-readable list of actions and their input schemas: `GET /api/v2/openapi.json` (or MCP `tools/list`, or `php artisan wantly:action --list`). This guide is also available as the `guide_read` action, the MCP resource `wantly://guides/catalog`, and `GET /api/v2/guide`.

---

## 1. Ground rules

1. **Preview first.** Every write action accepts `"dry_run": true`: it runs completely, returns the result, then rolls everything back. Always dry-run merges, category merges, deletes and any change touching more than a few products, and check the result before running it for real.
2. **Products are addressed by id or SKU.** In lists, JSON numbers are ids and JSON strings are SKUs. SKUs can be all digits (`"112021"`), so send them as strings.
3. **Do not guess facts.** If a colour, size or model is not in the product name, its attributes or the supplier's data, ask a human instead of inventing a value.
4. **Prefer the human's source data.** Supplier price lists, catalogs and the supplier's website are more reliable than codes inside SKUs.
5. **Report, then act.** For large changes, show the plan (groups, names, values) and get approval before applying.
6. **Errors are data.** A failed action returns `{"ok": false, "error": {"code", "message", "details"}}`. Read `details`: unknown fields are listed, missing products are named, and conflicts include the conflicting record.

---

## 2. Data model

### Products

| Field (action output) | Meaning |
|-----------------------|---------|
| `id`, `sku`, `name` | Identity. **SKU is unique across all products, including trashed ones.** A trashed product's SKU cannot be reused. |
| `variable` | `false` = simple product. `true` = it has variations (child items with their own SKU, price and images). |
| `category`, `categories` | Main category, plus extra categories (a product can be in several). |
| `price` | Sale price at the smallest quantity tier (not an average). `null` for variable products: their variations have the prices. |
| `min_quantity`, `price_tiers` | Only when the product has quantity tiers or a minimum above 1: `[{min_quantity, price}]`, e.g. 2.00 from 10 pcs, 1.50 from 100 pcs. Below the smallest tier there is no cheaper price: a proposal line uses the smallest tier and reports a warning. |
| `price_raw` | Full price structure: `{"<supplier>": {"<min quantity>": [cost, sale, delivery, discount %]}}`, e.g. `{"First Austria": {"1": [0, 6.94, 0, 0]}}`. |
| `description`, `description_long` | Short and long description (HTML). |
| `attributes` | Attribute values of this product (see Attributes). |
| `tags` | Labels such as "New" or "Sale". |
| `images` | Gallery; the first image is the main image. |
| `variation_attributes` | For variable products: the attributes the variations differ by (max 3). |
| `fixed_attributes` | "Preset value" attributes: fixed when the product is added to a proposal. |
| `deleted` | `true` = in the trash. Deleting is always a soft delete; `products_restore` brings products back. |

### Variations

A variable product has variations. Each variation has its own `sku`, price, images and an optional name, and exactly **one value per variation attribute**, e.g. Model `FA-5417-6` + Color `Cream`. Every combination of values is unique within the product.

### Attributes (product features)

Shared definitions such as Color, Size, Brand or Material, with a `type`:

| type | Meaning | Usable as variation attribute |
|------|---------|-------------------------------|
| 0 | Select value: a shared list of values (Size: S, M, L) | yes |
| 1 | Colour select | yes |
| 2 | Free text per product | no |
| 3 | Amount calculation | no |
| 4–7 | Included products, related products, related presets, print settings | no — leave these alone |

- Types 0 and 1 have **catalog values** (one shared list per attribute); products point to those values. `attributes_add_values` adds to the list without duplicating (case-insensitive).
- One attribute is **either a variation attribute or a fixed (preset) attribute on a product, never both**. The tools enforce this.
- Tenants often have look-alike attributes in several languages (`Color`, `Colour`, `Spalva`). Prefer the one that already has the most values and is used on similar products (`attributes_list` shows `value_count`). Do not create another one without asking.
- Packaging and logistics attributes (EAN, carton weight, pallet quantity…) usually differ per product. When products are merged, such differing attributes cannot be kept on the parent; `products_merge_preview` lists them under `dropped_differing`.

### Categories

A tree: each category has a `parent_id` (0 = top level). A product has one main category and any number of extra categories. `categories_tree` returns the tree, or a flat list with paths, with the number of active products directly in each category.

### Tags

Simple labels (`tags_list`, `tags_create`) attached to products with `products_tag` (add or remove).

### Images

All images live in one library: the original plus thumbnail, small (255 px) and medium (900 px) versions. A product has a main image and a gallery; each variation has its own images.

- `products_set_images` with `urls` downloads images into the library and attaches them. An URL that was imported before is reused, not downloaded again.
- Use `variation_sku` to set one variation's images.
- Modes: `replace` (default), `append`, `prepend`. The first image becomes the main image.
- In dry runs, images are not downloaded; the response lists them under `not_downloaded_in_dry_run`.

---

## 3. Workflows

### 3.1 Merge separate products into one product with variations

Typical cases: the same item imported once per colour or size ("Kettle 1.7L - Blue", "Kettle 1.7L - White"), or once per model with identical names that differ only by SKU suffix.

1. **Find candidates.** Use `products_find_same_name` (identical names), `products_search` with an SKU prefix (`"sku": "005417*"`), or text search.
2. **Understand the difference.** Run `products_merge_preview` with the products. Check `name_difference` and `suggested_values` for each product, plus `recommended_attributes`, `kept_on_parent` and `dropped_differing`. If the names are identical, use the product attributes (`products_get`), the SKU pattern, or the supplier's catalog or website.
3. **Choose 1–3 variation attributes**, e.g. `["Color"]`, `["Size", "Color"]` or `["Model", "Color"]`, and one value per product per attribute. Values are plain text; missing catalog values are created.
4. **Dry-run `products_merge`** and check the parent's name, SKU and the variation grid.
5. **Run it for real**, then verify with `products_get`.

Rules:

- **A family that varies by two things must be merged once, with two attributes.** Never merge it per size or per colour: a variable product cannot be merged again.
- **Variation values are removed from the parent name automatically** ("Mug 300ml - Red" → "Mug 300ml"). Pass `name` to set it yourself.
- The parent SKU defaults to the shared SKU prefix (cut at a separator, e.g. `005417-`) or a generated code. Pass `sku` to choose. It must not be used by any product (trashed included) or variation.
- `images: "variations"` (default) keeps each variation's own pictures and gives the parent a gallery with the first image of each variation (the first variation's image is the main image). Keep this rule when setting images later: a variable product's gallery holds one image per variation.
- The original products go to the trash. Their SKUs now belong to the variations, so imports and `products_restore` treat them as existing.
- **True duplicates** (same product, same attributes, only a different SKU or a "V2") are not variations. Report them instead of merging.
- **SKU colour codes are unreliable.** For example, `BL` has meant Blue, Black and Light Blue on different products of the same supplier. Use the product's own colour attribute, the photo, or the supplier catalog to name a colour. When sources conflict, ask.

Example (`POST /api/v2/products_merge`):

```json
{
  "attributes": ["Model", "Color"],
  "variations": [
    { "product": "005417-5-BU", "values": ["FA-5417-5", "Blue"] },
    { "product": "005417-5-WI", "values": ["FA-5417-5", "White"] },
    { "product": "005417-6-BA", "values": ["FA-5417-6", "Black"] },
    { "product": "005417-6-CR", "values": ["FA-5417-6", "Cream"] }
  ],
  "sku": "005417",
  "dry_run": true
}
```

### 3.2 Clean up categories

1. Get the structure with `categories_tree` (`"format": "flat"` gives paths and counts).
2. Propose 8–15 top-level groups by product type, not by supplier or brand, unless the human prefers brands.
3. Create the missing parents (`categories_create`), move categories under them (`categories_update` with `parent_id`), rename (`categories_update` with `name`).
4. Merge duplicates (`categories_merge`): products move to the target and the sources are deleted. Use `move_subcategories: true` when a source has children.
5. Delete empty leftovers (`categories_delete` only works on empty categories).
6. Dry-run the merges first; check that no category ends up empty by accident.

### 3.3 Add images

1. Find the official image URL. Supplier websites often follow a pattern based on the product code; check one URL before using the pattern for many products.
2. `products_set_images` with `urls` for a product, or with `variation_sku` for a single variation.
3. Verify with `products_get` (`images`, or `variations[].images`).

### 3.4 Fold duplicate products into the real one

The same item is sometimes imported twice, e.g. once from the supplier's order list (right name, price, EAN, no photo) and once from their catalog (photo and specs, no price).

1. Match the pairs by a reliable key: supplier code, EAN, or an SKU pattern (`FA-5043-1` = `005043-1`). Check a few by hand.
2. Run `products_absorb_duplicate` with `source` (the duplicate) and `target` (the product to keep). It copies attributes the target does not have (existing values are never overwritten), the images (by default only if the target has none) and tags, then trashes the duplicate.
3. If the real product is already a variation, use `target_variation_sku`: the images go to that variation. Attributes go to the variable parent only with `attributes_to_parent: true`, and only when they are true for all its variations.
4. To fold a duplicate *variable* product, absorb each of its variations into the matching target (`source_variation_sku` → `target` / `target_variation_sku`), then absorb the duplicate parent itself.
5. Do absorptions before merging: the merge then carries the shared attributes to the new parent and keeps each variation's photo.

### 3.5 Bulk edits

- Find with `products_search` (combine `query`, `sku`, `category_id`, `variable`, `has_image`; page with `limit` max 200 and `offset`).
- Change with `products_update` (name, SKU, descriptions, categories, `recurring` for subscriptions such as monthly modules, `unit` / `unit2`, `vat`; with `variation_sku` it renames or re-SKUs one variation), `products_set_price` (cost and sale for a quantity tier, also per variation), `products_tag`, `products_set_images`.
- Create a new product with `products_create`: `name`, `sku`, `category_ids` (+ `primary_category_id`), optional descriptions, `attributes` / `fixed_attributes` (`[{attribute, value}]` or `values` for select attributes; fixed = a preset value such as Brand, never also a variation attribute), `image_ids` / `image_urls`. A simple product takes `cost` / `sale` (`min_quantity`, `supplier`). A variable parent takes `variation_attributes` (1–3 select/colour attributes) and optional `variations` (`variation_sku`, `values`, `cost`, `sale`, images — the same rules as `products_add_variation`); it has no price of its own. Fires the product-created webhook (not in a dry run).
- Add a missing variation to an existing variable product with `products_add_variation`: parent `id` or `sku`, `variation_sku`, `values` (one text value per variation attribute, in the order of the parent's `variation_attributes` from `products_get`; missing values are created), `cost`, `sale` (optional `min_quantity`, `supplier` — default = the other variations' supplier key — `name`, `image_ids`, `image_urls`). The SKU must be free (trashed products included) and the value combination new on that parent; the parent gallery gets the first image of every variation and the display price is recalculated. Run it with `dry_run` first.
- Move to the trash with `products_delete`; undo with `products_restore` (needs product ids).

---

## 4. Error codes

| code | HTTP | Meaning |
|------|------|---------|
| `invalid_input` | 422 | Wrong or unknown field; `details.errors` lists each problem. |
| `not_found` | 404 | Product, category, attribute, tag or image does not exist; `details` names it. |
| `conflict` | 409 | Name or SKU already used, category still has products or subcategories, restore blocked… |
| `forbidden` | 403 | The key or user lacks the scope (`catalog.read`, `catalog.write`, `catalog.delete`). |
| `unauthorized` | 401 | Missing or invalid API key. |
| `api_disabled` | 403 | The tenant's plan does not include API access. |
| `merge_not_possible`, `merge_failed` | 422 | The merge was refused (e.g. variable products selected, duplicate value combinations, SKU taken). |
| `image_download_failed` | 422 | The image URL could not be downloaded. |
| `unknown_action` | 404 | Check the action name. |
| `internal_error` | 500 | Unexpected; report it to a human with the input you sent. |

## 5. Not available here

Plan, billing, modules and API key settings cannot be changed through these actions, by design. Proposals and contacts still use the v1 API (`/api/getproposals`, `/api/createproposalfromcatalog`… see the API docs); catalog actions for them are planned.
