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

# Product Discovery

> Search any merchant catalog and fetch rich product details - variants, pricing, images, and availability

## Overview

Use product discovery to power search bars, product detail pages, and catalog browsers across any merchant on the internet.

Both `products.search` and `products.details` are **async jobs** - they return a `refId`, and results arrive once the background work completes. See [Polling](/v1/sdk/server/guides/polling) or [Webhooks](/v1/sdk/server/guides/webhooks) for how to handle the async pattern.

```mermaid theme={null}
sequenceDiagram
    participant App
    participant Henry

    App->>Henry: products.search() / products.details()
    Henry-->>App: { refId, status: "pending" }
    loop Poll until complete
        App->>Henry: products.poll*({ refId })
        Henry-->>App: { status: "processing" | "complete" | "failed" }
    end
    Henry-->>App: { status: "complete", result: { ... } }
```

***

## Prerequisites

```typescript theme={null}
import HenrySDK from '@henrylabs/sdk';

const henry = new HenrySDK({
	apiKey: process.env.HENRY_SDK_API_KEY!,
});
```

***

## Search the catalog

<Steps>
  <Step title="Kick off a product search">
    Call `products.search` with a text query and optional filters. Returns immediately with a `refId`.

    ```typescript theme={null}
    const search = await henry.products.search({
      type: 'global',
      filters: {
        type: 'text',
        query: 'Nike Air Max',
        country: 'US',
        price: { min: 50, max: 300, currency: 'USD' },
        sortBy: 'price-low-to-high',  // or 'price-high-to-low'
        supportedOnly: true,          // only checkout-supported merchants
      },
      limit: 20,              // 1–100, default 20
      mode: 'async',          // or 'sync' to wait up to 30s for results
    });

    console.log(search.refId, search.status);
    // → "prs-ref_3fa8..." "pending"
    ```
  </Step>

  <Step title="Poll for results">
    Use `products.pollSearch` to check status until it's `"complete"` or `"failed"`. See [Polling](/v1/sdk/server/guides/polling) for the reusable helper and strategies.

    ```typescript theme={null}
    const result = await henry.products.pollSearch({ refId: search.refId });
    // result.status: "pending" | "processing" | "complete" | "failed"
    ```
  </Step>

  <Step title="Read the results">
    ```typescript theme={null}
    const { products, pagination } = result.result!;

    for (const product of products) {
      console.log({
        name: product.name,
        price: `${product.price.value} ${product.price.currency}`,
        merchant: product.merchant,
        link: product.link,       // use this as the cart item `link`
        inStock: product.availability === 'in_stock',
      });
    }

    // Paginate with the cursor
    if (pagination.nextCursor) {
      const nextPage = await henry.products.search({
        type: 'global',
        filters: { type: 'text', query: 'Nike Air Max' },
        cursor: Number(pagination.nextCursor),
      });
    }
    ```
  </Step>
</Steps>

***

## Fetch product details

If you need more granular product information, such as available options and pricing, call `products.details` with the `link` from search for the full enriched payload.

<Steps>
  <Step title="Request product details">
    ```typescript theme={null}
    const detailJob = await henry.products.details({
      link: 'https://www.nike.com/t/air-max-270-shoes/AH8050-002',
      country: 'US',          // optional default country
      mode: 'async',          // or 'sync' to wait up to 30s for results
    });

    console.log(detailJob.refId, detailJob.status);
    // → "prd-ref_3fa8..." "pending"
    ```
  </Step>

  <Step title="Poll for results">
    Use `products.pollDetails` to check status. See [Polling](/v1/sdk/server/guides/polling) for the reusable helper.

    ```typescript theme={null}
    const detail = await henry.products.pollDetails({ refId: detailJob.refId });
    // detail.status: "pending" | "processing" | "complete" | "failed"
    ```
  </Step>

  <Step title="Read the enriched product">
    ```typescript theme={null}
    const product = detail.result;

    console.log({
      name: product?.name,
      price: product?.price,
      options: product?.options,    // recursive option tree with availability
      featuredImage: product?.images?.find(i => i.isFeatured)?.url,
    });
    ```
  </Step>
</Steps>

<Tip>
  Henry caches product details internally. If details for a `link` are already fresh, `pollDetails` will return `"complete"` on the very first call.
</Tip>

***

## Image search

Search by image instead of text. Pass a URL, data URL, or base64 payload to find visually similar products.

```typescript theme={null}
// Search with an image URL
const imageSearch = await henry.products.search({
	type: 'global',
	filters: {
		type: 'image',
		imageUrl: 'https://static.nike.com/a/images/t_web_pdp_535_v2/f_auto/example.png',
		country: 'US',
	},
	limit: 10,
	mode: 'async',
});

// Poll the same way as text search
const result = await henry.products.pollSearch({ refId: imageSearch.refId });
```

You can also pass a base64-encoded image:

```typescript theme={null}
const imageSearch = await henry.products.search({
	type: 'global',
	filters: {
		type: 'image',
		imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...',
		country: 'US',
	},
	limit: 5,
});
```

***

## Sync mode

All async operations support `mode: "sync"`, which waits up to 30 seconds for the operation to complete before returning. This can simplify your code for quick lookups by avoiding polling entirely.

```typescript theme={null}
// Sync mode - returns results directly (no polling needed)
const search = await henry.products.search({
	type: 'global',
	filters: { type: 'text', query: 'Nike Air Max' },
	limit: 5,
	mode: 'sync',
});

// If the search completed within 30s, results are already here
if (search.status === 'complete') {
	console.log(search.result?.products);
}
```

<Info>If the operation takes longer than 30 seconds, sync mode returns with a non-terminal status and you'll need to poll normally. For operations that may take longer (e.g. large searches), use `mode: "async"` with [Polling](/v1/sdk/server/guides/polling).</Info>

***

## Merchant search

Use `type: 'merchant'` to search within a specific merchant's catalog. Pass the merchant name, host, or any URL from that merchant.

```typescript theme={null}
const search = await henry.products.search({
	type: 'merchant',
	filters: { merchant: 'nike.com' },
	query: 'running shoes', // optional text query within the merchant
	limit: 20,
	mode: 'sync',
});
```

<Info>Merchant search accepts a case-insensitive name (e.g. `"Nike"`), a host (e.g. `"nike.com"`), or any URL from the merchant. When `query` is omitted, the merchant's catalog browse is returned.</Info>

***

## Search parameters reference

### Top-level parameters

| Parameter | Type                     | Description                                                                          |
| --------- | ------------------------ | ------------------------------------------------------------------------------------ |
| `type`    | `"global" \| "merchant"` | **Required.** `"global"` for cross-merchant search, `"merchant"` for single-merchant |
| `filters` | `object`                 | **Required.** Search filters (see below)                                             |
| `query`   | `string`                 | Optional text query (merchant search only)                                           |
| `limit`   | `number`                 | Results per page - `1` to `100`, default `20`                                        |
| `cursor`  | `integer`                | Pagination cursor from a previous response                                           |
| `mode`    | `"async" \| "sync"`      | `"async"` (default) returns immediately; `"sync"` waits up to 30s                    |

### Global search filters (`type: 'global'`)

| Parameter                | Type                                         | Description                                                                                                                                                                                                                        |
| ------------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filters.type`           | `"text" \| "image"`                          | **Required.** Text query or image-based search                                                                                                                                                                                     |
| `filters.query`          | `string`                                     | **Required for text.** Full-text search term                                                                                                                                                                                       |
| `filters.imageUrl`       | `string`                                     | **Required for image.** HTTP URL, data URL, or base64 payload                                                                                                                                                                      |
| `filters.country`        | `string`                                     | ISO country code (e.g. `"US"`, `"JP"`)                                                                                                                                                                                             |
| `filters.price.min`      | `number`                                     | Minimum price filter (text search only)                                                                                                                                                                                            |
| `filters.price.max`      | `number`                                     | Maximum price filter (text search only)                                                                                                                                                                                            |
| `filters.price.currency` | `string`                                     | Currency for price filter (e.g. `"USD"`)                                                                                                                                                                                           |
| `filters.sortBy`         | `"price-low-to-high" \| "price-high-to-low"` | Sort by price (text search only)                                                                                                                                                                                                   |
| `filters.supportedOnly`  | `boolean`                                    | Only return products from merchants where checkout is supported (`coverageStatus: "supported"` - see [Merchants](/v1/sdk/server/guides/merchant)). Applied after the search runs, so a page may contain fewer than `limit` results |

<Info>
  Global search results are ordered by merchant checkout coverage: products
  from `supported` merchants rank first, then `testing`, then `unsupported`
  (the search engine's order is preserved within each tier). Passing `sortBy`
  disables this ranking - an explicit price sort is kept as-is. To exclude
  non-supported merchants entirely, set `filters.supportedOnly: true`.
</Info>

### Merchant search filters (`type: 'merchant'`)

| Parameter                | Type     | Description                                                   |
| ------------------------ | -------- | ------------------------------------------------------------- |
| `filters.merchant`       | `string` | **Required.** Merchant name, host, or URL (e.g. `"nike.com"`) |
| `filters.price.min`      | `number` | Minimum price filter                                          |
| `filters.price.max`      | `number` | Maximum price filter                                          |
| `filters.price.currency` | `string` | Currency for price filter                                     |

***

## Error handling

| Error              | Cause                                    | Resolution                    |
| ------------------ | ---------------------------------------- | ----------------------------- |
| `401 Unauthorized` | Invalid API key                          | Check `HENRY_SDK_API_KEY`     |
| `400 Bad Request`  | Missing `query` or invalid filter values | Validate input before calling |
| `status: "failed"` | Job error - check `error` field          | Log and retry                 |

***

## Next steps

<CardGroup cols={2}>
  <Card title="Universal Cart" icon="cart-shopping" href="/v1/sdk/server/guides/universal-cart">
    Use the product `link` to add items to a cart
  </Card>

  <Card title="Merchants" icon="store" href="/v1/sdk/server/guides/merchant">
    Browse supported merchants beforehand
  </Card>
</CardGroup>
