> ## Documentation Index
> Fetch the complete documentation index at: https://www.octogen.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# POST /products/more-like-this — find similar products

> Find products similar to a source product — identified by URL, UUID, or an image — across all active crawled catalogs, or restricted to a catalog allowlist, using product enrichment and relative price preferences.

`POST /products/more-like-this` returns products similar to a source product. The API resolves the source by URL or UUID within your catalog grants, builds the similarity query from indexed product enrichment, excludes the source product from results, and returns a standard product list page. The source can also be an **image** — see [Image sources](#image-sources).

Use this endpoint for related product rails, substitutions, recommendation modules, and agent workflows that start from a known product. To see the strategies side by side with live results, open the [interactive demo](https://storerouter.ai/demos/mlt-api.html) or the [strategy recipes guide](/docs/guides/mlt-strategy-recipes). For the image input, open the [image input demo](https://storerouter.ai/demos/mlt-image-api.html).

## Request

```http theme={null}
POST https://api.octogen.ai/v1/products/more-like-this
Authorization: Bearer <your-platform-api-key>
Content-Type: application/json
```

### Body parameters

<ParamField body="source" type="object" required>
  Source identifier. Provide exactly one of `source.url`, `source.uuid`, or `source.image`.
</ParamField>

<ParamField body="source.url" type="string">
  Canonical product URL to use as the source product.
</ParamField>

<ParamField body="source.uuid" type="string">
  Indexed product UUID to use as the source product.
</ParamField>

<ParamField body="source.image" type="object">
  Source image to derive the similarity query from. Provide exactly one of `source.image.b64` or `source.image.url`. Slower than product sources — see [Image sources](#image-sources) for behavior, guards, and latency.
</ParamField>

<ParamField body="source.image.b64" type="string">
  Base64-encoded image bytes. Decoded size is capped at 8 MB (`413` beyond it). Downscale client-side before sending (\~1024px JPEG) — the pipeline downscales internally, so larger uploads only add transfer time.
</ParamField>

<ParamField body="source.image.url" type="string">
  HTTPS image URL fetched server-side — any public host, so retailer product-image URLs can be passed directly. Redirects are not followed, the fetch validates that both DNS resolution and the connected peer are public addresses, and the same 8 MB cap applies while streaming.
</ParamField>

<ParamField body="catalogs" type="string[]">
  Optional catalog allowlist (1–100 keys) restricting **results** to these catalogs. Retrieval-only: the source product may live outside the allowlist and still resolves. Keys are not validated — an unknown or inaccessible key simply matches nothing. When omitted, search runs across all active crawled catalogs. With `debug: true`, the applied allowlist is echoed as `effectiveQuery.catalogs`. Replaces the former single-valued `catalog` field, which is now rejected with a `422`.
</ParamField>

<ParamField body="include_facets" type="Facet[]">
  Additional include facets appended after StoreRouter's server-generated audience facets.
</ParamField>

<ParamField body="exclude_facets" type="Facet[]">
  Facets to exclude from the similar-products search.
</ParamField>

<ParamField body="price_preference" type="&#x22;lower&#x22; | &#x22;any&#x22; | &#x22;higher&#x22;" default="any">
  Relative price preference compared with the source product's `currentPrice`. Use `lower` for less expensive alternatives, `higher` for premium alternatives, or `any` for no relative price filter. Image sources require `any` (`400` otherwise) — there is no source product to be price-relative to.
</ParamField>

<ParamField body="retrieval_embedding_columns" type="EmbeddingColumn[]">
  Embedding columns used to retrieve candidate products. Overrides the server-selected default: `["style_embedding", "tags_embedding"]` when the source product has styles or tags, otherwise `["embedding"]`. Accepted values: `embedding`, `style_embedding`, `tags_embedding`, `attributes_embedding`. Must contain at least one value.
</ParamField>

<ParamField body="ranking_embedding_columns" type="EmbeddingColumn[]">
  Embedding columns used to re-score and rank the retrieved candidates. When omitted, ranking uses the base `embedding`. Accepts the same values as `retrieval_embedding_columns`; must contain at least one value. Combine the two to retrieve one way and rank another — see [Strategy recipes](#strategy-recipes).
</ParamField>

<ParamField body="cursor" type="string">
  Opaque pagination cursor from a previous response's `nextCursor` field. Pass it unmodified with the same request fields to retrieve the next page. Supported for image sources: the cursor embeds the relaxation round that produced the page, so continuations re-run the exact converged query. Deterministic within the 24h generation-cache window.
</ParamField>

<ParamField body="limit" type="integer" default="12">
  Number of products to return per page. Accepted range: 1–100.
</ParamField>

<ParamField body="debug" type="boolean" default="false">
  When `true`, the response includes a curated camelCase `effectiveQuery` object showing the server-derived retrieval query.
</ParamField>

### Example

```bash theme={null}
curl -sS https://api.octogen.ai/v1/products/more-like-this \
  -H "Authorization: Bearer $OCTOGEN_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": {
      "url": "https://warrenlotas.com/products/black-hoodie"
    },
    "catalogs": ["warrenlotas"],
    "price_preference": "any",
    "limit": 12
  }'
```

### Embedding columns

Every indexed product carries several embeddings. Retrieval columns decide which products come back as candidates; ranking columns decide how those candidates are ordered.

| Column | Captures | Use it for |
| - | - | - |
| `style_embedding` | Aesthetic and styling signals | "Same look" — the default retrieval when the source has styles/tags |
| `tags_embedding` | Descriptive tags (details, materials, features) | Pairs with `style_embedding` in the default strategy |
| `attributes_embedding` | Structured attributes (closure, hardware, fit, …) | Maximum concrete attribute overlap |
| `embedding` | General-purpose product representation | Broad semantic discovery; the default ranking signal |

### Strategy recipes

| Goal | Request |
| - | - |
| Same look (default) | Omit both parameters |
| Closest attribute matches | `"retrieval_embedding_columns": ["attributes_embedding"]` |
| Broad semantic discovery | `"retrieval_embedding_columns": ["embedding"]` |
| Max attribute matches, ordered by look | `"retrieval_embedding_columns": ["attributes_embedding"], "ranking_embedding_columns": ["style_embedding"]` |
| Same look, cheaper | `"price_preference": "lower"` |

Send `debug: true` with any combination and the response's `effectiveQuery` shows the exact query the server executed. For worked examples of each strategy with live results, see the [strategy recipes guide](/docs/guides/mlt-strategy-recipes).

## Image sources

Send a photo — a product shot, a look to match — and the API generates the similarity query from the image: a multimodal parse reads it, the query processors derive facets (category, gender, color, attributes), and the search runs with zero-result facet relaxation. See it live in the [image input demo](https://storerouter.ai/demos/mlt-image-api.html).

```json theme={null}
{
  "source": { "image": { "b64": "<base64 image bytes>" } },
  "limit": 6,
  "debug": true
}
```

Behavior to know:

* **Latency**: the first sight of an image runs generation — expect p50 ≤ 8s, \~11s when relaxation rounds run. **Repeat images are served from a generation cache (24h) in \~0.25s**, and the response's `resolution` field says which happened: `image_query_generation` (pipeline ran) or `cached_image_query` (cached).
* **Facet relaxation**: every image-derived facet is an inference (including a `brand_name` read off a visible logo), so zero-result searches automatically retry with the least-trusted facet tiers dropped. With `debug: true` the response reports `relaxationRounds` and the released facets. Facets you pass in `include_facets` are never dropped.
* **Response shape**: image responses return `sourceImage.hash` and `resolution` instead of the resolved-product `source` stanza. `nextCursor` is a normal opaque cursor — pass it back with the same request body to page through the result set.
* **`include_facets` / `exclude_facets` / embedding-column overrides** work exactly as for product sources.

## Response

<ResponseField name="source" type="object">
  Public identity of the source product that powered the similarity query. Present for `url`/`uuid` sources; image sources return `sourceImage` instead.

  <Expandable title="Source properties">
    <ResponseField name="catalogKey" type="string" required>
      Catalog that supplied the source product.
    </ResponseField>

    <ResponseField name="uuid" type="string" required>
      Source product UUID.
    </ResponseField>

    <ResponseField name="productUrl" type="string" required>
      Canonical product URL for the source.
    </ResponseField>

    <ResponseField name="title" type="string | null">
      Source product title.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="sourceImage" type="object">
  Present for image sources only: `{ "hash": string }` — the content hash of the submitted image bytes.
</ResponseField>

<ResponseField name="resolution" type="string">
  Present for image sources only. How the source was resolved: `"image_query_generation"` (the generation pipeline ran) or `"cached_image_query"` (served from the 24h generation cache — a repeat of an image seen before).
</ResponseField>

<ResponseField name="items" type="MerchantProductListItem[]" required>
  Similar products for the current page. See the [Product model](/docs/api-reference/models/product) for the full field reference.
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Cursor for the next page of similar products. `null` means no more results are available.
</ResponseField>

<ResponseField name="effectiveQuery" type="object | null">
  Present only when `debug` is `true`. Contains the public server-derived query fields: `text`, `retrievalEmbeddingColumns`, `rankingEmbeddingColumns`, `facets`, `exclusionFacets`, `priceMin`, `priceMax`, `limit`, and — when a `catalogs` allowlist was sent — `catalogs` (the normalized allowlist as applied). For image sources it additionally carries `generationStages` (per-stage generation timings), `degradedToTextFallback`, and — when relaxation ran — `relaxationRounds` and `relaxationDroppedFacets`.
</ResponseField>

### Example response

```json theme={null}
{
  "source": {
    "catalogKey": "warrenlotas",
    "uuid": "prod_01HX...",
    "productUrl": "https://warrenlotas.com/products/black-hoodie",
    "title": "Black Hoodie"
  },
  "items": [
    {
      "uuid": "prod_01HY...",
      "catalogKey": "warrenlotas",
      "title": "Washed Black Hoodie",
      "productUrl": "https://warrenlotas.com/products/washed-black-hoodie",
      "imageUrl": "https://cdn.example.com/washed-black-hoodie.jpg",
      "currentPrice": 190,
      "isActive": true,
      "updatedAt": "2026-05-11T18:04:10Z"
    }
  ],
  "nextCursor": null
}
```

## SDK equivalents

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  from octogen_ai_sdk import OctogenClient

  async def main() -> None:
      async with OctogenClient() as client:
          page = await client.more_like_this_products(
              source_url="https://warrenlotas.com/products/black-hoodie",
              catalogs=["warrenlotas"],
              price_preference="any",
              limit=12,
          )
          for product in page.items:
              print(product.title, product.current_price)

  asyncio.run(main())
  ```

  ```typescript TypeScript theme={null}
  import { OctogenClient } from "@octogen-ai/sdk";

  const client = new OctogenClient();
  const page = await client.moreLikeThisProducts({
    source: { url: "https://warrenlotas.com/products/black-hoodie" },
    catalogs: ["warrenlotas"],
    pricePreference: "any",
    limit: 12,
  });

  for (const product of page.items) {
    console.log(product.title, product.currentPrice);
  }
  ```
</CodeGroup>

## Errors

The endpoint uses the standard Catalog API error model:

| Status | Meaning |
| - | - |
| `401` | Missing, malformed, or invalid Bearer API key. |
| `403` | API key is valid but not allowed to access this resource. |
| `404` | The source product is not visible for this API key. |
| `422` | Request validation failed, including missing source identifiers, both source identifiers being present, an invalid limit, an invalid `price_preference`, or an unknown embedding column. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.