lululemon
lululemon's athletic apparel catalogue — search it, and read one product's full configurator: every colourway with its own price and images, the size…
lululemon's athletic apparel catalogue — search it, and read one product's full configurator: every colourway with its own price and images, the size options, and which exact SKUs are buyable right now.
Domain: shop.lululemon.com
Also known as: lululemon, Lululemon, lululemon athletica, lulu lemon, LULU
Prefer the capability
bowmark.yoga_outfit_shopping covers this provider and routes around it when it is having a bad day. Reach for this page when you need shop.lululemon.com specifically.
Call it directly
bowmark.providers.lululemon.search(query: { query: string; limit?: number; offset?: number }): Promise<LululemonSearch>
bowmark.providers.lululemon.getProduct(query: { productId: string }): Promise<LululemonProduct>
bowmark.providers.lululemon.getProducts(query: { productIds: string[] }): Promise<LululemonProductBatch>
bowmark.providers.lululemon.getProductAttributes(query: { productId: string }): Promise<LululemonProductAttributes>
bowmark.providers.lululemon.getSimilarProducts(query: { productId: string; limit?: number }): Promise<LululemonSimilarProducts>Functions
| Function | What it does |
|---|---|
search | Searches lululemon's catalogue by free text and returns matching product rows, closest match first — id, title, URL, price range, how many colours the style comes in, and whether it is in… |
getProduct | Reads one product's full configurator the way its product page presents it — every colourway with its own price, sale price, promo message, swatch, image set and URL; the size picker… |
getProducts | Reads the full configurator for MANY products in one call — the shape to use when ranking a candidate set, because a search row carries a price range and a colour count but not the… |
getProductAttributes | Reads what lululemon's OWN product page publishes about a garment and the third-party pricing door does not carry at all: the category the site files it under, the collection description,… |
getSimilarProducts | Returns the products lululemon's own product pages recommend alongside one product — the 'You may also like' rail — as priced rows in the store's own ranked order, de-duplicated to one row… |
Types
interface LululemonVariant {
/** The identifier lululemon's own checkout uses, e.g. "us_117376359". */
sku: string;
/** Which option each dimension is set to, e.g. { size: "4" }. */
options: Record<string, string>;
/** The store's own availability flag for this exact SKU. */
available: boolean;
price: number | null;
salePrice: number | null;
/** Why this SKU's price is, or is not, a markdown. Derived from price and
* salePrice, so it can never disagree with them. */
sale: SaleEvidence;
}
interface SaleEvidence {
/** True only with retailer evidence. NEVER from a low price or a title. */
onSale: boolean;
currentPrice: number | null;
/** The list price, when the markdown is against one. */
originalPrice: number | null;
/** ISO 4217, or null. ALWAYS null here: this feed publishes bare numbers with
* no currency code anywhere in the payload. */
currency: string | null;
promotionMessage: string | null;
evidenceType: "compare_at_price" | "sale_price" | "retailer_sale_badge" | "published_promotion" | "none";
/** The retailer text or field the classification rests on, verbatim. */
evidenceText: string | null;
}
interface ProductImage {
/** Absolute HTTPS url. */
url: string;
altText: string | null;
/** The colourway's own colour — sound because lululemon publishes its image
* list PER COLOURWAY, so every picture in it is that colour by construction. */
color: string | null;
colorId: string | null;
/** Always [] here: this feed links a picture to a COLOUR, never to a size. */
variantIds: string[];
}
/** The retailer's own labels. On this door that is audience and nothing else —
* the HPDP feed publishes no description, tags, fabric, collection or category.
* Every other field is null or [], honestly. */
interface PublishedProductAttributes {
audience: string | null;
garmentType: "sports_bra" | "tank" | "crop_top" | "leggings" | "shorts" | "other" | null;
categories: string[];
collections: string[];
fabrics: string[];
materials: string[];
color: string | null;
colorFamily: string | null;
pattern: string | null;
styleTags: string[];
neckline: string | null;
strapWidth: string | null;
backDesign: string | null;
sleeveLength: string | null;
rise: string | null;
waistband: string | null;
inseam: string | null;
legShape: string | null;
fit: string | null;
coverage: string | null;
}
/** Shared colour and family — what makes two pieces PLAUSIBLY coordinate. None
* of it establishes a set. */
interface CoordinationMetadata {
collectionNames: string[];
fabricNames: string[];
colorName: string | null;
colorId: string | null;
colorFamily: string | null;
productFamily: string | null;
}
/** An EXPLICIT retailer-published relationship. ALWAYS [] on this door: the feed's
* only product-to-product relation is the ALGORITHMIC similarity rail, which is
* getSimilarProducts. Reading that as a set would turn "the recommender put these
* near each other" into "lululemon sells these together". */
interface RetailerSetEvidence {
evidenceType: "official_set" | "shop_the_set" | "complete_the_look" | "matching_piece";
evidenceText: string | null;
sourceUrl: string | null;
setId: string | null;
relatedProducts: Array<{ productId: string | null; handle: string | null; title: string | null; url: string | null }>;
}
interface LululemonOptionGroup {
/** The machine name. "size" on every lululemon product measured. */
type: string;
/** The site's own label for the picker, e.g. "Size". */
label: string;
options: { value: string; label: string }[];
}
interface LululemonColorway {
/** lululemon's own colour code, e.g. "TRUE-NAVY". */
colorId: string;
color: string;
colorFamily: string | null;
price: number | null;
/** Set only when this colourway is marked down. */
salePrice: number | null;
promoMessage: string | null;
/** The product URL pinned to this colour, as the store publishes it. */
url: string;
swatchImage: string | null;
images: string[];
/** The SAME pictures as images, carrying this colourway's colour and colorId.
* images stays a bare string[] for existing callers; this is the additive half. */
imageAssets: ProductImage[];
sale: SaleEvidence;
coordination: CoordinationMetadata;
/** ISO 4217, or null. Always null — see SaleEvidence.currency. */
currency: string | null;
inStock: boolean;
optionGroups: LululemonOptionGroup[];
/** What can be BOUGHT in this colour right now — not the full size run. */
variants: LululemonVariant[];
}
interface LululemonSizeType {
/** A sibling product that is the same style in another length. */
productId: string;
size: string;
selected: boolean;
}
interface LululemonProduct {
id: string;
title: string;
brand: string;
url: string;
/** The store's own audience attribute, e.g. "women". */
gender: string | null;
rating: number | null;
reviewCount: number | null;
inStock: boolean;
priceLow: number | null;
priceHigh: number | null;
colorways: LululemonColorway[];
/** Other lengths of the same style. Empty on every product measured — on this
* site an inseam is its OWN product, not an option. */
sizeTypes: LululemonSizeType[];
attributes: PublishedProductAttributes;
coordination: CoordinationMetadata;
retailerSetEvidence: RetailerSetEvidence[];
}
/** One published product-detail block from the page, verbatim. */
interface LululemonFeature {
heading: string;
body: string;
}
/** What lululemon's OWN product page publishes about a garment, which the
* third-party pricing door does not carry at all. */
interface LululemonProductAttributes {
productId: string;
url: string;
title: string;
/** The site's own ProductGroup category, e.g. "Leggings". */
category: string | null;
description: string | null;
/** Trademarked fabric names off the detail accordion, e.g. ["Nulu"]. */
fabrics: string[];
fit: string | null;
/** "High-Rise" / "Mid-Rise" / "Low-Rise", as the title spells it. */
rise: string | null;
/** Every detail block, unmapped and in page order. */
features: LululemonFeature[];
/** The site's own aggregate — the honest one. The pricing door reports 0
* reviews on products whose live page shows 22,748. */
ratingValue: number | null;
reviewCount: number | null;
/** Per-FIELD origin for the eight facts above, keyed by the same names. THIS
* is how a refused read is told apart from a garment with nothing published:
* fabrics [] beside status "absent" is lululemon saying it names no fabric,
* and fabrics [] beside status "unreachable" is the page refusing us. Ranking
* that treats the two alike silently prefers whichever candidates loaded. */
provenance: Record<string, FieldProvenance>;
/** The roll-up. On a refused page ratio is 0 and unreachableFields names all
* eight — the machine-readable form of the sentence in warnings. */
completeness: Completeness;
/** What could not be reached. Non-empty means the page refused. */
warnings: string[];
}
/** Where one field's value came from, and the retailer text behind it. */
interface FieldProvenance {
/** "published" — lululemon stated it. "absent" — the page rendered and said
* nothing about this field. "unreachable" — the page refused, so UNKNOWN. */
status: "published" | "absent" | "unreachable";
/** Which door: "lululemon_pdp_ldjson", "lululemon_pdp_accordion" or
* "lululemon_pdp_title". Null when nothing filled it. */
source: string | null;
/** The retailer's own words the value rests on, for a field derived from
* prose. Null for a field the site published as a typed value. */
evidence: string | null;
/** On "unreachable" only: what was refused. */
detail?: string;
}
interface Completeness {
fields: number;
published: number;
absent: number;
unreachable: number;
/** published / fields, to 3dp. */
ratio: number;
/** The fields that are UNKNOWN rather than known-empty. */
unreachableFields: string[];
sourcesUsed: string[];
}
/** What getProducts returns. PARTIAL by construction — the pricing catalogue
* holds ~39% of the ids in lululemon's own sitemap, so ids it does not carry are
* NAMED rather than silently dropped or thrown over. */
interface LululemonProductBatch {
/** In the order the ids were passed, not the order they finished. */
products: LululemonProduct[];
/** Every id that did not read, with the catalogue's own sentence. */
missing: Array<{ productId: string; detail: string }>;
requested: number;
warnings: string[];
}
interface LululemonRow {
id: string;
title: string;
url: string;
priceLow: number | null;
priceHigh: number | null;
colorCount: number | null;
inStock: boolean | null;
/** False when the row came from the site's product index and the catalogue
* behind the prices does not carry it. id and url still work. */
priced: boolean;
}
interface LululemonSearch {
query: string;
products: LululemonRow[];
/** How many entries matched IN TOTAL, before the row cap — the size of the
* thing you are paging through, not of this page. */
matched: number;
/** Where in the ranked match list this page started. */
offset: number;
/** The offset that reads the NEXT page, or null at the end of the list. Pass
* it back verbatim — it is NOT offset + products.length, because a row that
* was reached for and lost is dropped from products and named in warnings, so
* a page of 24 can hand back 22 rows. */
nextOffset: number | null;
warnings: string[];
}
interface LululemonSimilarProducts {
seedProductId: string;
products: LululemonRow[];
totalRanked: number | null;
warnings: string[];
}Examples
// The configurator: which colours does the Align pant come in, and which of
// them can be bought in a size 6 right now?
// NOTE the shape of the answer: a colourway lists ONLY the sizes it can
// currently sell, so presence is the stock signal. v.available was true on
// every SKU ever observed here — do not filter on it and conclude anything.
const p = await bowmark.providers.lululemon.getProduct({ productId: "prod2020012" });
const inSix = p.colorways
.filter((c) => c.variants.some((v) => v.options.size === "6"))
.map((c) => ({ color: c.color, price: c.salePrice ?? c.price }));
return { product: p.title, sellableSizes: p.colorways[0]?.optionGroups[0]?.options.map((o) => o.label), inSix };// Search, then read the cheapest match in full.
const { products, matched, warnings } = await bowmark.providers.lululemon.search({ query: "align pant" });
const priced = products.filter((r) => r.priced).sort((a, b) => (a.priceLow ?? 0) - (b.priceLow ?? 0));
const best = priced[0];
if (!best) return { matched, warnings, note: "nothing matched with a price" };
return { matched, warnings, detail: await bowmark.providers.lululemon.getProduct({ productId: best.id }) };Related
yoga_outfit_shopping | Coordinated yoga outfit shopping — the capability this provider backs. |
aloyoga | Also backs the same capability. |
beyondyoga | Also backs the same capability. |