Skip to main content

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.

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 queryShopping agent / chatbot
A photo or screenshotVisual / “shop the look” app
A merchant catalog feedProduct recommendation feed
A merchant-scoped storefrontEmbedded merchant storefront
A known product URL”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:
ModeShapeWhen to reach for it
Global texttype: 'global', filters: { type: 'text', query }Discovery across the open web
Global imagetype: 'global', filters: { type: 'image', imageUrl }Visual search from a photo / screenshot
Merchant texttype: 'merchant', filters: { merchant }, optional top-level querySearch within one merchant’s catalog
Merchant browsetype: 'merchant', filters: { merchant }, no queryPaginated 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 for the reusable helper - this guide assumes it’s imported as pollJob.
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.
1

Search globally with filters

Use type: 'global' with filters.type: 'text' and any of country, price, sortBy.
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;
2

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.
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
}
3

Drop into a cart

const cart = await henry.cart.create({
  items: [{ link: top.link, quantity: 1 }],
  tags: { sessionId: 'chat_session_abc' },
});

return cart.data.checkoutUrl;
Cart tags are perfect for tying a checkout back to a conversation or user. Filter on them later with cart.list({ tags }).

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

Run an image search

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;
2

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.
const picked = matches.find((p) => p.link === userPickedLink)!;

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

Cart + hosted checkout

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;
Price filters (filters.price.min/max) and sortBy are text-search only. Image search returns visually-similar matches, ordered by relevance.

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

Browse the catalog page by page

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);
2

Spend details calls only on diffs

Compare each row against your cached snapshot. Re-fetch details when price moves or availability flips.
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,
    });
  }
}
3

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.
const cart = await henry.cart.create({
  items: [{ link: alert.productLink, quantity: 1 }],
});
return cart.data.checkoutUrl;
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.

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

Search within the merchant

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];
2

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.
// 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;
3

Add the fully-specified item to a cart

const cart = await henry.cart.create({
  items: [
    {
      link: candidate.link,
      quantity: 1,
      selectedOptions: ['regular', 'black', '10-w'],
    },
  ],
});

return cart.data.checkoutUrl;
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.

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

(Optional) preview the product

Useful if you want to show price / availability / image before sending the user to checkout.
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);
}
2

Create the cart and hand off

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;
3

(Optional) take payment headlessly

If you’re collecting card details in your own UI via the Card Element, skip the hosted URL and call cart.checkout.purchase directly. See Headless Checkout for the full pattern.
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 },
);

Error handling & retries

Every flow above touches the same async machinery, so the same handling applies:
SymptomLikely causeWhat to do
status: 'failed' with error.codeMerchant-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 / processingOperation didn’t finish in the 30s windowFall back to polling with pollJob instead of re-issuing the request
result.options.status === 'unknown'Variants couldn’t be inferredDrop into cart without selectedOptions and let hosted checkout collect them
404 Not Found on cart.fetchcartId belongs to a different app, or doesn’t existVerify the cart was created with the same API key
401 Unauthorized everywhereHENRY_SDK_API_KEY missing or wrongCheck your env
For long-running purchases, prefer Webhooks over polling - register a webhookUUID on the cart’s settings.events and Henry will push completion events to you.

Next steps

Product discovery

Full reference for products.search and products.details parameters

Universal cart

Cart settings, items, tags, and lifecycle events

Checkout

Hosted iframe / redirect or headless cart.checkout.purchase

Polling

The reusable pollJob helper used throughout this guide

Webhooks

Skip polling entirely - have Henry push order events to you

Merchants

Look up which merchants are supported before scoping a flow