npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

iherb-api

v0.1.0

Published

Typed iHerb product search and supplement label extraction for Bun and Node.js

Readme

iherb-api

Typed, unofficial iHerb product search and supplement-label extraction for Bun and Node.js 20+.

The package accepts text produced by your OCR pipeline, finds likely iHerb products, groups package variants, and extracts the complete server-rendered product label. OCR itself is intentionally out of scope.

Install

bun add iherb-api

Export cookies from an existing browser

Run the package CLI:

# After the package is published to npm:
bunx iherb-api
# equivalent:
bun x iherb-api

From a cloned repository:

bun install
bun run cookies

The command automatically checks your existing Google Chrome and Brave profiles, selects the profile with a usable iHerb session, decrypts only cookies for *.iherb.com, and prints:

IHERB_COOKIE="..."
IHERB_USER_AGENT="..."

Copy both lines into .env. The matching User-Agent is exported because some browser-verification cookies are bound to the browser identity.

No browser window is opened. The command does not stop or modify a running browser and never queries cookies for other domains. The browser must already have an iHerb session; if it does not, log in normally and rerun the command.

The CLI also performs a server-side validation request. Modern Cloudflare checks can bind access to TLS fingerprint and IP in addition to cookies and User-Agent. If validation prints a warning, the exported values are still shown, but they may not make Bun fetch pass the challenge. In that case, searchProducts() can use its sitemap fallback, while getProduct() reports IHerbBlockedError instead of claiming the protection was bypassed.

Options:

bunx iherb-api --list-profiles
bunx iherb-api --browser brave
bunx iherb-api --browser chrome --profile "Profile 2"
bunx iherb-api --no-validate

Profile names and directory names are both accepted. Profile listing counts iHerb rows without decrypting cookie values.

Cookie extraction currently requires Bun. On Linux it uses the logged-in desktop keyring through secret-tool; on macOS, Keychain may ask you to allow access to Chrome or Brave Safe Storage. Legacy Windows v10 cookies use the optional @primno/dpapi dependency. Chrome/Brave v20 App-Bound Encryption cannot be decrypted by a standalone CLI; the command reports that limitation instead of attempting to bypass the operating-system protection.

If Bun blocks the optional native DPAPI installer on Windows, review the package and enable it explicitly with bun pm trust @primno/dpapi.

The printed values are secrets: do not commit them or paste them into logs.

The same behavior is available programmatically:

import {
  discoverBrowserProfiles,
  extractIHerbBrowserSession,
} from "iherb-api";

const profiles = await discoverBrowserProfiles({ browser: "auto" });
const session = await extractIHerbBrowserSession({
  browser: "chrome",
  profile: "Default",
});

console.log(session.profile.name);
// Keep session.cookieHeader secret.

Usage

import { createIHerbClient } from "iherb-api";

const client = createIHerbClient({
  // Supply a session explicitly when iHerb requires browser verification.
  // Never commit this value or print it in logs.
  cookieHeader: process.env.IHERB_COOKIE,
  userAgent: process.env.IHERB_USER_AGENT,
  locale: {
    country: "US",
    language: "en-US",
    currency: "USD",
  },
});

const search = await client.searchProducts(`
  California Gold Nutrition
  Magnesium Bisglycinate Chelate
  100 mg per capsule
  240 veggie capsules
`);

const selected =
  search.families[0]?.selectedVariantId ??
  search.candidates[0]?.productId;

if (selected) {
  // Passing the candidate URL avoids resolving the numeric ID through sitemaps.
  const candidate = search.candidates.find(
    (item) => item.productId === selected,
  );
  const product = await client.getProduct(candidate?.url ?? selected);

  console.log(product.name);
  console.log(product.suggestedUse);
  console.log(product.facts);
  console.log(product.derived.perUnitFacts);
}

Direct internal catalog JSON

When you already know an iHerb product ID, use getCatalogProduct() to avoid fetching or parsing a product web page:

const product = await client.getCatalogProduct(103274);

console.log(product.name);
console.log(product.brand.name);
console.log(product.servingSize);
console.log(product.servingsPerContainer);
console.log(product.potency);
console.log(product.keyIngredients);
console.log(product.certifications);

This method calls iHerb's undocumented internal catalog.app.iherb.com/recommendations/comparison/{productId} and recommendations/aicomparison/{productId} JSON endpoints. In current testing, they require only the iher-pref1 locale cookie, which the client creates automatically. An iHerb account and copied browser cookies are not required for this method.

The direct response includes identity, brand, current price and availability, image metadata, package quantity, serving size, servings per container, potency, certifications and—when the AI comparison response is available—key ingredients. It does not expose the complete Supplement Facts table, suggested use, other ingredients, warnings or UPC. Use getProduct() when those page-only fields are required. Every keyIngredients entry is marked with source: "ai_comparison" and must not be treated as authoritative label data.

These endpoints are not public API contracts and can change without notice. diagnostics explicitly lists missing fields instead of presenting the partial response as a complete supplement label.

Search behavior

searchProducts() returns ranked candidates and product families:

const result = await client.searchProducts(ocrText, {
  limit: 10,
  sitemapFallback: true,
});

result.families[0]?.variants;          // e.g. 60 and 240 capsule SKUs
result.families[0]?.selectedVariantId; // null when OCR is ambiguous
result.candidates[0]?.confidence;
result.candidates[0]?.scoreReasons;

Live search reads only the first iHerb results page. Cards with the same groupId are returned as variants of one family. variantsComplete remains false because the site does not provide a documented completeness guarantee.

When live search is blocked or yields no candidates, the client can create an in-memory fuzzy index from iHerb's product sitemaps:

const snapshot = await client.refreshProductIndex();
await Bun.write("iherb-index.json", JSON.stringify(snapshot));

// On a later process start:
client.importProductIndex(
  JSON.parse(await Bun.file("iherb-index.json").text()),
);

The library itself stays portable and does not write index files.

Extracted product data

getProduct() returns:

  • product ID, group ID, part number, canonical URL, brand and breadcrumbs;
  • package quantity, form factor, price, availability, images and rating;
  • serving size and servings per container;
  • every row of Supplement Facts, including raw cells and source forms;
  • per-unit amounts when the serving size is unambiguous;
  • suggested use, other ingredients, warnings and description;
  • dimensions, shipping weight, expiration date and country of origin when present.

The parser uses iHerb's server-rendered HTML. It prioritizes #modelProperties, data-cart-info, the Supplement Facts table and semantic product-overview sections, with JSON-LD and metadata as fallbacks.

Cookies and blocking

Pass cookies through cookieHeader or a tough-cookie CookieJar:

import { CookieJar } from "tough-cookie";
import { createIHerbClient, IHerbBlockedError } from "iherb-api";

const cookieJar = new CookieJar();
const client = createIHerbClient({ cookieJar });

try {
  await client.searchProducts("NOW Foods magnesium glycinate 180 tablets");
} catch (error) {
  if (error instanceof IHerbBlockedError) {
    // Refresh the caller-managed session manually.
  }
}

The client persists Set-Cookie, but it does not automatically call the browser-profile extractor, solve CAPTCHAs, or bypass a browser-verification challenge. Cookie values are never included in library error messages.

Development

bun install
bun run typecheck
bun test
bun run build

Usage notice

This project is not affiliated with or endorsed by iHerb. iHerb's interfaces are undocumented and can change without notice. Before using the package, review the current Terms of Use, robots.txt, and sitemap index. The package does not crawl paginated search or category pages.