Skip to main content

Search Product Options (Market)

mdmcp-market_data_options_search

Search individual product options from the market-data service for a given date. Each option row is the weekly summary for the week that contains that date — there is no daily resolution, so two dates falling in the same week return the same snapshot. The result reports that week as evaluated_window (start:end), with the end capped at today: a date in the current week reports up to today, not the future Saturday, so an end that is not a Saturday marks a still-forming week. Returns all matching rows up to size (default 10, max 50) inline. total_row_count is the true matching population; when it exceeds the returned rows the result is truncated=true — narrow the query (tighter filters / smaller scope) or sort to surface the rows you need within the cap.

Full reference is on demand via the market_data_docs tool (topics: filter_fields, metrics, group_by_fields, dates_and_compare, text_search, composition, product_searches, response_shape, lookup_matching; pass ids=[...] for specific entries, e.g. topic="filter_fields", ids=["gender", "tier"]). This description is intentionally condensed — every filter field, metric, group-by / histogram / percentile field, date / snapping / compare rule, price-unit scale and text-search syntax is retrievable there, one section per call. Call it before building a query whenever you need a field id, metric id, valid value, operator, or price scale you do not already have. Read topic="response_shape" to interpret a result you are already holding — what truncated and total_row_count mean, and what the "others" roll-up does and does not let you say about the hidden tail. Read topic="product_searches" before summing any category breakdown: the product_searches and product_details group-bys return a TREE flattened to mixed depths, where an ancestor row already contains its descendants.

Use this tool for specific products ("show me", "list", "the cheapest"). For "bestsellers / top sellers / fastest selling", sort by sellout_percentage desc. It is also the expected follow-up when the user wants to drill down from an aggregate market_data_table result to the individual products behind it ("show me the products behind that", stat-to-evidence): reuse the same filters (plus a filter pinning the row's group value, e.g. the retailer or brand) and set date to a day inside the aggregate window — typically its end_date. One exception: a synthetic row — "Unmapped" (the include_missing bucket) or the "others" roll-up — has no filter value behind its label, so it cannot be drilled into; say so instead of issuing a filter that silently returns nothing. The underlying products are always queryable; never direct the user to check retailer websites or other manual alternatives. For aggregated analytics (snapshots, breakdowns, histograms, trends), use market_data_table instead.

There is no SKU-level search or SKU-level pricing on this server, in any vertical. The sku_* metrics (sku_count, sku_count_retailer_avg, sku_availability, sku_sellout_pct, pct_mix_sku_count) are option-level AGGREGATES — counts and rates over an option's SKUs — not a way to address one SKU. size_options DOES filter to options offered in a size, but it selects whole options: it does not restrict the row or its metrics to that size, and there is no size/variant dimension to group or sort on. So "which dresses come in a size 16" is answerable, while "what did just the queen size sell for" is not — prices, counts and rates always describe the whole option. Answer at the option level and say the per-SKU breakdown is not available.

Resolve filter values BEFORE querying: most filters take canonical EDITED ids/slugs, not free text (the brand filter is the exception — see below). Get them from the lookup tools and copy the returned values verbatim — search_retailers -> retailer (retailers[].slug), search_brands -> brand_slug (slug), search_markets -> market (string code, e.g. "UK"), search_product_searches -> product_searches (integer id), search_size_group -> size_options (as "group-\{id}" strings), search_size_options -> size_options (integer ids; keep group strings and option ids in separate filters). When a user names a company ("Nike products"), default to resolving it as a retailer (search_retailers -> retailer); use brand_slug only for a brand within a retailer ("Nike at Foot Locker"). The brand filter takes a brand NAME as text — never a slug or id, and it never requires a lookup: for a confident search_brands match use that match's name (the canonical spelling, which also catches variant listings), otherwise use the user's string verbatim. A search_brands miss is never a reason to skip the filter — it means the brand is unmapped, which is exactly what brand is for (including a hit whose slug is null: its name is the value to use). brand and a POSITIVE (eq/in) brand_slug are OR-combined when both are supplied; a negated brand_slug (neq/not_in) stays an AND exclusion that always holds. When you take a brand_slug from search_brands, first confirm the returned name really matches the user's brand (ignore case, punctuation, ®/accents, hyphens vs spaces — that leniency is for judging the NAME, never for rewriting the slug, which is case-sensitive): the tool returns a best-effort list for almost any input, so a returned slug is NOT proof of a match — if none matches, treat the brand as unmapped and use the brand filter. At least one filter is required.

Brand strategy (this tool favours RECALL): for a confident search_brands match, filter on BOTH brand_slug (that match's slug) AND brand (that match's name) so unmapped and variant listings are also returned; for no confident match, use brand alone. Tell the user you are matching on both the normalised and raw brand, so results include unmapped/variant listings.

Results assume healthy data coverage: a clean aggregate can be silently wrong if a retailer had a data gap or stale stretch in the window. Before trusting retailer-scoped figures, check get_retailer_coverage for the same retailers and date range, and caveat or narrow the analysis when it reports gap episodes overlapping the window.

vertical (apparel | beauty | homeware) selects the top-level data segment; omit to use the account's default vertical (see market_data_entitlements). This is NOT a product category — categories like dresses or mascara are a within-vertical filter found via search_product_searches. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so sku_count runs higher there — but no vertical returns per-SKU rows. Available verticals vary by account; requesting one you're not entitled to returns an error — call market_data_entitlements to see yours.

Default filters: is_second_hand=false and outlet=false are appended automatically when those fields are absent from filters. To include or target second-hand or outlet assortments, pass the field explicitly — an explicit filter always wins over the default.

Filter field table

fieldtypedefinition
retailerstringRegion-specific retailer slug (e.g. levi-uk, zara-us). Use values returned by search_retailers. This is a DIFFERENT id space from brand_slug: a retailer slug is not a brand slug. Both search_brands and search_retailers surface entries for a name like 'Levi's', but they return distinct values — retailer levi-uk vs. brand levis — that are not interchangeable between the two fields. There is no bare levis retailer slug. Default to retailer when a user names a company ('Nike products', 'the Adidas range'). CASE-SENSITIVE: pass the slug exactly as search_retailers returned it; a re-spelled value matches nothing. Display-name values (spaces or punctuation) are rejected with a pointer to search_retailers.
brand_slugstringNormalised brand slug — the label/manufacturer (e.g. nike, h-m, levis). Use values returned by search_brands (the entries that carry a slug). This is a DIFFERENT id space from retailer: a brand slug is not a retailer slug. Both search_brands and search_retailers surface entries for a name like 'Levi's', but they return distinct values — brand levis vs. retailer levi-uk — that are not interchangeable between the two fields. Use brand_slug for a brand within a retailer ('Nike at Foot Locker'); a bare company name defaults to retailer. CASE-SENSITIVE: pass the slug exactly as search_brands returned it — some carry capitals (LC-Waikiki, EA7) and a re-spelled value matches a different key, not the same brand (Dakine and dakine are separate keys). Display-name values (spaces or punctuation) are rejected with a pointer to search_brands — if the brand has no slug, use the free-text brand field instead. If you also supply a brand filter, a POSITIVE (eq/in) brand_slug is combined with it using OR (see the brand field); a negated one (neq/not_in) stays an AND exclusion that always holds.
brandtextFree-text brand match — the 'manual search' path for brands that have no normalised slug (only ~half of products carry a brand_slug). Matches the brand name as a case-insensitive phrase (e.g. bila77, Hugo Boss); a list matches ANY of the given names. Supports eq (one name) and in (a list) only, at most 50 names per request — each name is matched separately, so for a longer list resolve slugs and use brand_slug in instead. IMPORTANT: when brand and a POSITIVE (eq/in) brand_slug are both supplied they are combined with OR — a product matching EITHER field is returned (a deliberate exception to the otherwise-AND filter list). A negated brand_slug (neq/not_in) is NOT part of that OR: it stays an AND exclusion that always holds, so it still removes its brands from a brand match. A brand filter alongside a positive brand_slug CANNOT be broken down by brand_slug (rejected) — the backend would restrict the buckets to the filtered slug(s) and drop the volume brand added; group by brand (raw) for that breakdown. Tell the user you are matching either brand field rather than requiring both. The value is a brand NAME, never a slug — see the lookup workflow for which name to use. Prefer brand_slug when search_brands returns the brand with a slug; use brand for a name it has no slug for (or when the user asks for a literal brand name).
genderstringGender category. Valid values: women, men, unisex-adults, girls, boys, unisex-kids. Shorthand adult__all__ and child__all__ expand to all adult or all child genders. Case-insensitive (Unisex Kids normalises to unisex-kids); unknown values are rejected with an error.
marketstringRetailer market code — a 2-letter code like UK or US. Use the id values returned by search_markets. Case-insensitive; ISO GB is accepted for the UK. Country names (e.g. United Kingdom) are rejected with an error.
product_searchesinteger[]EDITED product-search category IDs. Use the INTEGER id returned by search_product_searches — this field takes no slug, unlike retailer and brand_slug. That row's curation_value (sub_cat_jeans_bottoms) is an internal keyword, and name / name_full are labels; none of the three is a filter value. Pass 1302, not "1302". The categories form a TREE (top-level category > subcategory > style), and an id selects that category AND everything beneath it — filtering Tops returns every T-Shirt too. So OR-ing an ancestor with its own descendant is a no-op that reads like a widening: in [Tops, T-Shirts] returns exactly the Tops count. Sibling categories do not overlap, so OR-ing across branches adds up — with ONE exception: Apparel (excl footwear & accessories) is a roll-up spanning most apparel categories despite looking like an ordinary top-level id, so OR-ing it with any other category returns its own figure unchanged. Never OR it with anything. Category names repeat across branches (three different categories are called Shorts), so match on the whole path, not the leaf name.
predominant_colourstringEDITED predominant colour classification. Valid values: black, grey, maroon, red, pink, fuchsia, purple, blue, navy, teal, aqua, green, lime, yellow, orange, copper, brown, gold, neutral, silver, white. The values unassigned and multicolour also appear in the data; when this field is used as a group_by, they merge into the others row only under a top-N roll-up; otherwise they appear as their own rows. Case-insensitive; unknown values are rejected with an error.
predominant_patternstringEDITED predominant pattern classification. Valid values: plain, abstract, animal, aztec, camouflage, checks, conversational, floral, geometric, graphics, lace, paisley, spots, stripes, tile. Shorthand pattern__all__ expands to every pattern except plain (i.e. patterned products only). Case-insensitive; unknown values are rejected with an error.
compositionstringFabric composition material — PRESENCE only. Supports eq and in only. Valid values (exact, lowercase): cotton, organic cotton, wool, silk, linen, cashmere, leather, calf leather, suede, viscose, acetate, rayon, polyester, recycled polyester, elastane, polyamide, nylon, spandex, polyurethane, acrylic, rubber. Matches any product CONTAINING the material — products usually have several, so material populations overlap. For HOW MUCH of a fibre a garment contains, use composition_percentage, not this field.
composition_percentagenumberHow much of a named fibre a garment contains, on a 0-100 scale (20 = 20%, NOT 0.2). Requires material (same fabric vocabulary as composition); value carries the percentage, and value2 the upper bound on between only — it is rejected on every other op, which ignores it. Ops: gt, gte, lt, lte, between, eq. Express a BAND as one between filter with value <= value2 — two filters on the same fibre that between them pin both ends (gte + lte, eq + lte, two betweens) are rejected, because they match on different mentions and admit garments outside the band. Distinct from composition, which is presence-only, and from a composition group-by, whose pct_mix is a fibre's share of PRESENCES, not garment weight. Percentages come from parsed care text and are per mention, not per garment component, so bands are NOT additive: one garment can match several disjoint bands, and garments whose percentage failed to parse match none. Call market_data_docs(topic="composition") before banding or comparing retailers.
in_stockbooleanProduct has at least one available SKU.
is_second_handbooleanWhether the product is second hand.
outletbooleanWhether the product comes from an outlet assortment.
advertised_discountedbooleanWhether the product is currently advertised as discounted.
pricenumberCurrent selling price in the requested currency. Filter thresholds use the value x 100 — a fixed scale the backend applies regardless of ISO 4217 exponent (GBP £49.99 = 4999; JPY ¥5,000 = 500000). Integer values only: a fractional value is rejected as un-multiplied major units. Returned prices are in major units. See market_data_docs topic="metrics" for the price-units note.
full_pricenumberHighest observed selling price in the requested currency. Filter thresholds use the value x 100 (fixed; GBP 4999 = £49.99). Integer values only: a fractional value is rejected as un-multiplied major units. Returned prices are in major units. See market_data_docs topic="metrics" for the price-units note.
advertised_discount_percentagenumberCurrent advertised discount percentage, on a 0-100 scale: 50 means 50% off — do NOT pass 0.5 for 50%.
deepest_advertised_discount_percentagenumberDeepest advertised discount percentage ever recorded on the option, on a 0-100 scale (50 = 50% off).
first_advertised_discount_percentagenumberFirst advertised discount percentage recorded on the option, on a 0-100 scale (50 = 50% off).
has_had_advertised_discountbooleanWhether the option has ever had an advertised discount.
sellout_percentagenumberPercentage of SKUs linked to the option that have sold out, on a 0-100 scale: 50 means 50% sold out — do NOT pass 0.5 for 50%.
tierstringRetailer market segment. Valid values: value, mass, premium, luxury. Case-insensitive; unknown values are rejected with an error.
sku_countintegerTotal number of SKUs linked to the product option.
option_idstringEDITED option identifier.
size_optionsinteger[] | string[]Size option IDs from search_size_options, or size group IDs as group-\{id} strings from search_size_group.
nametextFull-text search on product name. Elasticsearch query_string: default AND; UPPERCASE OR/NOT; trailing wildcards only. Wrap each multi-word term in escaped double quotes or it splits into independent AND-ed words instead of matching the phrase; inside an OR list a broad word then dominates and silently broadens the match — e.g. windbreaker OR \"shower jacket\" OR \"track jacket\", not windbreaker OR shower jacket OR track jacket (which collapses to a bare jacket match). See market_data_docs topic text_search.
descriptiontextFull-text search on product description. Elasticsearch query_string: default AND; UPPERCASE OR/NOT; trailing wildcards only. Wrap each multi-word term in escaped double quotes or it splits into independent AND-ed words instead of matching the phrase; inside an OR list a broad word then dominates and silently broadens the match — e.g. waterproof OR \"shower resistant\" OR \"wind resistant\", not waterproof OR shower resistant OR wind resistant (which collapses to a bare resistant match). See market_data_docs topic text_search.
date_founddateProduct launch date. Accepts literal dates (YYYY-MM-DD) and relative expressions — see market_data_docs topic dates_and_compare for the date-filter rules.
date_first_selloutdateDate all SKUs first went out of stock. Accepts literal and relative dates — see market_data_docs topic dates_and_compare for the date-filter rules.
date_first_majority_sku_selloutdateDate >=51% of SKUs first went out of stock (requires 2+ SKUs). Accepts literal and relative dates — see market_data_docs topic dates_and_compare for the date-filter rules.
normalised_average_ratingnumberAverage user review rating on a 5-point star scale (1.0 to 5.0). Despite the name it is NOT normalised to 0-1 — gte 4 means 4+ stars.
number_of_reviewsintegerNumber of user reviews on the product.
activewear_categorykeywordActivewear classification. Valid values: performance (sports/training activewear), athleisure (fashion-led activewear), none (not activewear). Supports eq and neq only.
is_licensed_activewearbooleanWhether the product is officially licensed activewear (e.g. NFL, NBA branded gear). Supports eq only.
sport_typekeywordSport the product is associated with. Valid values: american_football, baseball, basketball, boxing, cycling, football, golf, handball, hockey, hiking_and_outdoors, lacrosse, rugby, running, skateboarding, snowsports, softball, surfing, tennis, training, volleyball, yoga, other. Supports eq and in only.

Valid operators by field type

field typevalid operators
stringeq, neq, in, not_in
keywordeq, neq, in, not_in
text (name, description)eq only (query_string full-text)
text (brand)eq (one name), in (any of a list) — phrase match, not query_string
integer[] / integer[] | string[] (id lists)eq, neq, in, not_in
number / integereq, neq, gt, gte, lt, lte, between, in, not_in
dateeq, neq, gt, gte, lt, lte, between
booleaneq, neq

Available fields

field
brand
care
composition
cs_grp
days_in_stock
days_to_first_majority_sku_sellout
advertised_discount_percentage
advertised_discounted
deepest_advertised_discount_percentage
description
first_advertised_discount_percentage
first_price
full_price
gender
has_had_advertised_discount
image_urls
in_stock
is_second_hand
market
name
normalised_average_rating
number_of_reviews
option_id
outlet
predominant_colour
predominant_pattern
price
product_hash
product_searches
restock_count
retailer
sellout_percentage
sku_count
url
activewear_category
is_licensed_activewear
sport_type

Returned brand field: brand.name/brand.slug are the canonical EDITED brand and its brand_slug when the product is mapped; for unmapped products (~half have no brand_slug) they fall back to the retailer's raw brand name and slug. So a row's brand.slug is NOT guaranteed to be a valid brand_slug filter value, nor to match a brand_slug group-by bucket in market_data_table — do not feed it straight back into a brand_slug filter; resolve the brand via search_brands first, or filter on the raw brand name instead.

composition returns each fibre with its percentage(s), e.g. cotton 96%, elastane 4%. A fibre can carry several percentages (polyester 88%/100%) because they are parsed per mention in the care text, not per garment part — all are shown; do not read one as the garment's figure. ? means the fibre was detected but no percentage was parsed — it is NOT 0%. care is the raw source text the percentages came from, returned unjoined; request both together to check a garment against its source. See market_data_docs(topic="composition").

Sortable fields

sort field
advertised_discount_percentage
deepest_advertised_discount_percentage
first_advertised_discount_percentage
first_price
full_price
normalised_average_rating
number_of_reviews
price
sellout_percentage
sku_count

Sort order: asc (lowest first) or desc (highest first).

Parameters​

NameTypeRequiredConstraintsDescription
namestringyes—concise name describing the type of data that you are trying to request
datestringyes—Date to search, as YYYY-MM-DD. Resolves to the weekly summary covering the week that contains this date (there is no daily resolution), so two dates in the same week return the same snapshot. Supported range: the past two years (730 days), inclusive.
filtersobject[]yes—AND-combined filters. Nesting and OR groups are not supported, with one exception: brand and a POSITIVE (eq/in) brand_slug are OR-combined when both are supplied (a product matching either brand field is returned). A negated brand_slug (neq/not_in) stays an AND exclusion — it always holds, even alongside brand. At least one filter is required.
sizeintegernodefault: 10 · 1–50Number of results to return. Default 10, max 50.
fieldsstring[]no—Fields to return for each option. If omitted, a compact default set covering the most commonly useful fields is returned.
sortobjectno—Optional sort. If omitted the service returns results in its default order.
verticalstringno—Optional. The market vertical (top-level data segment) to query: apparel | beauty | homeware. This is NOT a product category — product categories (e.g. dresses, mascara) are a within-vertical filter found via search_product_searches. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so sku_count runs higher there — but no vertical returns per-SKU rows, or exposes a size/variant dimension to group or sort on. When omitted it resolves to the account's default vertical (reported as default_vertical by market_data_entitlements). Available verticals vary by account — only pass this when the user explicitly asks about a different vertical; otherwise omit it and let the account default apply. Requesting a vertical you're not entitled to returns an error; call market_data_entitlements to see yours.
currencystringno—Optional ISO 4217 currency code (e.g. USD, EUR, GBP), case-insensitive. Converts every price-denominated value in the request to this currency. Price INPUTS — any price filter (price / full_price) threshold and, in market_data_table, the histogram interval — use a FIXED 'value x 100' scale, applied regardless of the currency's ISO 4217 exponent (GBP £49.99 = 4999, £1,600 = 160000, interval 1000 = a £10 band; JPY ¥5,000 = 500000, interval 1000 = a ¥10 band). All returned prices — market_data_table price metrics / percentiles and market_data_options_search per-option prices — are in whole (major) currency units, so price inputs and outputs differ by 100x. See the price-units note — in this tool's description, or market_data_docs topic="metrics" when it is not — incl. the ISO deviation for zero-/3-decimal currencies. When omitted it is resolved from the caller's config (else USD). The backend validates the code; an unknown code is rejected upstream.

Try it​

Loading interactive widget…

Code examples​

curl -s https://mcp.edited.com/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-H "x-api-key: $MCP_API_KEY" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "mdmcp-market_data_options_search",
"arguments": {
"name": "example",
"date": "example",
"size": 10,
"vertical": "example",
"currency": "example"
}
}
}'

Input schema​

{
"type": "object",
"properties": {
"name": {
"description": "concise name describing the type of data that you are trying to request",
"examples": [
"cheapest discounted Zara dresses",
"in-stock Nike sneakers in the UK",
"new arriving luxury handbags on Farfetch"
],
"type": "string"
},
"date": {
"description": "Date to search, as YYYY-MM-DD. Resolves to the weekly summary covering the week that contains this date (there is no daily resolution), so two dates in the same week return the same snapshot. Supported range: the past two years (730 days), inclusive.",
"examples": [
"2026-03-15"
],
"format": "date",
"type": "string"
},
"filters": {
"description": "AND-combined filters. Nesting and OR groups are not supported, with one exception: `brand` and a POSITIVE (`eq`/`in`) `brand_slug` are OR-combined when both are supplied (a product matching either brand field is returned). A negated `brand_slug` (`neq`/`not_in`) stays an AND exclusion — it always holds, even alongside `brand`. At least one filter is required.",
"examples": [
[
{
"field": "retailer",
"op": "in",
"value": [
"zara"
]
}
]
],
"items": {
"properties": {
"field": {
"description": "Field to filter on. See the filter field table — in this tool's description, or `market_data_docs` `topic=\"filter_fields\"` when it is not.",
"enum": [
"retailer",
"brand_slug",
"brand",
"gender",
"market",
"product_searches",
"predominant_colour",
"predominant_pattern",
"composition",
"composition_percentage",
"in_stock",
"is_second_hand",
"outlet",
"advertised_discounted",
"price",
"full_price",
"advertised_discount_percentage",
"deepest_advertised_discount_percentage",
"first_advertised_discount_percentage",
"has_had_advertised_discount",
"sellout_percentage",
"tier",
"sku_count",
"option_id",
"size_options",
"name",
"description",
"date_found",
"date_first_sellout",
"date_first_majority_sku_sellout",
"normalised_average_rating",
"number_of_reviews",
"activewear_category",
"is_licensed_activewear",
"sport_type"
],
"examples": [
"retailer"
],
"type": "string"
},
"op": {
"description": "Comparison operator. Use 'between' for numeric or date range bounds. Use 'eq' for text-search fields (name, description). 'brand' supports 'eq' (one name) and 'in' (a list) only.",
"enum": [
"eq",
"neq",
"in",
"not_in",
"gt",
"gte",
"lt",
"lte",
"between"
],
"examples": [
"eq",
"in"
],
"type": "string"
},
"value": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"items": {
"type": "string"
},
"type": "array"
},
{
"items": {
"type": "integer"
},
"type": "array"
},
{
"items": {
"type": "number"
},
"type": "array"
}
],
"description": "Primary filter value. Use a list for 'in' and 'not_in'.",
"examples": [
"zara",
[
"zara",
"hm"
],
true
]
},
"value2": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Upper bound used only with 'between'.",
"examples": [
100
]
},
"material": {
"description": "Fibre name, REQUIRED for `composition_percentage` and rejected on every other field. Names which fibre the percentage in `value` applies to — `material: \"spandex\", op: \"gte\", value: 20` is 'at least 20% spandex'. Takes the same closed fabric vocabulary as the `composition` filter.",
"examples": [
"spandex",
"recycled polyester"
],
"type": "string"
}
},
"required": [
"field",
"op",
"value"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"size": {
"default": 10,
"description": "Number of results to return. Default 10, max 50.",
"examples": [
10
],
"maximum": 50,
"minimum": 1,
"type": "integer"
},
"fields": {
"description": "Fields to return for each option. If omitted, a compact default set covering the most commonly useful fields is returned.",
"examples": [
[
"option_id",
"name",
"retailer",
"price"
]
],
"items": {
"enum": [
"brand",
"care",
"composition",
"cs_grp",
"days_in_stock",
"days_to_first_majority_sku_sellout",
"advertised_discount_percentage",
"advertised_discounted",
"deepest_advertised_discount_percentage",
"description",
"first_advertised_discount_percentage",
"first_price",
"full_price",
"gender",
"has_had_advertised_discount",
"image_urls",
"in_stock",
"is_second_hand",
"market",
"name",
"normalised_average_rating",
"number_of_reviews",
"option_id",
"outlet",
"predominant_colour",
"predominant_pattern",
"price",
"product_hash",
"product_searches",
"restock_count",
"retailer",
"sellout_percentage",
"sku_count",
"url",
"activewear_category",
"is_licensed_activewear",
"sport_type"
],
"type": "string"
},
"type": "array"
},
"sort": {
"description": "Optional sort. If omitted the service returns results in its default order.",
"examples": [
{
"field": "price",
"order": "asc"
}
],
"properties": {
"field": {
"description": "Field to sort on. Must be one of the documented sort fields.",
"enum": [
"advertised_discount_percentage",
"deepest_advertised_discount_percentage",
"first_advertised_discount_percentage",
"first_price",
"full_price",
"normalised_average_rating",
"number_of_reviews",
"price",
"sellout_percentage",
"sku_count"
],
"examples": [
"price"
],
"type": "string"
},
"order": {
"description": "Sort order. 'asc' for lowest first, 'desc' for highest first.",
"enum": [
"asc",
"desc"
],
"examples": [
"asc"
],
"type": "string"
}
},
"required": [
"field",
"order"
],
"type": "object"
},
"vertical": {
"description": "Optional. The market vertical (top-level data segment) to query: `apparel` | `beauty` | `homeware`. This is NOT a product category — product categories (e.g. dresses, mascara) are a within-vertical filter found via `search_product_searches`. All three verticals are supported and return the SAME row grain: one row per product option. Homeware options bundle more SKUs per option (e.g. bed sizes) than apparel or beauty, so `sku_count` runs higher there — but no vertical returns per-SKU rows, or exposes a size/variant dimension to group or sort on. When omitted it resolves to the account's default vertical (reported as `default_vertical` by `market_data_entitlements`). Available verticals vary by account — only pass this when the user explicitly asks about a different vertical; otherwise omit it and let the account default apply. Requesting a vertical you're not entitled to returns an error; call `market_data_entitlements` to see yours.",
"enum": [
"apparel",
"beauty",
"homeware"
],
"type": "string"
},
"currency": {
"description": "Optional ISO 4217 currency code (e.g. `USD`, `EUR`, `GBP`), case-insensitive. Converts every price-denominated value in the request to this currency. Price INPUTS — any price filter (`price` / `full_price`) threshold and, in `market_data_table`, the histogram `interval` — use a FIXED 'value x 100' scale, applied regardless of the currency's ISO 4217 exponent (GBP £49.99 = `4999`, £1,600 = `160000`, interval `1000` = a £10 band; JPY ¥5,000 = `500000`, interval `1000` = a ¥10 band). All returned prices — `market_data_table` price metrics / percentiles and `market_data_options_search` per-option prices — are in whole (major) currency units, so price inputs and outputs differ by 100x. See the price-units note — in this tool's description, or `market_data_docs` `topic=\"metrics\"` when it is not — incl. the ISO deviation for zero-/3-decimal currencies. When omitted it is resolved from the caller's config (else `USD`). The backend validates the code; an unknown code is rejected upstream.",
"type": "string"
}
},
"required": [
"name",
"date",
"filters"
]
}

Output schema​

Describes both branches of the envelope — result on success, error on a refusal. Validate against this rather than pattern-matching the prose above; see Concepts → Response shape.

{
"type": "object",
"properties": {
"result": {
"description": "Envelope returned by the market-data query tools.\n\n``rows`` are the inline rows, hard-capped (``size`` for options, ``limit``\nfor table). ``truncated`` says, exactly, whether the cap hid matching rows.\n``total_row_count`` is EXACT-OR-NONE — the exact matching count when the tool\ncan know it, ``None`` when it cannot; it is never a lower bound or estimate:\n\n- options: the true matching population (backend ``meta.result_count``);\n- table, not truncated: the exact returned row count;\n- table, truncated (either ``include_others`` setting): ``None`` — the\n ``/table`` backend returns no population metadata, so once the cap hides\n rows the true count is genuinely unknown, and a fabricated integer (a former\n ``limit + 1`` \"floor\") reads like an exact small count. Read ``truncated``\n for the \"more rows exist\" signal; raise ``limit`` or use\n ``include_others=true`` for coverage. The trailing ``\"others\"`` row carries\n the rolled-up tail on the ``include_others=true`` path.\n- trend: ALWAYS ``None``, truncated or not — a trend is a per-period fan-out\n (one backend call per bucket), so there is no single population to count.\n ``truncated`` is still exact, and the roll-up is per period.\n\nUnder ``include_others=true`` (table path only) the rows past ``limit`` are\nrolled into ONE trailing ``\"others\"`` row standing for every group not listed\nindividually. That row is not a count of them and carries no information\nabout how many there are, so a group count, a \"showing X of Y rows\", or a\nper-group average cannot be derived from a truncated result. Whether its\nvalue combines with the visible rows is a property of the METRIC and the\ngroup-by, not of this row.\n\nPeriod provenance (table path) makes the multi-period aggregation legible:\n\n- ``granularity`` — the auto-selected bucket size (``W``/``M``); ``W`` for the\n single-week options path.\n- ``period_count`` — how many W/M buckets the range spans.\n- ``evaluated_window`` — ``\"start:end\"`` (the same shape as the trend ``dates``\n column) for the span of dates the values ACTUALLY describe. Requested dates\n are snapped OUTWARD to bucket bounds, so this is usually wider than what was\n asked for: request June (``2026-06-01`` / ``2026-06-30``) and the values\n describe ``2026-05-31:2026-07-04``. The ``start`` is the covering bucket's\n start — the backward snap genuinely sweeps that pre-request data into the\n aggregate. The ``end`` is capped at today: an in-progress trailing bucket\n rounds outward to a nominal bucket-end that holds no data past today, so the\n end reports the latest date that can contribute rather than the boundary.\n An ``end`` that is NOT on the granularity's natural boundary (a non\n month-end for ``M``, a non-Saturday for ``W``) therefore signals an\n incomplete final period. Report figures against this window, not against the\n requested dates. On the options path it is the covering weekly bucket for\n the requested ``date``.\n- ``compare_evaluated_window`` — the same, for a non-trend ``compare``: the\n comparison range snaps independently to whole buckets. The\n ``<metric>_compare`` column headers name this same evaluated window, so\n header and field agree on the window the figures describe. ``None`` without\n ``compare`` and under ``trend`` (each interval resolves its own\n comparison window there).\n- ``compare_period_count`` / ``compare_combined_over_periods`` — the same\n period provenance for that compare window, which spans and combines\n independently of the primary range: the ``<metric>_compare`` change\n figures are computed against a baseline first combined over this many\n periods (``combined_over_periods`` / ``period_count`` describe only the\n primary window). ``None`` without a non-trend ``compare``.\n- ``compare_windows_overlap`` — ``True`` when the primary and comparison\n evaluated windows share at least one bucket. A relative ``previous_period``\n compare is derived to abut without overlap, so this fires only on a\n hand-supplied absolute ``compare`` whose snapped window collides with the\n primary's — in which case a near-zero ``<metric>_compare`` delta is a\n self-contamination artefact, not a real change. ``None`` without a\n non-trend ``compare``.\n- ``combined_over_periods`` — ``True`` when the range spans >1 period on the\n non-trend path, i.e. the value went through the cross-period combine.\n ``False`` for a single period, for ``trend`` (each row is already one\n period), and for options. Date filters do not suppress it: they are\n evaluated per period, so they can empty periods out of an averaged\n metric's divisor — the combined value may describe fewer populated\n periods than ``period_count``, down to a single period's raw value —\n but they never switch the path away from per-period combining.\n- ``metric_combine`` — ``metric_id -> combine method`` (see\n ``METRIC_COMBINE_BY_ID``) so the single value is machine-self-describing;\n ``None`` on the options path (no aggregate metrics). When\n ``combined_over_periods`` is ``True`` the matching metric columns are also\n tagged in ``columns`` (e.g. ``Product Count [avg/period]``).",
"properties": {
"name": {
"type": "string"
},
"columns": {
"items": {
"type": "string"
},
"type": "array"
},
"rows": {
"items": {
"items": {},
"type": "array"
},
"type": "array"
},
"total_row_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
},
"truncated": {
"type": "boolean"
},
"currency": {
"type": "string"
},
"vertical": {
"type": "string"
},
"granularity": {
"anyOf": [
{
"enum": [
"W",
"M"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"period_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null
},
"evaluated_window": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"compare_evaluated_window": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"compare_period_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null
},
"compare_combined_over_periods": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null
},
"compare_windows_overlap": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null
},
"combined_over_periods": {
"default": false,
"type": "boolean"
},
"metric_combine": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"name",
"columns",
"rows",
"total_row_count",
"truncated",
"currency",
"vertical"
],
"type": "object"
},
"error": {
"type": "object",
"description": "Present instead of `result` when the call was rejected. Branch on `code`; never string-match `message`.",
"properties": {
"code": {
"type": "string",
"enum": [
"invalid_input",
"not_entitled",
"upstream_unavailable",
"internal"
]
},
"message": {
"type": "string"
},
"field": {
"type": "string",
"description": "Offending parameter, when the failure is attributable to one."
},
"details": {
"type": "object",
"additionalProperties": true
}
},
"required": [
"code",
"message"
]
}
},
"oneOf": [
{
"required": [
"result"
]
},
{
"required": [
"error"
]
}
],
"x-fastmcp-wrap-result": true
}