{"openapi":"3.1.0","info":{"title":"Golf Equipment API (free beta)","version":"v1.1","description":"Golf equipment data for apps, retailers and fitters: normalized product search, fit scoring, recommendations, comparison and a catalog change log. Free during the beta.\n\n`info.version` is the API contract version (also `meta.version` and the `X-API-Version` header). Catalog data releases are separate and are listed in the changelog.\n\nEvery endpoint except this document needs an API key (`x-api-key` header or `Authorization: Bearer`). Beta keys start `fms_test_`.\n\nKeyed responses are sent with `Cache-Control: private, no-store`. Catalog data is cached per server for up to 5 minutes, so a response can be up to 5 minutes behind a catalog update.\n\nSend `x-request-id` (1-64 characters from `A-Za-z0-9._-`) to choose the id echoed in `meta.requestId` and the `X-Request-Id` header; anything else is replaced with a generated id.\n\nRate limits: per account (all keys on an account share one allowance), plus a per-IP limit (120 requests a minute by default) that is checked before the key. Every 429 carries `Retry-After` (whole seconds), `X-RateLimit-*` headers and `error.details.resetAt`.\n\nWebhooks are switched off during the beta."},"servers":[{"url":"https://www.fitmyswing.com/api/v1"}],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"x-coverageTarget":{"fromYear":2016,"throughYear":2026},"x-supportedCategories":["driver","iron","driver_shaft","iron_shaft","ball","grip","wedge","putter","fairway_wood","hybrid"],"paths":{"/health":{"get":{"summary":"API health and coverage summary","responses":{"200":{"description":"Health summary","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"X-API-Version":{"$ref":"#/components/headers/XApiVersion"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-Usage-Warning":{"$ref":"#/components/headers/XUsageWarning"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}},"/openapi":{"get":{"summary":"OpenAPI contract","description":"This document. Public: no API key is needed. It is the raw OpenAPI document, not the `{ data, meta }` envelope, and is cached for an hour.","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document","headers":{"X-API-Version":{"$ref":"#/components/headers/XApiVersion"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"405":{"$ref":"#/components/responses/MethodNotAllowed"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}},"/changelog":{"get":{"summary":"Catalog change log","description":"Default feed (`type=catalog`): one entry per changed catalog field, or per product added or removed, newest first. Page with `cursor`; keep your own high-water mark with `since`, which is exclusive. `type=verification` returns the older source-observation feed (field names in camelCase, one entry per product field and verification state) and accepts only `type`, `since`, `category`, `limit` and `cursor`. Unknown query parameters are rejected with 400 and `details.unknown` and `details.supported`.","parameters":[{"name":"type","in":"query","description":"Which feed. `catalog` is the default.","schema":{"type":"string","enum":["catalog","verification"],"default":"catalog"}},{"name":"since","in":"query","description":"Only entries after this instant (exclusive). ISO 8601 date or timestamp.","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","description":"Only entries at or before this instant (inclusive). `type=catalog` only. Must be later than `since`.","schema":{"type":"string","format":"date-time"}},{"name":"productId","in":"query","description":"Only changes to this product. `type=catalog` only.","schema":{"type":"string"}},{"name":"category","in":"query","schema":{"$ref":"#/components/schemas/CurrentEquipmentCategory"}},{"name":"changeType","in":"query","description":"`type=catalog` only.","schema":{"type":"string","enum":["added","updated","removed"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"cursor","in":"query","description":"`pagination.nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated changelog entries","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"X-API-Version":{"$ref":"#/components/headers/XApiVersion"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-Usage-Warning":{"$ref":"#/components/headers/XUsageWarning"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangelogEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}},"/products/search":{"get":{"summary":"Search normalized equipment products","description":"`q` uses the same matcher as the FitMySwing search box: spacing, punctuation and word order do not matter, brand shorthands and roman numerals work, and close typos still match. With `q` and no `sort`, results come best match first (the same as `sort=relevance`); without `q` the default order is brand, model, year. Unknown query parameters are rejected with 400 and `details.unknown` and `details.supported`.","parameters":[{"name":"q","in":"query","description":"Search text, e.g. `stealth 2`, `p790`, `tm qi10`.","schema":{"type":"string"}},{"name":"category","in":"query","schema":{"$ref":"#/components/schemas/CurrentEquipmentCategory"}},{"name":"brand","in":"query","description":"Case, spacing and punctuation are ignored. An exact brand name returns that brand only. Otherwise the name, slug or alias of a manufacturer (`Ben Hogan` finds `Ben Hogan Golf`) or the first words of a brand (`UST` finds `UST Mamiya`) are matched. An unknown brand returns an empty list.","schema":{"type":"string"}},{"name":"yearMin","in":"query","description":"Earliest model year (1980 to 2026). Products with no year are not filtered out.","schema":{"type":"integer","minimum":1980,"maximum":2026}},{"name":"yearMax","in":"query","description":"Latest model year (1980 to 2026). Must not be below `yearMin`.","schema":{"type":"integer","minimum":1980,"maximum":2026}},{"name":"discontinued","in":"query","schema":{"type":"boolean"}},{"name":"minConfidence","in":"query","description":"Minimum confidence score (0–1). Only products with confidenceScore ≥ this value are returned.","schema":{"type":"number","minimum":0,"maximum":1}},{"name":"sort","in":"query","description":"`relevance` is best match first and needs `q` to mean anything. With `q` the default is `relevance`; without it the default is brand, model, year. Any other value orders by that field and uses `q` only as a filter.","schema":{"type":"string","enum":["relevance","brand","confidence","year"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Product search results","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"X-API-Version":{"$ref":"#/components/headers/XApiVersion"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-Usage-Warning":{"$ref":"#/components/headers/XUsageWarning"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductSearchEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}},"/products/{id}":{"get":{"summary":"Get a product by canonical API ID","description":"An id from before a rename (listed in the catalog's id aliases) answers with the current product, sets `meta.resolvedFrom` to the id you sent and `Content-Location` to the canonical URL.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["provenance"]},"description":"Include field-level source observations for this product."}],"responses":{"200":{"description":"Product record","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"X-API-Version":{"$ref":"#/components/headers/XApiVersion"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-Usage-Warning":{"$ref":"#/components/headers/XUsageWarning"},"Content-Location":{"description":"Canonical URL of the product. Only set when the requested id was a legacy id.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}},"/fit-score":{"post":{"summary":"Score product fit for a golfer profile","description":"Scores each product with the same scorer and guardrails as the FitMySwing fitting, so a product scores the same here as it does in `/recommendations`. `swingSpeed` is optional: when it is missing it is estimated from handicap and gender and `golferProfile.swingSpeedEstimated` is `true` in the response. Uses 1 fit credit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FitScoreRequest"}}}},"responses":{"200":{"description":"Fit score results","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"X-API-Version":{"$ref":"#/components/headers/XApiVersion"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-Usage-Warning":{"$ref":"#/components/headers/XUsageWarning"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FitScoreEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}},"/recommendations":{"post":{"summary":"Generate deterministic DB-backed recommendations","description":"Ranks the catalog with the FitMySwing fitting's own scorers and guardrails. `swingSpeed` is optional (estimated from handicap and gender). Without `gender`, women's models are excluded. Without `categories`, the categories follow `fittingFor`. Uses 1 fit credit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecommendationsRequest"}}}},"responses":{"200":{"description":"Ranked recommendations","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"X-API-Version":{"$ref":"#/components/headers/XApiVersion"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-Usage-Warning":{"$ref":"#/components/headers/XUsageWarning"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecommendationsEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}},"/compare":{"post":{"summary":"Compare products and optionally rank by golfer fit","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompareRequest"}}}},"responses":{"200":{"description":"Comparison results","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"X-API-Version":{"$ref":"#/components/headers/XApiVersion"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-Usage-Warning":{"$ref":"#/components/headers/XUsageWarning"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompareEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"No content. Allowed methods and headers are in the Access-Control-* response headers."}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key"},"BearerAuth":{"type":"http","scheme":"bearer"}},"headers":{"XRequestId":{"description":"The request id: your valid `x-request-id`, else a generated `eq_…` id.","schema":{"type":"string"}},"XApiVersion":{"description":"API contract version.","schema":{"type":"string","const":"v1.1"}},"XRateLimitLimit":{"description":"Requests allowed per minute.","schema":{"type":"integer"}},"XRateLimitRemaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"XRateLimitReset":{"description":"Unix time, in seconds, when the window resets.","schema":{"type":"integer"}},"XUsageWarning":{"description":"Present when the account has used 90% or more of its monthly allowance.","schema":{"type":"string"}},"RetryAfter":{"description":"Whole seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"Allow":{"description":"Methods this endpoint supports.","schema":{"type":"string"}}},"responses":{"BadRequest":{"description":"Invalid query parameter or body (including unknown query parameters, a bad cursor, bad percent-encoding in a path id, a non-JSON body, a body over 128 KB or a missing `Content-Type: application/json`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Unauthorized":{"description":"Missing, invalid, revoked or suspended API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"NotFound":{"description":"Product not found. For POST endpoints, `details.productIds` lists the missing ids.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"MethodNotAllowed":{"description":"This endpoint does not support the method","headers":{"Allow":{"$ref":"#/components/headers/Allow"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Per-minute rate limit, per-IP limit or monthly allowance exceeded. Wait `Retry-After` seconds; `error.details.resetAt` is when it resets.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Unexpected server error. Quote `meta.requestId` if you contact us.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"schemas":{"CurrentEquipmentCategory":{"type":"string","enum":["driver","iron","driver_shaft","iron_shaft","ball","grip","wedge","putter","fairway_wood","hybrid"]},"Meta":{"type":"object","required":["version","requestId"],"properties":{"version":{"type":"string","const":"v1.1"},"requestId":{"type":"string"},"resolvedFrom":{"type":"string","description":"`/products/{id}` only: the legacy id that was requested. The response is for the current product."},"coverageTarget":{"type":"object","properties":{"fromYear":{"type":"integer"},"throughYear":{"type":"integer"}}}}},"ErrorEnvelope":{"type":"object","required":["error","meta"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","not_found","rate_limited","method_not_allowed","internal_error","unavailable"],"description":"`method_not_allowed` is 405, `rate_limited` is 429 and `internal_error` is 500. `unavailable` (503) is reserved and not returned by the v1 endpoints."},"message":{"type":"string"},"details":{"description":"Extra context. For `rate_limited`: `resetAt` (ISO 8601) and, for the monthly allowance, `usage`. For unknown query parameters: `unknown` and `supported`. For invalid bodies: zod `formErrors` and `fieldErrors`."}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"PublicProduct":{"type":"object","required":["id","category","brand","model","displayName","year","lifecycleStatus","price","fitAttributes","specs","sourceSummary","confidenceScore","confidence","lastVerifiedAt","legacyRef"],"properties":{"id":{"type":"string","examples":["driver:callaway:elyte-x:2026"]},"category":{"$ref":"#/components/schemas/CurrentEquipmentCategory"},"brand":{"type":"string"},"model":{"type":"string"},"displayName":{"type":"string"},"year":{"type":["integer","null"]},"lifecycleStatus":{"type":"string","enum":["current","recent","discontinued","unknown"],"description":"`current` is not discontinued and model year 2025 or later; `recent` is not discontinued and older; `unknown` has no model year."},"price":{"type":"object","required":["amountUsd","tier","source"],"properties":{"amountUsd":{"type":["number","null"]},"tier":{"type":"string"},"source":{"type":"string","enum":["seed","retailer","unknown"]}}},"fitAttributes":{"type":"object","additionalProperties":true,"description":"Per-category fit data. Wedges also carry `bounceOptions` and `grindOptions` (arrays), `launchBias` and `spinBias`; `bounce` and `grind` are legacy single values and are empty."},"specs":{"type":"object","additionalProperties":true,"description":"Per-category specs. Wedges: `lofts`, `bounceOptions`, `grindOptions`, `construction`, `material`, `soleWidth`, `finish` (`bounce` and `grind` are deprecated aliases of the two option arrays). Putters: `lengthOptions`, `toeHang`, `faceMilling`, `swingSpeedRange`, `headType`, `insert`, `hosel`."},"sourceSummary":{"type":"object","required":["sourceCount","primarySourceType","provenanceStatus"],"properties":{"sourceCount":{"type":"integer"},"primarySourceType":{"type":"string","enum":["seed","manufacturer","retailer","manual","unknown"]},"provenanceStatus":{"type":"string","enum":["unverified_seed","partial","verified","unknown"]}}},"confidenceScore":{"type":"number","minimum":0,"maximum":1,"description":"Overall confidence (shorthand for confidence.overall)"},"confidence":{"type":"object","required":["overall","verificationTier","fields"],"description":"Structured confidence detail with per-field scores","properties":{"overall":{"type":"number","minimum":0,"maximum":1,"description":"Product-level composite confidence"},"verificationTier":{"type":"string","enum":["verified","partial","low","unverified"],"description":"Verification tier derived from overall confidence"},"fields":{"type":"object","additionalProperties":{"type":"number","minimum":0,"maximum":1},"description":"Per-field confidence scores (e.g. { priceUSD: 0.95, lofts: 0.88 })"}}},"lastVerifiedAt":{"type":["string","null"],"format":"date-time"},"legacyRef":{"type":"object","required":["table","id"],"properties":{"table":{"type":"string","enum":["drivers","irons","driver_shafts","iron_shafts","balls","grips","wedges","putters","fairway_woods","hybrids"]},"id":{"type":"integer"}}}}},"GolferProfile":{"type":"object","required":["swingSpeed","handicap"],"description":"Golfer profile for `/compare`, where `swingSpeed` is required.","properties":{"swingSpeed":{"type":"number","minimum":40,"maximum":150},"handicap":{"type":"number","minimum":-10,"maximum":54},"fittingFor":{"type":"string","enum":["driver","irons","both"]},"miss":{"type":"string","enum":["Slice","Hook","Ballooning","Inconsistent","Push","Pull"]},"trajectory":{"type":"string","enum":["Low","Mid","High"]},"tempo":{"type":"string","enum":["Smooth","Moderate","Aggressive"]},"budget":{"type":"string","enum":["Under $300","$300–600","$600–1000","$1000+"],"description":"A plain hyphen is accepted for the range bands (`$300-600` is read as `$300–600`)."},"primaryGoal":{"type":"string","maxLength":500},"gender":{"type":"string","enum":["Men","Women"],"description":"On recommendations and fit-score, a missing gender excludes women's models (flag `womens_model`)."},"currentClubIds":{"type":"array","maxItems":50,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Catalog ids of the clubs the golfer plays today. Recommendations never include them, and may return the golfer's own head as `keepCurrent`. Ignored by compare."}}},"RankingGolferProfile":{"type":"object","required":["handicap"],"description":"Golfer profile for `/fit-score` and `/recommendations`. `swingSpeed` is optional and is estimated from handicap and gender when missing.","properties":{"swingSpeed":{"type":"number","minimum":40,"maximum":150},"handicap":{"type":"number","minimum":-10,"maximum":54},"fittingFor":{"type":"string","enum":["driver","irons","both"]},"miss":{"type":"string","enum":["Slice","Hook","Ballooning","Inconsistent","Push","Pull"]},"trajectory":{"type":"string","enum":["Low","Mid","High"]},"tempo":{"type":"string","enum":["Smooth","Moderate","Aggressive"]},"budget":{"type":"string","enum":["Under $300","$300–600","$600–1000","$1000+"],"description":"A plain hyphen is accepted for the range bands (`$300-600` is read as `$300–600`)."},"primaryGoal":{"type":"string","maxLength":500},"gender":{"type":"string","enum":["Men","Women"],"description":"On recommendations and fit-score, a missing gender excludes women's models (flag `womens_model`)."},"currentClubIds":{"type":"array","maxItems":50,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Catalog ids of the clubs the golfer plays today. Recommendations never include them, and may return the golfer's own head as `keepCurrent`. Ignored by compare."}}},"ResolvedGolferProfile":{"type":"object","required":["swingSpeed","handicap"],"description":"The profile the ranking used: the request profile with `swingSpeed` filled in.","properties":{"swingSpeed":{"type":"number","minimum":40,"maximum":150},"swingSpeedEstimated":{"type":"boolean","const":true,"description":"Present (and `true`) only when `swingSpeed` was estimated because the request had none."},"handicap":{"type":"number","minimum":-10,"maximum":54},"fittingFor":{"type":"string","enum":["driver","irons","both"]},"miss":{"type":"string","enum":["Slice","Hook","Ballooning","Inconsistent","Push","Pull"]},"trajectory":{"type":"string","enum":["Low","Mid","High"]},"tempo":{"type":"string","enum":["Smooth","Moderate","Aggressive"]},"budget":{"type":"string","enum":["Under $300","$300–600","$600–1000","$1000+"],"description":"A plain hyphen is accepted for the range bands (`$300-600` is read as `$300–600`)."},"primaryGoal":{"type":"string","maxLength":500},"gender":{"type":"string","enum":["Men","Women"],"description":"On recommendations and fit-score, a missing gender excludes women's models (flag `womens_model`)."},"currentClubIds":{"type":"array","maxItems":50,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Catalog ids of the clubs the golfer plays today. Recommendations never include them, and may return the golfer's own head as `keepCurrent`. Ignored by compare."}}},"FitScoreResult":{"type":"object","required":["productId","product","totalScore","scoreBreakdown","reasons","warnings"],"properties":{"productId":{"type":"string"},"product":{"$ref":"#/components/schemas/PublicProduct"},"totalScore":{"type":"integer","minimum":0,"maximum":100,"description":"On `/fit-score` and `/recommendations` this equals `matchScore`."},"matchScore":{"type":"integer","minimum":0,"maximum":100,"description":"`rawScore` as a share of `maxScore`, comparable across categories. `/fit-score` and `/recommendations` only."},"rawScore":{"type":"number","description":"The fitting's own score before normalisation."},"maxScore":{"type":"number","description":"The most the scorer for this category can award."},"flags":{"type":"array","items":{"type":"string","enum":["over_budget","off_category","off_band","womens_model","discontinued"]},"description":"Guardrails the product breaks for this golfer: over budget, wrong iron category, outside the shaft weight band, a women's model for a golfer who is not (or did not say), discontinued. `/fit-score` and `/recommendations` compute them the same way, so they agree on a product."},"rank":{"type":"integer","minimum":1,"description":"1-based position in the category's recommendations. `/recommendations` only."},"rankingReasons":{"type":"array","description":"Why `matchScore` is what it is: the fitting scorer's own awards and penalties for this product and golfer. The `points` add up to `rawScore`. Absent for putters. `reasons` and `scoreBreakdown` are the API scorer's and can differ.","items":{"type":"object","required":["signal","points","max","note"],"properties":{"signal":{"type":"string","description":"Stable key, e.g. `speed_window`, `price_tier`, `forgiveness`."},"points":{"type":"number","description":"Points this signal added (negative for a penalty)."},"max":{"type":["number","null"],"description":"The most this signal can add; null when it has no fixed ceiling."},"note":{"type":"string","description":"Plain English."}}}},"rankedBecause":{"type":"string","description":"One sentence built from `rankingReasons`: \"Ranked first because ...\" for rank 1, otherwise \"Scored for you because ...\". Absent for putters."},"scoreBreakdown":{"type":"object","additionalProperties":{"type":"number"}},"reasons":{"type":"array","items":{"type":"string"}},"warnings":{"type":"array","items":{"type":"string","enum":["outside_speed_range","outside_handicap_range","over_budget","off_category","off_band","womens_model","discontinued","low_confidence_data"]}}}},"FitScoreRequest":{"type":"object","required":["golferProfile","productIds"],"properties":{"golferProfile":{"$ref":"#/components/schemas/RankingGolferProfile"},"productIds":{"type":"array","minItems":1,"maxItems":25,"items":{"type":"string"}}}},"RecommendationsRequest":{"type":"object","required":["golferProfile"],"properties":{"golferProfile":{"$ref":"#/components/schemas/RankingGolferProfile"},"categories":{"type":"array","minItems":1,"maxItems":10,"items":{"$ref":"#/components/schemas/CurrentEquipmentCategory"},"description":"Defaults to driver and driver shaft, iron and iron shaft, or all four, following `golferProfile.fittingFor`."},"limit":{"type":"integer","minimum":1,"maximum":20,"description":"Products per category. Default 5."},"excludeDiscontinued":{"type":"boolean","description":"Leave discontinued products out of the ranking."}}},"CompareRequest":{"type":"object","required":["productIds"],"properties":{"productIds":{"type":"array","minItems":2,"maxItems":8,"items":{"type":"string"}},"golferProfile":{"$ref":"#/components/schemas/GolferProfile"}}},"ProductEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"product":{"$ref":"#/components/schemas/PublicProduct"},"provenance":{"type":"object","properties":{"observations":{"type":"array","items":{"$ref":"#/components/schemas/SourceObservation"}},"verification":{"$ref":"#/components/schemas/ProductVerificationSummary"}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"SourceObservation":{"type":"object","required":["entityType","entityId","fieldName","observedValue","sourceType","confidence","verificationStatus","observedAt"],"properties":{"entityType":{"type":"string"},"entityId":{"type":"string"},"fieldName":{"type":"string"},"observedValue":{},"sourceType":{"type":"string","enum":["seed","manufacturer","retailer","manual","community","unknown","manufacturer_claim","derived","editorial"],"description":"Where the value came from. manufacturer_claim, derived and editorial are fit-profile values (launch and spin bias, forgiveness, handicap and swing-speed windows): a claim the manufacturer's page makes (sourceUrl is that page, observedValue.quote the wording), a value derived by a published rule (observedValue.rule), or an editorial judgement. For these, observedValue is an object { value, quote?, rule? }. They are judgement, not spec facts, so they never count towards confidence or verification."},"sourceUrl":{"type":["string","null"]},"confidence":{"type":"number","minimum":0,"maximum":1},"verificationStatus":{"type":"string"},"observedAt":{"type":"string","format":"date-time"}}},"FieldVerificationSummary":{"type":"object","required":["fieldName","status","confidence","sourceCount","primaryValue","normalizedValue","values","lastObservedAt"],"properties":{"fieldName":{"type":"string"},"status":{"type":"string","enum":["verified","partial","conflicting","seed_only"]},"confidence":{"type":"number","minimum":0,"maximum":1},"sourceCount":{"type":"integer"},"primaryValue":{},"normalizedValue":{"type":"string"},"values":{"type":"array","items":{"type":"object","additionalProperties":true}},"lastObservedAt":{"type":["string","null"],"format":"date-time"}}},"ProductVerificationSummary":{"type":"object","required":["productId","status","fieldCount","verifiedFields","partialFields","conflictingFields","seedOnlyFields","confidenceByField","fields"],"properties":{"productId":{"type":"string"},"status":{"type":"string","enum":["verified","partial","conflicting","seed_only"]},"fieldCount":{"type":"integer"},"verifiedFields":{"type":"array","items":{"type":"string"}},"partialFields":{"type":"array","items":{"type":"string"}},"conflictingFields":{"type":"array","items":{"type":"string"}},"seedOnlyFields":{"type":"array","items":{"type":"string"}},"confidenceByField":{"type":"object","additionalProperties":{"type":"number"}},"fields":{"type":"array","items":{"$ref":"#/components/schemas/FieldVerificationSummary"}}}},"CatalogChangeEntry":{"type":"object","description":"One change to the catalog: a field of a product that changed, or a product that was added or removed.","required":["id","productId","category","changeType","fieldName","oldValue","newValue","changedAt","source","prNumber"],"properties":{"id":{"type":"integer"},"productId":{"type":"string"},"category":{"$ref":"#/components/schemas/CurrentEquipmentCategory"},"changeType":{"type":"string","enum":["added","updated","removed"]},"fieldName":{"type":["string","null"],"description":"Public field name (`priceUsd`, `headSizeCc`, `flexOptions`…). Null for added and removed."},"oldValue":{},"newValue":{},"changedAt":{"type":"string","format":"date-time"},"source":{"type":"string","description":"`catalog-sync` (recorded when the change was applied) or `backfill` (rebuilt from the catalog's git history)."},"prNumber":{"type":["integer","null"],"description":"The pull request that made the change, when known."}}},"ChangelogEntry":{"type":"object","description":"A `type=verification` entry: a source-observation event for one product field.","required":["id","productId","category","fieldName","eventType","verificationStatus","confidence","sourceCount","observedAt","summary","values"],"properties":{"id":{"type":"string"},"productId":{"type":"string"},"category":{"$ref":"#/components/schemas/CurrentEquipmentCategory"},"fieldName":{"type":"string","description":"camelCase public field name (`priceUsd`, `headSizeCc`, `torqueRange`)."},"eventType":{"type":"string","enum":["field_verified","field_observed","field_conflict"]},"verificationStatus":{"type":"string","enum":["verified","partial","conflicting","seed_only"]},"confidence":{"type":"number","minimum":0,"maximum":1},"sourceCount":{"type":"integer"},"observedAt":{"type":"string","format":"date-time"},"summary":{"type":"string"},"values":{"type":"array","description":"Distinct observed values."}}},"Pagination":{"type":"object","properties":{"limit":{"type":"integer"},"cursor":{"type":["string","null"]},"nextCursor":{"type":["string","null"]},"total":{"type":"integer"}}},"ProductSearchEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/PublicProduct"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"ChangelogEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"entries":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/CatalogChangeEntry"},{"$ref":"#/components/schemas/ChangelogEntry"}]},"description":"`CatalogChangeEntry` by default; `ChangelogEntry` when `type=verification`."},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"FitScoreEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"scores":{"type":"array","items":{"$ref":"#/components/schemas/FitScoreResult"}},"golferProfile":{"$ref":"#/components/schemas/ResolvedGolferProfile"}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"RecommendationsEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"golferProfile":{"$ref":"#/components/schemas/ResolvedGolferProfile"},"recommendations":{"type":"array","items":{"type":"object","properties":{"category":{"$ref":"#/components/schemas/CurrentEquipmentCategory"},"products":{"type":"array","items":{"$ref":"#/components/schemas/FitScoreResult"},"description":"Best first, each with `rank`."},"keepCurrent":{"$ref":"#/components/schemas/FitScoreResult","description":"The golfer's own head from `currentClubIds`, when it would have ranked first or close to the top pick. Driver, iron, fairway wood, hybrid and wedge only."}}}},"modelUsage":{"type":"object","properties":{"llmUsed":{"type":"boolean","const":false},"method":{"type":"string","const":"deterministic_db_ranking"},"rankingVersion":{"type":"string","description":"Version of the ranking that produced these results."}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"CompareEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"products":{"type":"array","items":{"type":"object","additionalProperties":true}},"fitScores":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/FitScoreResult"}},{"type":"null"}],"description":"Scored with the same fitting scorer, flags and warnings as `/fit-score` and `/recommendations` (so `matchScore`, `rawScore`, `maxScore` and `flags` are present and `totalScore` equals `matchScore`), best first. Has no `rank`. Null when no `golferProfile` was sent."},"golferProfile":{"oneOf":[{"$ref":"#/components/schemas/ResolvedGolferProfile"},{"type":"null"}],"description":"The profile the scores used. Null when no `golferProfile` was sent."},"bestFitProductId":{"type":["string","null"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"HealthEnvelope":{"type":"object","properties":{"data":{"type":"object","additionalProperties":true},"meta":{"$ref":"#/components/schemas/Meta"}}}}}}