> ## 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.

# Resolve a product by URL

> Resolve a product page URL from StoreRouter's index or, when needed, with bounded on-demand product resolution.

Product Lookup turns one product page URL into a `MerchantProductView`. By default, StoreRouter checks the product index first. If the URL is not indexed and on-demand product resolution is available, StoreRouter fetches the page and returns the useful product fields it can verify.

The response always identifies its source:

* `source: "indexed"` returns the full indexed product record and catalog context.
* `source: "on_demand"` returns a partial-safe product view without inventing an indexed UUID, catalog ownership, or lifecycle state.

## Choose a resolution mode

| `resolutionMode` | Behavior | Use it when |
| - | - | - |
| `auto` | Check the index, then use on-demand resolution on a miss. This is the default. | A user gives you a product URL and either source is acceptable. |
| `index_only` | Check only the catalogs available to your API key. Never fetch the product page. | You require indexed identity, predictable index latency, or the previous lookup behavior. |
| `on_demand_only` | Skip the index and resolve the current product page. | You explicitly need fresh page metadata or are testing the on-demand path. |

Requests that can use on-demand resolution also accept `onDemandCachePolicy`:

| `onDemandCachePolicy` | Behavior |
| - | - |
| `prefer_cache` | Reuse a recent result when available. This is the default. |
| `refresh` | Request a fresh resolution. Safety checks and service-wide rate limits still apply. |

`refresh` does not apply to `index_only` requests.

## Make a lookup request

<CodeGroup>
  ```bash curl theme={null}
  curl -sS https://api.octogen.ai/v1/products/lookup \
    -H "Authorization: Bearer $OCTO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://example.com/products/blue-linen-dress",
      "resolutionMode": "auto",
      "onDemandCachePolicy": "prefer_cache"
    }'
  ```

  ```python python theme={null}
  import asyncio

  from octogen_ai_sdk import OctogenClient


  async def main() -> None:
      async with OctogenClient() as client:
          result = await client.lookup_product(
              "https://example.com/products/blue-linen-dress",
              resolution_mode="auto",
              on_demand_cache_policy="prefer_cache",
          )

      product = result.product
      if result.source == "indexed":
          print("Indexed product", product.uuid, result.catalog_display_name)
      else:
          print("On-demand product", product.title, product.currency)
          print("Completeness", result.resolution.completeness if result.resolution else None)


  asyncio.run(main())
  ```

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

  const client = new OctogenClient();
  const result = await client.lookupProduct(
    "https://example.com/products/blue-linen-dress",
    {
      resolutionMode: "auto",
      onDemandCachePolicy: "prefer_cache",
    }
  );

  const { product } = result;
  if (result.source === "indexed") {
    console.log("Indexed product", product.uuid, result.catalogDisplayName);
  } else {
    console.log("On-demand product", product.title, product.currency);
    console.log("Completeness", result.resolution?.completeness);
  }
  ```
</CodeGroup>

<Note>
  On-demand resolution is synchronous and bounded. It reads the returned HTML but does not render JavaScript. Allow up to 8 seconds for a cold request, and use your normal timeout and retry policy for transient errors.
</Note>

## Handle indexed and on-demand results

Both sources return `product: MerchantProductView`, but some fields have different guarantees.

| Field | Indexed result | On-demand result |
| - | - | - |
| `source` | `"indexed"` | `"on_demand"` |
| `catalogKey`, `catalogDisplayName`, `sourceBaseUrl` | Catalog values | `null` |
| `product.uuid` | Stable indexed UUID | `null` |
| `product.catalogKey` | Supplying catalog | `null` |
| `product.isActive` | Indexed lifecycle state | `null` |
| `product.productUrl` | Required | Required |
| `requestedUrl` | The URL you submitted, echoed back | The URL you submitted, echoed back |
| `normalizedUrl` | The matched product's StoreRouter-normalized stable URL | `null` |
| `resolvedUrl` | `null` | Final URL after redirects |
| `canonicalUrl` | `null` | Canonical URL declared by the product page (falls back to the final fetched URL when the page declares none) |
| Other product fields | Full indexed record when available | Populated only when supported by page evidence |

For follow-up lookups, use `normalizedUrl` on indexed results and
`resolvedUrl` on on-demand results: submit either on a follow-up lookup and it
deterministically re-resolves the same product. Prefer storing them over the
URL you originally submitted.

On-demand results are ephemeral. StoreRouter does not add them to your catalogs or product index, and you cannot retrieve them later by UUID. Store the returned `resolvedUrl` and any fields your application needs.

### On-demand example

```json theme={null}
{
  "requestId": "req_01J...",
  "source": "on_demand",
  "catalogKey": null,
  "catalogDisplayName": null,
  "sourceBaseUrl": null,
  "requestedUrl": "https://example.com/products/blue-linen-dress",
  "resolvedUrl": "https://example.com/products/blue-linen-dress",
  "normalizedUrl": null,
  "canonicalUrl": "https://example.com/products/blue-linen-dress",
  "product": {
    "uuid": null,
    "catalogKey": null,
    "productUrl": "https://example.com/products/blue-linen-dress",
    "title": "Blue Linen Dress",
    "brand": {"name": "Example"},
    "currentPrice": 128,
    "currency": "USD",
    "imageUrl": "https://example.com/images/blue-linen-dress.jpg",
    "images": ["https://example.com/images/blue-linen-dress.jpg"],
    "isActive": null
  },
  "resolution": {
    "completeness": "partial",
    "method": "json_ld",
    "rendered": false,
    "missingFields": ["inStock"]
  },
  "cacheStatus": "miss",
  "warnings": []
}
```

`resolution.completeness` is `complete` or `partial`. `resolution.method` identifies the strongest evidence used: `json_ld`, `open_graph`, `html_meta`, or `resolved_url`. `missingFields` and `warnings` tell you which expected fields were unavailable or need attention. Do not treat `partial` as an error when the fields your application needs are present.

## Preserve indexed-only behavior

If your integration assumes every result has a stable UUID, catalog context, and lifecycle state, opt out of the default fallback:

```json theme={null}
{
  "url": "https://example.com/products/blue-linen-dress",
  "resolutionMode": "index_only"
}
```

An index-only miss returns `404 product_not_found`, matching the previous Product Lookup behavior.

## Handle errors

| Status | `detail` | Action |
| - | - | - |
| `404` | `product_not_found` | The selected sources did not produce a useful product. Check the URL or change the resolution mode. |
| `422` | `invalid_product_url` or `unsafe_url` | Fix the URL. StoreRouter will not resolve unsafe or unsupported targets. |
| `429` | `rate_limit_exceeded` or `product_lookup_rate_limited` | Wait for the `Retry-After` interval, then retry with backoff. |
| `502` | `product_lookup_upstream_failed` | The merchant page failed upstream. Retry with backoff. |
| `503` | `product_lookup_fallback_unavailable` | On-demand resolution is temporarily unavailable. Retry later or use `index_only`. |
| `504` | `product_lookup_timed_out` | The bounded lookup deadline expired. Retry with backoff. |

All Product Lookup calls use the service-wide API budget described in [Rate Limits](/docs/guides/rate-limits). See [Error Handling](/docs/guides/error-handling) for the SDK exception model and general retry guidance.


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