---
title: "ListProducts"
method: GET
path: "/api/products"
---

# ListProducts

`GET /api/products`

## Request

- Base URL: `https://api.caddence.golf`
- URL: `https://api.caddence.golf/api/products`
- Auth: none declared

## Query parameters

- `search` string, nullable: Search query
- `retailer` string, nullable: Filter by retailer name (case-insensitive) or slug; comma-separated for multiple. Matches only products with an in-stock offer at the retailer; unknown retailers return empty results.
- `brand` string, nullable: Filter by brand
- `product_type` string, nullable: Filter by product type
- `category` string, nullable: Filter by category
- `hand` string, nullable: Filter by hand: LH or RH
- `loft` string, nullable: Filter by loft angle (e.g., 10.5, 18.0)
- `bounce` string, nullable: Filter by bounce angle
- `grind` string, nullable: Filter by grind
- `flex` string, nullable: Filter by flex
- `shaft` string, nullable: Filter by shaft
- `shaft_material` string, nullable: Filter by shaft material: steel, graphite (comma-separated)
- `condition` string, nullable: Filter by condition
- `color` string, nullable: Filter by color
- `gender` string, nullable: Filter by gender: ladies, junior, mens
- `min_price` number, nullable: Minimum price
- `max_price` number, nullable: Maximum price
- `on_sale` boolean, nullable: Only show products on sale
- `min_retailers` integer, nullable: Minimum number of retailers
- `sort_by` string, nullable: Sort by: retailer_count (default), price_low, price_high, savings, rrp_discount, name, brand
- `include_out_of_stock` boolean: Decision D-001: include out-of-stock products in the catalog. Defaults to true so OOS products remain visible/indexable.
- `limit` integer: Results limit
- `offset` integer, nullable: Results offset. Mutually exclusive with 'page' (400 if both).
- `page` integer, nullable: 1-based page number; translated to offset=(page-1)*limit. Mutually exclusive with 'offset' (400 if both).

## Response `200`

Request fulfilled, document follows

- BaseProductListResponse
  - `products` BaseProduct[], required
    - `id` union, required: Product identifier
      - string
      - integer
    - `slug` string: URL-friendly slug
    - `name` string, required: Product name/title
    - `title` string, required: Product title
    - `brand` string, required: Brand name
    - `min_price` number, nullable: Minimum in-stock price; last-seen price when OOS
    - `max_price` number, nullable: Maximum in-stock price; last-seen price when OOS
    - `new_min_price` number, nullable: Minimum in-stock price across NewCondition offers
    - `used_min_price` number, nullable: Minimum in-stock price across UsedCondition offers
    - `rrp` number, nullable: Recommended retail price (from offers)
    - `rrp_discount_pct` integer, nullable: Discount vs RRP as integer percentage
    - `image_url` string, nullable: Product image URL
    - `url` string, nullable: Product URL (first retailer)
    - `category` string, nullable: Product category
    - `product_type` string, nullable: Product type
    - `variant_count` integer, required: In-stock variants (0 when OOS)
    - `retailer_count` integer, required: In-stock retailers (0 when OOS)
    - `retailers` string[], required: List of in-stock retailers (empty when OOS)
    - `hands` string[]: Distinct hands (LH/RH) across in-stock variants; empty when hand does not apply or product is out of stock
    - `flexes` string[]: Distinct raw variant flex values (canonicalised client-side); empty when flex does not apply. Powers the PLP Flex facet.
    - `shafts` string[]: Distinct variant shaft materials (steel/graphite, pipeline-inferred); empty when unknown. Powers the PLP Shaft facet.
    - `lofts` string[]: Distinct variant lofts in degrees (drivers/wedges); empty when N/A. Combined with club_numbers into the PLP Loft/Club facet.
    - `club_numbers` string[]: Distinct variant club numbers (woods/hybrids, e.g. '3','5'); empty when N/A. Combined with lofts into the PLP Loft/Club facet.
    - `release_year` integer, nullable: Product release year
    - `price_trend` number[]: Last 10 min-price points for sparkline
    - `price_change_7d_pct` number, nullable: 7-day price change percentage; null when a compared day is missing or the day-pair's retailer composition differs (mix-artifact guard)
    - `badge_type` string, nullable: Badge type: drop, lowest, new, retailers
    - `badge_value` string, nullable: Badge display text
    - `is_in_stock` boolean, nullable: True when at least one retailer has the product in stock
    - `last_available_price` number, nullable: Lowest recorded price across any offer (in or out of stock)
    - `last_available_at` string, nullable: ISO timestamp a scraper last actually observed any offer for this product (offers.last_seen_at, migration 036); null = freshness unknown
    - `content_changed_on` string, nullable: ISO date the product's served facts last changed: day-min or day-max price or the set of retailers differed from the day before (migration 062); the sitemap lastmod. null before the first refresh
    - `catalogue_kind` string, nullable: The catalogue entry kind of the page, 'family' or 'product' (migration 082); null for a page no installed catalogue binds
    - `index_eligible` boolean, nullable: False for a family page below the indexing threshold (fewer than family_index_min_retailers in-stock retailers and fewer than family_index_min_priced_children priced child models): noindex and out of the sitemap. null before migration 082
    - `caddence_score` integer, nullable: Deal-quality score 0-100; null when suppressed/unstamped
    - `caddence_score_tier` string, nullable: Score band: excellent / good / fair / poor
    - `caddence_score_flag` string, nullable: Why a score is caveated or absent: provisional (under 30 tracked days) / thin_market / data_suspect / no_history; null = confident
    - `state` string, nullable: Lifecycle state (migration 041): active / tracked (sold out, live retailers still list it) / untracked (only dead retailers or vanished listings). Never 'archived' here — the matview excludes those rows from list surfaces. Null in the pre-migration window.
    - `price_signal` string, nullable: Canonical price state: 'low' (at/near its 90-day low), 'falling' (below typical and dropped this month), 'steady' (near typical), 'above_typical' (above typical). Null when history is too thin to classify or the current price looks like a mis-grouped component.
    - `typical_price` number, nullable: 90-day average min-price — the 'typical' baseline
    - `low_90d` number, nullable: 90-day minimum min-price
    - `below_typical_pct` integer, nullable: Signed % of current vs 90-day typical (negative = below typical)
    - `change_30d_gbp` number, nullable: Signed £ change vs ~30 days ago (negative = cheaper now)
  - `total` integer, required
  - `buyable_total` integer
  - `limit` integer, required
  - `offset` integer, required

## Other responses

- `400`: Bad request syntax or unsupported method

---

[API](https://skmtc.dev/caddence/apis/golf-product-api.md) · [All operations](https://skmtc.dev/caddence/apis/golf-product-api/llms.txt) · [OpenAPI document](https://skmtc.dev/caddence/apis/golf-product-api/revisions/adef6c56f7c0?raw)
