This page documents the response shape of every Fanatical MCP tool. Read it alongside /for-ai. The tool outputSchema already carries this information in machine-readable form; this page adds context and gotchas that a schema alone does not carry.
Every tool response is an object with region and currency at the top level:
{
"region": "GB",
"currency": "GBP",
"results": [ ... ] // search_products
"product": { ... } // get_product (or null)
"products": [ ... ], // get_products
"not_found": [ ... ], // get_products — the identifiers that matched no product
"lookup_by": "slug" // get_products — "slug" or "steam_id"
"sales": [ ... ] // list_sales
"values": [ ... ], // list_facets
"facet": "tags" // list_facets — the facet field these values belong to
}
Every monetary field inside the response uses the top-level currency. Agents do not need to check currency on individual records.
get_product returns { region, currency, product: null } when the product is not found or not available in the region. The null result is not an error; it is a valid answer.
get_products reports a miss differently. The products array holds only the products that were found. It never holds null entries. Each identifier that matched no product goes into not_found. The lookup_by field tells you what not_found holds: slug strings, or Steam application IDs.
Products come back in the order you asked for them, and each record carries the identifier it answers. Match a record to your request by its slug or steam_id field. Do not match by array position, because the misses are absent from the array.
get_product and get_products are direct lookups: they return preorder and out-of-stock products with no filtering. search_products excludes both by default. This asymmetry is intentional on both sides — a point lookup answers "what is this product", a search answers "what can a customer buy right now".
Returned by search_products (as items of results), by get_product (as product), and by get_products (as items of products). get_product and get_products return the same full record. search_products returns a trimmed subset.
product_id values. Stable while a listing is live. A re-listed product may get a new product_id.get_product.steam_id returns the base edition, which is the record with a null parent. To get a specific edition, look it up by slug.game, dlc, bundle, game-bundle, mystery-bundle, book, software, gift-card. Note: search_products.type accepts only three input values: game, dlc, or bundle. The bundle input matches the output values bundle, game-bundle and mystery-bundle. A non-null bundle_price_messages marks a multi-item pack regardless of type — check that field, not type, to detect packs.image and vertical_image URLs are the canonical Fanatical cover art for the product, served from cdn-ext.fanatical.com, a CDN configured for external hotlinking. Prefer these URLs over running a separate image search on the product name — an image search often picks up store-listing screenshots from competitor retailers, and Fanatical's own art has correct licensing and current promotional overlays.cdn-ext.fanatical.com.The record holds two URLs that make the site do something when the page opens. Use them to give the user a direct action in your answer, instead of only a link to a page.
url instead.coupon is not null.cart_url the primary action — “Add to cart” — and coupon.coupon_url the second action — “Apply coupon FANATICAL15”. Use cart_url on its own when the product has a coupon, because that URL does both steps. Both URLs carry the same tracking parameters as url. Use them exactly as returned.price when not on sale.sort: "discount". For pick-and-mix bundles, this represents the saving versus RRP of the constituent items.sale_slug: null can still be discounted. sale_slug indicates curated sale membership, not the presence of a discount. Use discount_percent > 0 to test for a discount.sale_slug is null.star-deal > best-ever > highlighted-deal > flash-sale. Only the highest-priority applicable label is shown. This is not a discount ranking — a star-deal is not necessarily a bigger discount than a flash-sale. It reflects promotional weight, not price.verified, playable, or unsupported. Null when Valve has not rated the product.Every taxonomy field comes as two arrays: display names and slugs. Display names go to users; slugs go back to search_products.
search_products.tag.search_products.developer.search_products.publisher.search_products.franchise.list_facets or from a prior product record. A guessed slug that does not exist returns zero results with no error. Always look up the slug first.steam, drm free, gog, epicgames). Null when no DRM applies.windows, mac, linux, xbox series, xbox one.Overwhelmingly Negative, 5 is Mixed, 8 is Very Positive, and 9 is Overwhelmingly Positive. A score of 0 means either that Steam has too few reviews to rate the product, or that this listing has no review data of its own. Usually a string; some source records give a number. Null when the product has no Steam review data. To filter on this field, use search_products.min_steam_review_score.steam_review_score, for example Very Positive. If the score is 0, the label gives the number of reviews, for example No user reviews or 3 user reviews. Null when the product has no Steam review data.valid_until covers any reason the current state may expire: sale end, coupon end, key stock expiry, or a scheduled price change. Compare to the current time to gauge urgency. Use sort: "valid_until_asc" to surface anything ending soonest.The coupon field is null unless an active voucher applies to this product.
search_products returns a smaller coupon than get_product. A search result gives code, percent, coupon_price and coupon_url. Use get_product when you need name, description, coupon_saving or valid_until.
price and discount_percent show the product before the coupon. Read coupon_price for the price the user pays with the code. If you quote price for a product that has a coupon, you quote too much.coupon.coupon_url applies the code. cart_url adds the product and applies the code in one step. Do not build these URLs yourself — the server returns them with the tracking parameters already in place.Products in the catalogue are related in one direction: a variant, an edition, or a DLC has a parent. The record shows this relationship from both ends.
name, type, slug, url, and image.slug, name, type, image, url, price and discount_percent. The url already carries tracking parameters, so use it as it is. Entries carry no drm: assume the same DRM as the product you asked for.editions.The editions and dlc arrays hold a maximum of 10 items each. The two count fields give the true totals. When a count is more than 10, use search_products with the franchise or with a text query to get the remainder.
A product does not appear in its own arrays. The arrays hold the rest of the group:
editions gives its editions.editions gives the base game and the other editions.dlc gives the other DLC, and editions gives the base game and its editions.The dlc array is the DLC for the game group, not for the specific edition you asked for. DLC attaches to the base game. A user who has a deluxe edition can buy these DLC.
get_product returns these four fields by default. Set include_related to false to omit them and make the response smaller.
get_products omits these four fields by default. Set include_related to true to get them. The default is different because the request can hold 20 products, and each product can have many editions and DLC. When you need the DLC for one game, use get_product.
include_related is false, the four fields are absent from the record. An empty dlc array with a dlc_count of 0 means the game has no DLC in the region.Two bundle-related shapes appear on the product record. They serve different scenarios.
Present on individual products that are currently in a live bundle. The field carries the bundle's identity, price and pre-rendered pitch copy, so an agent recommending a game can name-drop the bundle without a second tool call.
Fields on in_bundle:
slug to fetch the full bundle detail with get_product.price is the entry-tier price.Non-null on records whose type is a bundle type. Each tier lists the products unlocked at that tier's price. Higher tiers include everything in lower tiers.
slug, name, type, image, drm and full_price. drm is given per item because one bundle can mix DRM platforms. full_price is the item's list price on its own, to compare picks by value; it is null when the item is not sold separately, which is true of about half of all bundle items. There is no current price or discount, because an item you cannot buy alone has neither. Tier items carry no url — look them up by slug.
build-your-own), products[] is the pool the user picks from at this tier, not what the user receives. The user picks tier_product_count items from this pool.build-your-own), the item pool lives on tier 0 only. Higher tiers return an empty products array — this is not a data bug. Every tier shares the same pool; higher tiers unlock more picks from it. To describe what's in the bundle at any tier, read bundle_tiers[0].products. To describe the tier prices, read every tier's price and tier_product_count.Every bundle carries five pre-rendered pitch strings, tuned per bundle_type, folded to the region's currency. Use these verbatim instead of synthesising a pitch.
Returned as items of sales[] from list_sales.
search_products.sale_slug to filter to products in this sale.Sales are sorted by product count descending. An empty array is a valid response — it means no sale is running.
Returned as items of values[] from list_facets. The facet field at the top level of the response says which facet these values belong to.
search_products filter (tag, publisher, franchise, or developer).search_products.count is at least 5 and the facet has a landing page. tags maps to /en/categories/<slug>, publishers to /en/publishers/<slug>, franchises to /en/franchises/<slug>. Developers have no landing page and never carry a url.The catalogue stores prices as maps keyed by currency. Before a response leaves the server, every monetary field is folded to a plain value in the region's currency.
Folded paths on the product record:
price, full_pricecoupon.coupon_price, coupon.coupon_savingin_bundle.price, in_bundle.full_pricein_bundle.bundle_price_messages.{tiny, short, quantity, value, long}bundle_tiers[].pricebundle_price_messages.{tiny, short, quantity, value, long}A folded field is null when the underlying map has no value for the region's currency. A folded pitch string is null when no copy exists for that currency.
Availability is per-country. Currency is derived from country. Two countries in the same currency zone (for example DE and FR) get the same prices but may have different availability.
Every timestamp is Unix seconds. Not milliseconds. Not an ISO string. Convert with new Date(value * 1000).
Every price is a plain number in the response wrapper's currency. No object, no key. Null when the price is not set for that currency.
Every slug is lowercase, hyphen-separated, ASCII. Slugs come from list_facets, list_sales, or from a prior product record. Never synthesise a slug — a guessed slug that does not exist returns zero results with no error.
Not-found is a valid response, not an error. get_product returns { region, currency, product: null }. get_products lists the identifiers it could not match in not_found. search_products, list_sales and list_facets return empty arrays in the relevant field.
Real errors return an MCP error response. Two error classes:
Back to /for-ai · Questions to dev@fanatical.com