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

# Common Use Cases

> End-to-end recipes that combine product search, details, and checkout for the patterns app developers reach for most.

## Overview

Henry's server SDK exposes three primitives - `products.search`, `products.details`, and the `cart.*` family - that compose into a handful of recurring app patterns. Pick the flow below that matches where your users **start**:

| If your users start from...  | Use flow                                                               |
| ---------------------------- | ---------------------------------------------------------------------- |
| A text query                 | [Shopping agent / chatbot](#flow-1-shopping-agent--chatbot)            |
| A photo or screenshot        | [Visual / "shop the look" app](#flow-2-visual--shop-the-look-app)      |
| A merchant catalog feed      | [Product recommendation feed](#flow-3-product-recommendation-feed)     |
| A merchant-scoped storefront | [Embedded merchant storefront](#flow-4-embedded-merchant-storefront)   |
| A known product URL          | ["Buy with Henry" from any link](#flow-5-buy-with-henry-from-any-link) |

Every flow ends the same way - a Henry cart with a `checkoutUrl` you can hand to your user, or a headless `cart.checkout.purchase` call that places the order from your server.

***

## Building blocks

### The four search modes

`products.search` is one endpoint with four useful shapes, switched by `type` and `filters.type`:

| Mode            | Shape                                                                   | When to reach for it                          |
| --------------- | ----------------------------------------------------------------------- | --------------------------------------------- |
| Global text     | `type: 'global'`, `filters: { type: 'text', query }`                    | Discovery across the open web                 |
| Global image    | `type: 'global'`, `filters: { type: 'image', imageUrl }`                | Visual search from a photo / screenshot       |
| Merchant text   | `type: 'merchant'`, `filters: { merchant }`, optional top-level `query` | Search within one merchant's catalog          |
| Merchant browse | `type: 'merchant'`, `filters: { merchant }`, **no** `query`             | Paginated browse of a merchant's full catalog |

### Variant resolution on details

`products.details` returns an `options` tree under `result.options`. Each option's `values[].nextOption` is **recursive** - selecting one value reveals the next axis (e.g. size → colour → length). To walk it:

1. Call `products.details` with `link`.
2. Read `result.options` to discover the first axis (say, "fit").
3. Re-call `products.details` with `selectedOptions: ['regular']` to surface the next axis (say, "colour").
4. Re-call with `selectedOptions: ['regular', 'black']` for the next (size), etc.

The `selectedOptions` array is **positional** - values are applied in order against the option chain. The same array drops straight into `cart.item.{ ..., selectedOptions }` when you're ready to add the item.

### Async vs sync

Every async operation (`products.search`, `products.details`, `cart.checkout.details`, `cart.checkout.purchase`) accepts `mode: 'async' | 'sync'`. Sync waits up to 30 seconds; async returns immediately with a `refId` you poll via the matching `poll*` helper. See [Polling](/v1/sdk/server/guides/polling) for the reusable helper - this guide assumes it's imported as `pollJob`.

```typescript theme={null}
import HenrySDK from '@henrylabs/sdk';
import { pollJob } from './utils/henry-poll';

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

***

## Flow 1 - Shopping agent / chatbot

**Use this when** your app takes a free-text user intent ("find me a quiet hairdryer under \$200") and returns purchasable products. Typical surfaces: AI assistants, conversational commerce, MCP tools.

```mermaid theme={null}
sequenceDiagram
    participant User
    participant App
    participant Henry
    User->>App: "find me a quiet hairdryer under $200"
    App->>Henry: products.search (global text + price filter)
    Henry-->>App: products[]
    App->>Henry: products.details (top pick)
    Henry-->>App: variants + availability
    App->>Henry: cart.create
    Henry-->>App: cartId + checkoutUrl
    App-->>User: "Here's the checkout link"
```

<Steps>
  <Step title="Search globally with filters">
    Use `type: 'global'` with `filters.type: 'text'` and any of `country`, `price`, `sortBy`.

    ```typescript theme={null}
    const search = await pollJob(
      await henry.products.search({
        type: 'global',
        filters: {
          type: 'text',
          query: 'quiet hairdryer',
          country: 'US',
          price: { max: 200, currency: 'USD' },
          sortBy: 'price-low-to-high',
        },
        limit: 10,
      }),
      (args) => henry.products.pollSearch(args),
    );

    const products = search.result!.products;
    ```
  </Step>

  <Step title="Enrich the top candidate">
    Search results are catalog-grade. For prices, availability, and variants accurate enough to commit to, fetch details on the candidate you want.

    ```typescript theme={null}
    const top = products[0];
    const detail = await pollJob(
      await henry.products.details({ link: top.link, country: 'US' }),
      (args) => henry.products.pollDetails(args),
    );

    if (detail.result?.availability !== 'in_stock') {
      // skip to the next candidate
    }
    ```
  </Step>

  <Step title="Drop into a cart">
    ```typescript theme={null}
    const cart = await henry.cart.create({
      items: [{ link: top.link, quantity: 1 }],
      tags: { sessionId: 'chat_session_abc' },
    });

    return cart.data.checkoutUrl;
    ```
  </Step>
</Steps>

<Tip>Cart `tags` are perfect for tying a checkout back to a conversation or user. Filter on them later with `cart.list({tags})`.</Tip>

***

## Flow 2 - Visual / "shop the look" app

**Use this when** your users upload or screenshot something they want to buy. Typical surfaces: Lens-style apps, social shopping, photo-to-buy.

`filters.imageUrl` accepts an HTTP(S) URL, a `data:` URL, or a raw base64 payload. Results route through `shopping.google.com` regardless of which merchant ends up matching.

<Steps>
  <Step title="Run an image search">
    ```typescript theme={null}
    const search = await pollJob(
      await henry.products.search({
        type: 'global',
        filters: {
          type: 'image',
          imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUh...',
          country: 'US',
        },
        limit: 12,
      }),
      (args) => henry.products.pollSearch(args),
    );

    const matches = search.result!.products;
    ```
  </Step>

  <Step title="Let the user pick, then enrich">
    Image search produces visually-similar candidates - present them as a grid, then fetch full details once the user commits.

    ```typescript theme={null}
    const picked = matches.find((p) => p.link === userPickedLink)!;

    const detail = await pollJob(
      await henry.products.details({ link: picked.link }),
      (args) => henry.products.pollDetails(args),
    );
    ```
  </Step>

  <Step title="Cart + hosted checkout">
    ```typescript theme={null}
    const cart = await henry.cart.create({
      items: [
        {
          link: picked.link,
          quantity: 1,
          // selectedOptions only needed once you've walked the option tree
        },
      ],
    });

    window.location.href = cart.data.checkoutUrl;
    ```
  </Step>
</Steps>

<Info>Price filters (`filters.price.min/max`) and `sortBy` are text-search only. Image search returns visually-similar matches, ordered by relevance.</Info>

***

## Flow 3 - Product recommendation feed

**Use this when** you're powering a recommendation or deal feed by mirroring a merchant's catalog. A periodic crawl plus change detection lets you surface fresh picks, price drops, or restocks to your users. Typical surfaces: recommendation feeds, deal sites, comparison engines, RSS-style product feeds, price trackers.

The trick: `products.search` in merchant mode with **no** `query` returns the merchant's broad catalog. Cache the cheap fields (`link`, `price`, `availability`), then only spend a `products.details` call on items that actually changed.

<Steps>
  <Step title="Browse the catalog page by page">
    ```typescript theme={null}
    let cursor: number | undefined = undefined;

    do {
      const page = await pollJob(
        await henry.products.search({
          type: 'merchant',
          filters: { merchant: 'nike.com' },
          // omit `query` — this is a catalog browse, not a search
          limit: 100,
          cursor,
        }),
        (args) => henry.products.pollSearch(args),
      );

      const { products, pagination } = page.result!;
      await upsertCatalogRows(products);  // your DB

      cursor = pagination.nextCursor ?? undefined;
    } while (cursor !== undefined);
    ```
  </Step>

  <Step title="Spend details calls only on diffs">
    Compare each row against your cached snapshot. Re-fetch details when `price` moves or `availability` flips.

    ```typescript theme={null}
    const changed = await diffAgainstCache(catalogRows);  // your logic

    for (const row of changed) {
      const detail = await pollJob(
        await henry.products.details({ link: row.link }),
        (args) => henry.products.pollDetails(args),
      );

      if (detail.result) {
        await notifySubscribers({
          link: row.link,
          name: detail.result.name,
          oldPrice: row.previousPrice,
          newPrice: detail.result.price,
        });
      }
    }
    ```
  </Step>

  <Step title="One-click buy from a deal alert">
    When a subscriber clicks through, the link they already have on the alert is the same `link` that goes into `cart.create` - no extra search needed.

    ```typescript theme={null}
    const cart = await henry.cart.create({
      items: [{ link: alert.productLink, quantity: 1 }],
    });
    return cart.data.checkoutUrl;
    ```
  </Step>
</Steps>

<Tip>
  Use `limit: 100` (the maximum) on browse calls to minimise the number of paginated requests, and persist `pagination.nextCursor` between crawl runs to resume mid-catalog instead of starting over.
</Tip>

***

## Flow 4 - Embedded merchant storefront

**Use this when** you're building a branded buy-button or niche storefront scoped to a specific merchant - your user starts inside one brand and you want to walk them through size / colour / length variants. Typical surfaces: white-label merchant search, creator-led storefronts, brand-specific buy-buttons.

This is the flow where `selectedOptions` does the heaviest lifting.

<Steps>
  <Step title="Search within the merchant">
    ```typescript theme={null}
    const search = await pollJob(
      await henry.products.search({
        type: 'merchant',
        filters: { merchant: 'nike.com' },
        query: 'running shoes',
        limit: 20,
      }),
      (args) => henry.products.pollSearch(args),
    );

    const candidate = search.result!.products[0];
    ```
  </Step>

  <Step title="Walk the option tree">
    Each `products.details` call surfaces the next axis. Re-call as the user picks values, feeding the running array back in.

    ```typescript theme={null}
    // Initial - discover the first axis (e.g. "fit")
    let detail = await pollJob(
      await henry.products.details({ link: candidate.link }),
      (args) => henry.products.pollDetails(args),
    );
    const fitAxis = detail.result?.options;
    // fitAxis.values[].value: 'regular' | 'wide' | ...

    // User picks "regular" — surface the next axis ("colour")
    detail = await pollJob(
      await henry.products.details({
        link: candidate.link,
        selectedOptions: ['regular'],
      }),
      (args) => henry.products.pollDetails(args),
    );
    const colourAxis = detail.result?.options;

    // User picks "black" — surface the final axis ("size")
    detail = await pollJob(
      await henry.products.details({
        link: candidate.link,
        selectedOptions: ['regular', 'black'],
      }),
      (args) => henry.products.pollDetails(args),
    );
    const sizeAxis = detail.result?.options;
    ```
  </Step>

  <Step title="Add the fully-specified item to a cart">
    ```typescript theme={null}
    const cart = await henry.cart.create({
      items: [
        {
          link: candidate.link,
          quantity: 1,
          selectedOptions: ['regular', 'black', '10-w'],
        },
      ],
    });

    return cart.data.checkoutUrl;
    ```
  </Step>
</Steps>

<Info>
  When `result.options.status === 'unknown'` Henry couldn't determine variants for that product - send the user straight to cart with the `link` only and let the hosted checkout collect any
  remaining selections.
</Info>

***

## Flow 5 - "Buy with Henry" from any link

**Use this when** the user (or your agent) already has a product URL and you want to skip search entirely. Typical surfaces: browser extensions, recipe sites, social-post buy-buttons, AI agents that receive a URL directly.

```mermaid theme={null}
sequenceDiagram
    participant User
    participant App
    participant Henry
    User->>App: clicks "Buy" on a product URL
    App->>Henry: products.details (optional - preview)
    App->>Henry: cart.create
    Henry-->>App: cartId + checkoutUrl
    App-->>User: opens hosted checkout
```

<Steps>
  <Step title="(Optional) preview the product">
    Useful if you want to show price / availability / image before sending the user to checkout.

    ```typescript theme={null}
    const detail = await pollJob(
      await henry.products.details({ link: incomingUrl }),
      (args) => henry.products.pollDetails(args),
    );

    if (detail.result?.availability === 'out_of_stock') {
      return showOutOfStockUI(detail.result);
    }
    ```
  </Step>

  <Step title="Create the cart and hand off">
    ```typescript theme={null}
    const cart = await henry.cart.create({
      items: [
        {
          link: incomingUrl,
          quantity: 1,
          // selectedOptions optional — hosted checkout will collect them if needed
        },
      ],
      settings: {
        options: {
          collectBuyerEmail: 'required',
          collectBuyerAddress: 'required',
        },
      },
    });

    return cart.data.checkoutUrl;
    ```
  </Step>

  <Step title="(Optional) take payment headlessly">
    If you're collecting card details in your own UI via the [Card Element](/v1/sdk/client/elements/card-element), skip the hosted URL and call `cart.checkout.purchase` directly. See [Headless Checkout](/v1/sdk/server/guides/checkout#headless-checkout) for the full pattern.

    ```typescript theme={null}
    const order = await pollJob(
      await henry.cart.checkout.purchase(cart.data.cartId, {
        buyer: {
          name: { firstName: 'Jane', lastName: 'Doe' },
          email: 'jane@example.com',
          shippingAddress: {
            line1: '350 5th Ave',
            city: 'New York',
            province: 'NY',
            postalCode: '10118',
            countryCode: 'US',
          },
          card: {
            nameOnCard: { firstName: 'Jane', lastName: 'Doe' },
            details: { cardToken: 'card_live_...' },
          },
        },
      }),
      (args) => henry.cart.checkout.pollPurchase(args),
      { intervalMs: 2000, maxAttempts: 30 },
    );
    ```
  </Step>
</Steps>

***

## Error handling & retries

Every flow above touches the same async machinery, so the same handling applies:

| Symptom                                              | Likely cause                                          | What to do                                                                                    |
| ---------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `status: 'failed'` with `error.code`                 | Merchant-side failure (e.g. captcha, listing pulled)  | Log `error`, fall back to the next candidate or surface a "we couldn't fetch this product" UI |
| `mode: 'sync'` returns with `pending` / `processing` | Operation didn't finish in the 30s window             | Fall back to polling with `pollJob` instead of re-issuing the request                         |
| `result.options.status === 'unknown'`                | Variants couldn't be inferred                         | Drop into cart without `selectedOptions` and let hosted checkout collect them                 |
| `404 Not Found` on `cart.fetch`                      | `cartId` belongs to a different app, or doesn't exist | Verify the cart was created with the same API key                                             |
| `401 Unauthorized` everywhere                        | `HENRY_SDK_API_KEY` missing or wrong                  | Check your env                                                                                |

For long-running purchases, prefer [Webhooks](/v1/sdk/server/guides/webhooks) over polling - register a `webhookUUID` on the cart's `settings.events` and Henry will push completion events to you.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Product discovery" icon="magnifying-glass" href="/v1/sdk/server/guides/product-discovery">
    Full reference for `products.search` and `products.details` parameters
  </Card>

  <Card title="Universal cart" icon="cart-shopping" href="/v1/sdk/server/guides/universal-cart">
    Cart settings, items, tags, and lifecycle events
  </Card>

  <Card title="Checkout" icon="credit-card" href="/v1/sdk/server/guides/checkout">
    Hosted iframe / redirect or headless `cart.checkout.purchase`
  </Card>

  <Card title="Polling" icon="clockwise" href="/v1/sdk/server/guides/polling">
    The reusable `pollJob` helper used throughout this guide
  </Card>

  <Card title="Webhooks" icon="bolt" href="/v1/sdk/server/guides/webhooks">
    Skip polling entirely - have Henry push order events to you
  </Card>

  <Card title="Merchants" icon="store" href="/v1/sdk/server/guides/merchant">
    Look up which merchants are supported before scoping a flow
  </Card>
</CardGroup>
