Golf Equipment API docs
Normalized product data, fit scoring, recommendations and comparisons. Every response carries confidence data and list endpoints use cursors.
Authentication
Every request needs an API key. Send it in an x-api-key header, or as a Bearer token in the Authorization header.
During the free beta every key starts fms_test_. Create and manage up to 10 keys per account in the developer console.
curl https://www.fitmyswing.com/api/v1/health \ -H "x-api-key: fms_test_..."
curl https://www.fitmyswing.com/api/v1/health \ -H "Authorization: Bearer fms_test_..."
Rate limits
Limits apply per account: every key on the account shares one allowance. Requests are counted in a sliding one-minute window shared across our servers; if that shared counter is briefly unavailable, each server counts on its own. A separate per-IP limit (120 requests a minute by default) is checked before your key. The sandbox limits below apply during the beta; email us if you need higher ones.
| Tier | Requests a minute | Data calls a month | Fit credits a month |
|---|---|---|---|
| Sandbox | 50 | 2,500 | 100 |
Authenticated responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). When you are over a limit you get a 429 with those headers, a Retry-After header (whole seconds) and error.details.resetAt (an ISO 8601 time). Wait that long before retrying. The monthly allowance returns the same 429 and resets on the first of the month (UTC); at 90% of it responses carry an X-Usage-Warning header.
Errors
Every error uses the same JSON shape:
{ "error": { "code": "not_found", "message": "Product not found." }, "meta": { "version": "v1.1", "requestId": "eq_..." } }| Code | HTTP | Meaning |
|---|---|---|
| unauthorized | 401 | Missing or invalid API key |
| invalid_request | 400 | Malformed body or invalid parameters |
| not_found | 404 | Resource not found |
| method_not_allowed | 405 | The endpoint does not support that method (see the Allow header) |
| rate_limited | 429 | Rate limit, per-IP limit or monthly allowance exceeded (see Retry-After) |
| internal_error | 500 | Unexpected server error |
| unavailable | 503 | Reserved. The v1 endpoints do not return it today |
A bad query parameter name returns invalid_request with details.unknown and details.supported. Every response has an X-Request-Id header and meta.requestId: send your own x-request-id (1 to 64 characters from letters, digits, ., _ and -) to choose it, otherwise we generate one. Browsers can call the API directly: every endpoint answers OPTIONS preflight requests.
Pagination
List endpoints use cursors. Pass the nextCursor from one response as cursor to get the next page. When nextCursor is null, you’ve reached the end.
Freshness and caching
Keyed responses are sent with Cache-Control: private, no-store, so do not cache them in a shared cache. Each of our servers keeps the catalog in memory for up to 5 minutes, so a response can be up to 5 minutes behind a catalog update. Use GET /v1/changelog to see what changed and when.
Confidence and data quality
Every product has a confidence object: an overall score from 0 to 1, a verification tier and a score per field. Use minConfidence to filter out weaker data.
Webhooks
Webhooks are switched off during the beta.
OpenAPI spec
The full contract is public, with no key needed, at /api/v1/openapi. It is the raw OpenAPI 3.1 document, not the { data, meta } envelope. Its version is the API version, v1.1; catalog data releases are separate and listed in the changelog.
TypeScript SDK
A typed TypeScript client is on the way. Until it’s published, call the API with plain fetch, as the examples below do. Every example needs Node 18 or newer, or any runtime with fetch.
Endpoints
Parameters, examples and response shapes for every endpoint. Sample responses are real output, trimmed for length: some fields and list items are left out.
/v1/health
Health & coverage summary
Returns API status, version, product counts by category, and coverage metadata. Useful for monitoring integrations. Counts can be up to 5 minutes behind a catalog update.
{
"data": {
"status": "ok",
"version": "v1.1",
"dbAvailable": true,
"supportedCategories": [
"driver",
"iron",
"driver_shaft",
"iron_shaft",
"ball",
"grip",
"wedge",
"putter",
"fairway_wood",
"hybrid"
],
"plannedCategories": [
"bag"
],
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"coverage": {
"dbAvailable": true,
"categories": {
"driver": 255,
"iron": 321,
"driver_shaft": 137,
"iron_shaft": 84,
"ball": 142,
"grip": 40,
"wedge": 120,
"putter": 253,
"fairway_wood": 130,
"hybrid": 94
},
"totalProducts": 1576
},
"authMode": "tenant"
},
"meta": {
"version": "v1.1",
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"requestId": "eq_sample"
}
}const res = await fetch("https://www.fitmyswing.com/api/v1/health", {
headers: { "x-api-key": process.env.FITMYSWING_API_KEY! },
})
const { data } = await res.json()
console.log(data.status) // "ok"curl "https://www.fitmyswing.com/api/v1/health" \ -H "x-api-key: fms_test_..."
import requests
r = requests.get("https://www.fitmyswing.com/api/v1/health",
headers={"x-api-key": "fms_test_..."})
print(r.json()["data"]["status"])/v1/products/search
Search products
Search and filter normalized equipment products. q uses the same matcher as the FitMySwing search box: spacing, punctuation and word order do not matter, and brand shorthands and close typos still match. With q and no sort, the best match comes first. Unknown query parameters are rejected with a 400 that lists the supported ones in details.supported.
| Name | In | Type | Description |
|---|---|---|---|
| q | query | string | Search text, e.g. g440, p-790 or tm qi10 |
| category | query | string | Filter by category (driver, iron, driver_shaft, iron_shaft, ball, grip, wedge, putter, fairway_wood, hybrid) |
| brand | query | string | Brand name, ignoring case, spacing and punctuation. An exact brand returns that brand only; otherwise a manufacturer name, slug or alias (Ben Hogan finds Ben Hogan Golf) or the first words of a brand (UST finds UST Mamiya). An unknown brand returns an empty list. |
| yearMin | query | integer | Earliest model year (1980–2026) |
| yearMax | query | integer | Latest model year (1980–2026) |
| discontinued | query | boolean | Filter by discontinued status |
| minConfidence | query | number | Minimum confidence score (0–1) |
| sort | query | string | relevance, brand, confidence or year. With q the default is relevance; without q it is brand, model, year. Any other sort orders by that field and uses q only as a filter. |
| limit | query | integer | Results per page (1–100, default 25) |
| cursor | query | string | Pagination cursor from previous response |
{
"data": {
"products": [
{
"id": "driver:ping:g440-k:2026",
"category": "driver",
"brand": "Ping",
"model": "G440 K",
"displayName": "Ping G440 K",
"year": 2026,
"lifecycleStatus": "current",
"price": {
"amountUsd": 600,
"tier": "premium",
"source": "seed"
},
"sourceSummary": {
"sourceCount": 3,
"primarySourceType": "manufacturer",
"provenanceStatus": "verified"
},
"confidenceScore": 0.94,
"confidence": {
"overall": 0.94,
"verificationTier": "verified",
"fields": {
"adjustable": 0.89,
"brand": 1,
"headSize": 0.89,
"lofts": 0.89,
"model": 1,
"year": 1
}
},
"lastVerifiedAt": "2026-10-01T00:00:00.000Z",
"legacyRef": {
"table": "drivers",
"id": 117
}
},
{
"id": "driver:ping:g440-k-hl:2026",
"category": "driver",
"brand": "Ping",
"model": "G440 K HL",
"displayName": "Ping G440 K HL",
"year": 2026,
"lifecycleStatus": "current",
"price": {
"amountUsd": 650,
"tier": "premium",
"source": "seed"
},
"sourceSummary": {
"sourceCount": 3,
"primarySourceType": "manufacturer",
"provenanceStatus": "verified"
},
"confidenceScore": 0.98,
"confidence": {
"overall": 0.98,
"verificationTier": "verified",
"fields": {
"adjustable": 0.96,
"brand": 1,
"headSize": 0.96,
"lofts": 0.96,
"model": 1,
"priceUSD": 0.96,
"year": 1
}
},
"lastVerifiedAt": "2026-10-01T00:00:00.000Z",
"legacyRef": {
"table": "drivers",
"id": 116
}
}
],
"pagination": {
"limit": 2,
"cursor": null,
"nextCursor": "eyJvZmZzZXQiOjJ9",
"total": 7
}
},
"meta": {
"version": "v1.1",
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"requestId": "eq_sample"
}
}const res = await fetch("https://www.fitmyswing.com/api/v1/products/search?category=driver&q=g440&limit=2", {
headers: { "x-api-key": process.env.FITMYSWING_API_KEY! },
})
const { data } = await res.json()
console.log(data.products.length, data.pagination.total)curl "https://www.fitmyswing.com/api/v1/products/search?category=driver&q=g440&limit=2" \ -H "x-api-key: fms_test_..."
import requests
r = requests.get("https://www.fitmyswing.com/api/v1/products/search?category=driver&q=g440&limit=2",
headers={"x-api-key": "fms_test_..."})
products = r.json()["data"]["products"]/v1/products/{id}
Get product by ID
Retrieve a single product by its canonical ID. Optionally include source provenance (field-level observations and verification summary). Fit-profile values (launch and spin bias, forgiveness, handicap and swing-speed windows) appear as observations with a sourceType of manufacturer_claim, derived (by a published rule) or editorial; they do not count towards confidence. An id from before a rename still works: the response is the current product, meta.resolvedFrom is the id you sent and the Content-Location header is the canonical URL. Wedges carry bounceOptions, grindOptions, construction, material and soleWidth; putters carry lengthOptions, toeHang, faceMilling and swingSpeedRange.
| Name | In | Type | Description |
|---|---|---|---|
| id* | path | string | Canonical product ID (e.g. wedge:callaway:opus-sp:2025) |
| include | query | string | Set to 'provenance' to include source observations |
{
"data": {
"product": {
"id": "wedge:callaway:opus-sp:2025",
"category": "wedge",
"brand": "Callaway",
"model": "Opus SP",
"displayName": "Callaway Opus SP",
"year": 2025,
"lifecycleStatus": "current",
"price": {
"amountUsd": 200,
"tier": "premium",
"source": "seed"
},
"sourceSummary": {
"sourceCount": 3,
"primarySourceType": "manufacturer",
"provenanceStatus": "verified"
},
"confidenceScore": 1,
"confidence": {
"overall": 1,
"verificationTier": "verified",
"fields": {
"bounceOptions": 1,
"brand": 1,
"finish": 1,
"grindOptions": 1,
"lofts": 1,
"material": 1,
"model": 1,
"priceUSD": 1,
"year": 1
}
},
"lastVerifiedAt": "2026-10-01T00:00:00.000Z",
"legacyRef": {
"table": "wedges",
"id": 887
},
"fitAttributes": {
"swingSpeedRange": [
70,
120
],
"handicapRange": [
0,
28
],
"bounceOptions": [
"6",
"8",
"10",
"12",
"14"
],
"grindOptions": [
"T",
"C",
"S",
"W",
"X"
],
"launchBias": "mid",
"spinBias": "high",
"bestFor": "Players who want a lower, more controlled wedge flight with high spin from a tour-shaped head."
},
"specs": {
"lofts": [
"48",
"50",
"52",
"54",
"56",
"58",
"60"
],
"bounceOptions": [
"6",
"8",
"10",
"12",
"14"
],
"grindOptions": [
"T",
"C",
"S",
"W",
"X"
],
"bounce": [
"6",
"8",
"10",
"12",
"14"
],
"grind": [
"T",
"C",
"S",
"W",
"X"
],
"finish": "chrome",
"material": "carbon-steel",
"soleWidth": "medium",
"keyTech": "Spin Pocket two-piece head (forged 1025 face, cast 8620 body), Spin Gen 2.0 face",
"url": "https://www.callawaygolf.com/product/wedges-2025-opus-sp-chrome"
}
}
},
"meta": {
"version": "v1.1",
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"requestId": "eq_sample"
}
}const res = await fetch("https://www.fitmyswing.com/api/v1/products/wedge:callaway:opus-sp:2025?include=provenance", {
headers: { "x-api-key": process.env.FITMYSWING_API_KEY! },
})
const { data } = await res.json()
console.log(data.product.displayName)curl "https://www.fitmyswing.com/api/v1/products/wedge:callaway:opus-sp:2025?include=provenance" \ -H "x-api-key: fms_test_..."
import requests
r = requests.get("https://www.fitmyswing.com/api/v1/products/wedge:callaway:opus-sp:2025?include=provenance",
headers={"x-api-key": "fms_test_..."})
product = r.json()["data"]["product"]/v1/fit-score
uses a fit creditScore product fit
Score how well one or more products fit a golfer profile, with the same scorer and guardrails as the FitMySwing fitting. Each score has totalScore (0–100, equal to matchScore), rawScore and maxScore, the breakdown by factor, reasons, warnings and flags (over_budget, off_category, off_band, womens_model, discontinued), plus rankingReasons (the fitting scorer's own awards and penalties, adding up to rawScore; none for putters) and rankedBecause (one sentence built from them). swingSpeed is optional: when it is missing it is estimated from handicap and gender, and the resolved golferProfile in the response says so with swingSpeedEstimated. Without gender, women's models are flagged. Consumes 1 fit credit per call.
{
"golferProfile": {
"handicap": 12,
"miss": "Slice",
"trajectory": "Mid",
"budget": "$300–600",
"gender": "Men"
},
"productIds": [
"driver:ping:g440-max:2025",
"driver:titleist:gts2:2026"
]
}{
"data": {
"scores": [
{
"productId": "driver:ping:g440-max:2025",
"product": {
"id": "driver:ping:g440-max:2025",
"category": "driver",
"brand": "Ping",
"model": "G440 MAX",
"displayName": "Ping G440 MAX",
"year": 2025,
"lifecycleStatus": "current",
"price": {
"amountUsd": 500,
"tier": "premium",
"source": "seed"
},
"confidenceScore": 0.94,
"confidence": {
"overall": 0.94,
"verificationTier": "verified"
}
},
"totalScore": 57,
"matchScore": 57,
"rawScore": 57,
"maxScore": 100,
"flags": [
"over_budget"
],
"scoreBreakdown": {
"categoryFit": 12,
"speedFit": 14,
"handicapFit": 12,
"budgetFit": 3,
"launchSpinFit": 8,
"forgivenessWorkabilityFit": 10,
"shaftWeightKickFit": 0,
"recencyFit": 9
},
"reasons": [
"Category matches the golfer profile.",
"Swing speed falls close to the product fit window.",
"Handicap range is aligned with the product target player.",
"Launch and spin bias support the stated ball-flight needs.",
"Forgiveness/workability profile is appropriate for the player."
],
"warnings": [
"over_budget"
],
"rankingReasons": [
{
"signal": "category",
"points": 15,
"max": 25,
"note": "It is a max forgiveness head, one of the types that suit you"
},
{
"signal": "handicap_window",
"points": 16,
"max": 20,
"note": "Your 12 handicap sits inside its 5-28 handicap window"
},
{
"signal": "speed_window",
"points": 14,
"max": 15,
"note": "Your estimated 93 mph sits inside its 80-110 mph window"
},
{
"signal": "price_tier",
"points": 3,
"max": 10,
"note": "Its premium price tier is not the mid tier your $300–600 budget points to"
},
{
"signal": "recency",
"points": 9,
"max": 10,
"note": "It is a 2025 model, and newer models score higher"
}
],
"rankedBecause": "Scored for you because your 12 handicap sits inside its 5-28 handicap window; it is a max forgiveness head, one of the types that suit you; and your estimated 93 mph sits inside its 80-110 mph window."
},
{
"productId": "driver:titleist:gts2:2026",
"product": {
"id": "driver:titleist:gts2:2026",
"category": "driver",
"brand": "Titleist",
"model": "GTS2",
"displayName": "Titleist GTS2",
"year": 2026,
"lifecycleStatus": "current",
"price": {
"amountUsd": 699,
"tier": "premium",
"source": "seed"
},
"confidenceScore": 0.96,
"confidence": {
"overall": 0.96,
"verificationTier": "verified"
}
},
"totalScore": 58,
"matchScore": 58,
"rawScore": 58,
"maxScore": 100,
"flags": [
"over_budget"
],
"scoreBreakdown": {
"categoryFit": 12,
"speedFit": 11,
"handicapFit": 14,
"budgetFit": 3,
"launchSpinFit": 8,
"forgivenessWorkabilityFit": 11,
"shaftWeightKickFit": 0,
"recencyFit": 10
},
"reasons": [
"Category matches the golfer profile.",
"Swing speed falls close to the product fit window.",
"Handicap range is aligned with the product target player.",
"Launch and spin bias support the stated ball-flight needs.",
"Forgiveness/workability profile is appropriate for the player."
],
"warnings": [
"over_budget"
],
"rankingReasons": [
{
"signal": "category",
"points": 15,
"max": 25,
"note": "It is a max forgiveness head, one of the types that suit you"
},
{
"signal": "handicap_window",
"points": 19,
"max": 20,
"note": "Your 12 handicap sits inside its 0-22 handicap window"
},
{
"signal": "speed_window",
"points": 11,
"max": 15,
"note": "Your estimated 93 mph sits inside its 85-115 mph window"
},
{
"signal": "price_tier",
"points": 3,
"max": 10,
"note": "Its premium price tier is not the mid tier your $300–600 budget points to"
},
{
"signal": "recency",
"points": 10,
"max": 10,
"note": "It is a 2026 model, and newer models score higher"
}
],
"rankedBecause": "Scored for you because your 12 handicap sits inside its 0-22 handicap window; it is a max forgiveness head, one of the types that suit you; and your estimated 93 mph sits inside its 85-115 mph window."
}
],
"golferProfile": {
"handicap": 12,
"miss": "Slice",
"trajectory": "Mid",
"budget": "$300–600",
"gender": "Men",
"swingSpeed": 93,
"swingSpeedEstimated": true
}
},
"meta": {
"version": "v1.1",
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"requestId": "eq_sample"
}
}const res = await fetch("https://www.fitmyswing.com/api/v1/fit-score", {
method: "POST",
headers: { "x-api-key": process.env.FITMYSWING_API_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({"golferProfile":{"handicap":12,"miss":"Slice","trajectory":"Mid","budget":"$300–600","gender":"Men"},"productIds":["driver:ping:g440-max:2025","driver:titleist:gts2:2026"]}),
})
const { data } = await res.json()
console.log(data.scores[0].totalScore, data.scores[0].flags)curl -X POST https://www.fitmyswing.com/api/v1/fit-score \
-H "x-api-key: fms_test_..." \
-H "Content-Type: application/json" \
-d '{"golferProfile":{"handicap":12,"miss":"Slice","trajectory":"Mid","budget":"$300–600","gender":"Men"},"productIds":["driver:ping:g440-max:2025","driver:titleist:gts2:2026"]}'import requests
r = requests.post("https://www.fitmyswing.com/api/v1/fit-score",
json={"golferProfile":{"handicap":12,"miss":"Slice","trajectory":"Mid","budget":"$300–600","gender":"Men"},"productIds":["driver:ping:g440-max:2025","driver:titleist:gts2:2026"]},
headers={"x-api-key": "fms_test_..."})
scores = r.json()["data"]["scores"]/v1/recommendations
uses a fit creditGet recommendations
Generate deterministic, DB-backed product recommendations for a golfer profile, ranked with the FitMySwing fitting's own scorers, so the API and the site pick the same clubs. Each product has rank, matchScore (0–100 against the most that scorer can award, comparable across categories), rawScore, maxScore, flags, and rankingReasons (the fitting scorer's own awards and penalties, adding up to rawScore) with rankedBecause (one sentence built from them), none for putters. Optional: swingSpeed (estimated from handicap and gender when missing), currentClubIds (clubs the golfer plays today: never recommended, and returned as keepCurrent when they would still rank at or near the top) and excludeDiscontinued. Without gender, women's models are left out. budget accepts $300-600 with a plain hyphen. modelUsage.rankingVersion names the ranking version. No LLM involved. Consumes 1 fit credit per call.
{
"golferProfile": {
"swingSpeed": 95,
"handicap": 12,
"miss": "Slice",
"fittingFor": "driver",
"budget": "$300–600",
"gender": "Men"
},
"categories": [
"driver"
],
"limit": 2,
"excludeDiscontinued": true
}{
"data": {
"golferProfile": {
"swingSpeed": 95,
"handicap": 12,
"fittingFor": "driver",
"miss": "Slice",
"budget": "$300–600",
"gender": "Men"
},
"recommendations": [
{
"category": "driver",
"products": [
{
"productId": "driver:ben-hogan-golf:ptx-max:2025",
"product": {
"id": "driver:ben-hogan-golf:ptx-max:2025",
"category": "driver",
"brand": "Ben Hogan Golf",
"model": "PTx MAX",
"displayName": "Ben Hogan Golf PTx MAX",
"year": 2025,
"lifecycleStatus": "current",
"price": {
"amountUsd": 380,
"tier": "mid",
"source": "seed"
},
"confidenceScore": 0.96,
"confidence": {
"overall": 0.96,
"verificationTier": "verified"
}
},
"totalScore": 63,
"matchScore": 63,
"rank": 1,
"rawScore": 63,
"maxScore": 100,
"flags": [],
"scoreBreakdown": {
"categoryFit": 12,
"speedFit": 13,
"handicapFit": 12,
"budgetFit": 10,
"launchSpinFit": 8,
"forgivenessWorkabilityFit": 10,
"shaftWeightKickFit": 0,
"recencyFit": 9
},
"reasons": [
"Category matches the golfer profile.",
"Swing speed falls close to the product fit window.",
"Handicap range is aligned with the product target player.",
"Launch and spin bias support the stated ball-flight needs.",
"Forgiveness/workability profile is appropriate for the player.",
"Price tier fits the stated budget."
],
"warnings": [],
"rankingReasons": [
{
"signal": "category",
"points": 15,
"max": 25,
"note": "It is a max forgiveness head, one of the types that suit you"
},
{
"signal": "handicap_window",
"points": 16,
"max": 20,
"note": "Your 12 handicap sits inside its 5-30 handicap window"
},
{
"signal": "speed_window",
"points": 13,
"max": 15,
"note": "Your 95 mph sits inside its 75-108 mph window"
},
{
"signal": "price_tier",
"points": 10,
"max": 10,
"note": "Its mid price tier matches your $300–600 budget"
},
{
"signal": "recency",
"points": 9,
"max": 10,
"note": "It is a 2025 model, and newer models score higher"
}
],
"rankedBecause": "Ranked first because your 12 handicap sits inside its 5-30 handicap window; it is a max forgiveness head, one of the types that suit you; and your 95 mph sits inside its 75-108 mph window."
},
{
"productId": "driver:sub-70:859:2024",
"product": {
"id": "driver:sub-70:859:2024",
"category": "driver",
"brand": "Sub 70",
"model": "859",
"displayName": "Sub 70 859",
"year": 2024,
"lifecycleStatus": "recent",
"price": {
"amountUsd": 399,
"tier": "mid",
"source": "seed"
},
"confidenceScore": 0.98,
"confidence": {
"overall": 0.98,
"verificationTier": "verified"
}
},
"totalScore": 60,
"matchScore": 60,
"rank": 2,
"rawScore": 60,
"maxScore": 100,
"flags": [],
"scoreBreakdown": {
"categoryFit": 12,
"speedFit": 13,
"handicapFit": 10,
"budgetFit": 10,
"launchSpinFit": 8,
"forgivenessWorkabilityFit": 9,
"shaftWeightKickFit": 0,
"recencyFit": 8
},
"reasons": [
"Category matches the golfer profile.",
"Swing speed falls close to the product fit window.",
"Handicap range is aligned with the product target player.",
"Launch and spin bias support the stated ball-flight needs.",
"Price tier fits the stated budget."
],
"warnings": [],
"rankingReasons": [
{
"signal": "category",
"points": 15,
"max": 25,
"note": "It is a max forgiveness head, one of the types that suit you"
},
{
"signal": "handicap_window",
"points": 14,
"max": 20,
"note": "Your 12 handicap sits inside its 8-30 handicap window"
},
{
"signal": "speed_window",
"points": 13,
"max": 15,
"note": "Your 95 mph sits inside its 75-108 mph window"
},
{
"signal": "price_tier",
"points": 10,
"max": 10,
"note": "Its mid price tier matches your $300–600 budget"
},
{
"signal": "recency",
"points": 8,
"max": 10,
"note": "It is a 2024 model, and newer models score higher"
}
],
"rankedBecause": "Scored for you because it is a max forgiveness head, one of the types that suit you; your 12 handicap sits inside its 8-30 handicap window; and your 95 mph sits inside its 75-108 mph window."
}
]
}
],
"modelUsage": {
"llmUsed": false,
"method": "deterministic_db_ranking",
"rankingVersion": "r6-2026-10-10"
}
},
"meta": {
"version": "v1.1",
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"requestId": "eq_sample"
}
}const res = await fetch("https://www.fitmyswing.com/api/v1/recommendations", {
method: "POST",
headers: { "x-api-key": process.env.FITMYSWING_API_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({"golferProfile":{"swingSpeed":95,"handicap":12,"miss":"Slice","fittingFor":"driver","budget":"$300–600","gender":"Men"},"categories":["driver"],"limit":2,"excludeDiscontinued":true}),
})
const { data } = await res.json()
data.recommendations[0].products.forEach((p) => console.log(p.rank, p.product.displayName, p.matchScore))curl -X POST https://www.fitmyswing.com/api/v1/recommendations \
-H "x-api-key: fms_test_..." \
-H "Content-Type: application/json" \
-d '{"golferProfile":{"swingSpeed":95,"handicap":12,"miss":"Slice","fittingFor":"driver","budget":"$300–600","gender":"Men"},"categories":["driver"],"limit":2,"excludeDiscontinued":true}'import requests
r = requests.post("https://www.fitmyswing.com/api/v1/recommendations",
json={"golferProfile":{"swingSpeed":95,"handicap":12,"miss":"Slice","fittingFor":"driver","budget":"$300–600","gender":"Men"},"categories":["driver"],"limit":2,"excludeDiscontinued":True},
headers={"x-api-key": "fms_test_..."})
recs = r.json()["data"]["recommendations"]/v1/compare
Compare products
Compare 2–8 products side-by-side. Optionally score each against a golfer profile (swingSpeed is required here) to find the best fit. Returns normalized specs, price and lifecycle. Scores use the same scorer, flags and warnings as /fit-score and /recommendations, so a product has the same matchScore, rawScore, maxScore and flags in all three; they carry no rank. The response echoes the golferProfile used.
{
"productIds": [
"driver:ping:g440-max:2025",
"driver:titleist:gts2:2026"
],
"golferProfile": {
"swingSpeed": 95,
"handicap": 12
}
}{
"data": {
"products": [
{
"id": "driver:ping:g440-max:2025",
"displayName": "Ping G440 MAX",
"category": "driver",
"year": 2025,
"lifecycleStatus": "current",
"price": {
"amountUsd": 500,
"tier": "premium",
"source": "seed"
},
"confidenceScore": 0.94
},
{
"id": "driver:titleist:gts2:2026",
"displayName": "Titleist GTS2",
"category": "driver",
"year": 2026,
"lifecycleStatus": "current",
"price": {
"amountUsd": 699,
"tier": "premium",
"source": "seed"
},
"confidenceScore": 0.96
}
],
"fitScores": [
{
"productId": "driver:titleist:gts2:2026",
"product": {
"id": "driver:titleist:gts2:2026",
"category": "driver",
"brand": "Titleist",
"model": "GTS2",
"displayName": "Titleist GTS2",
"year": 2026,
"lifecycleStatus": "current",
"price": {
"amountUsd": 699,
"tier": "premium",
"source": "seed"
},
"confidenceScore": 0.96,
"confidence": {
"overall": 0.96,
"verificationTier": "verified"
}
},
"totalScore": 56,
"matchScore": 56,
"rawScore": 56,
"maxScore": 100,
"flags": [],
"scoreBreakdown": {
"categoryFit": 12,
"speedFit": 12,
"handicapFit": 14,
"budgetFit": 7,
"launchSpinFit": 8,
"forgivenessWorkabilityFit": 11,
"shaftWeightKickFit": 0,
"recencyFit": 10
},
"reasons": [
"Category matches the golfer profile.",
"Swing speed falls close to the product fit window.",
"Handicap range is aligned with the product target player.",
"Launch and spin bias support the stated ball-flight needs.",
"Forgiveness/workability profile is appropriate for the player."
],
"warnings": [],
"rankingReasons": [
{
"signal": "category",
"points": 15,
"max": 25,
"note": "It is a max forgiveness head, one of the types that suit you"
},
{
"signal": "handicap_window",
"points": 19,
"max": 20,
"note": "Your 12 handicap sits inside its 0-22 handicap window"
},
{
"signal": "speed_window",
"points": 12,
"max": 15,
"note": "Your 95 mph sits inside its 85-115 mph window"
},
{
"signal": "recency",
"points": 10,
"max": 10,
"note": "It is a 2026 model, and newer models score higher"
}
],
"rankedBecause": "Scored for you because your 12 handicap sits inside its 0-22 handicap window; it is a max forgiveness head, one of the types that suit you; and your 95 mph sits inside its 85-115 mph window."
},
{
"productId": "driver:ping:g440-max:2025",
"product": {
"id": "driver:ping:g440-max:2025",
"category": "driver",
"brand": "Ping",
"model": "G440 MAX",
"displayName": "Ping G440 MAX",
"year": 2025,
"lifecycleStatus": "current",
"price": {
"amountUsd": 500,
"tier": "premium",
"source": "seed"
},
"confidenceScore": 0.94,
"confidence": {
"overall": 0.94,
"verificationTier": "verified"
}
},
"totalScore": 55,
"matchScore": 55,
"rawScore": 55,
"maxScore": 100,
"flags": [],
"scoreBreakdown": {
"categoryFit": 12,
"speedFit": 15,
"handicapFit": 12,
"budgetFit": 7,
"launchSpinFit": 8,
"forgivenessWorkabilityFit": 10,
"shaftWeightKickFit": 0,
"recencyFit": 9
},
"reasons": [
"Category matches the golfer profile.",
"Swing speed falls close to the product fit window.",
"Handicap range is aligned with the product target player.",
"Launch and spin bias support the stated ball-flight needs.",
"Forgiveness/workability profile is appropriate for the player."
],
"warnings": [],
"rankingReasons": [
{
"signal": "category",
"points": 15,
"max": 25,
"note": "It is a max forgiveness head, one of the types that suit you"
},
{
"signal": "handicap_window",
"points": 16,
"max": 20,
"note": "Your 12 handicap sits inside its 5-28 handicap window"
},
{
"signal": "speed_window",
"points": 15,
"max": 15,
"note": "Your 95 mph sits inside its 80-110 mph window"
},
{
"signal": "recency",
"points": 9,
"max": 10,
"note": "It is a 2025 model, and newer models score higher"
}
],
"rankedBecause": "Scored for you because your 12 handicap sits inside its 5-28 handicap window; it is a max forgiveness head, one of the types that suit you; and your 95 mph sits inside its 80-110 mph window."
}
],
"golferProfile": {
"swingSpeed": 95,
"handicap": 12
},
"bestFitProductId": "driver:titleist:gts2:2026"
},
"meta": {
"version": "v1.1",
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"requestId": "eq_sample"
}
}const res = await fetch("https://www.fitmyswing.com/api/v1/compare", {
method: "POST",
headers: { "x-api-key": process.env.FITMYSWING_API_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({"productIds":["driver:ping:g440-max:2025","driver:titleist:gts2:2026"],"golferProfile":{"swingSpeed":95,"handicap":12}}),
})
const { data } = await res.json()
console.log("Best fit:", data.bestFitProductId)curl -X POST https://www.fitmyswing.com/api/v1/compare \
-H "x-api-key: fms_test_..." \
-H "Content-Type: application/json" \
-d '{"productIds":["driver:ping:g440-max:2025","driver:titleist:gts2:2026"],"golferProfile":{"swingSpeed":95,"handicap":12}}'import requests
r = requests.post("https://www.fitmyswing.com/api/v1/compare",
json={"productIds":["driver:ping:g440-max:2025","driver:titleist:gts2:2026"],"golferProfile":{"swingSpeed":95,"handicap":12}},
headers={"x-api-key": "fms_test_..."})
print("Best:", r.json()["data"]["bestFitProductId"])/v1/changelog
Catalog change log
What changed in the catalog, newest first: one entry per changed field, or per product added or removed. Keep a high-water mark and pass it as since (exclusive) to sync. type=verification returns the older source-observation feed instead (camelCase field names, one entry per product field) and accepts only type, since, category, limit and cursor. Unknown query parameters are rejected with a 400.
| Name | In | Type | Description |
|---|---|---|---|
| type | query | string | catalog (default) or verification |
| since | query | string | Only entries after this ISO 8601 time (exclusive) |
| until | query | string | Only entries at or before this time (catalog only) |
| productId | query | string | Only changes to this product (catalog only) |
| category | query | string | Filter by category (driver, iron, driver_shaft, iron_shaft, ball, grip, wedge, putter, fairway_wood, hybrid) |
| changeType | query | string | added, updated or removed (catalog only) |
| limit | query | integer | Entries per page (1–100, default 25) |
| cursor | query | string | Pagination cursor from previous response |
{
"data": {
"entries": [
{
"id": 4292,
"productId": "putter:odyssey:ai-dual-1:2026",
"category": "putter",
"changeType": "updated",
"fieldName": "hosel",
"oldValue": "plumber-neck",
"newValue": "crank-neck",
"changedAt": "2026-10-09T16:07:10.000Z",
"source": "backfill",
"prNumber": 72
},
{
"id": 4291,
"productId": "putter:odyssey:dfx-2-ball-blade-ch:2025",
"category": "putter",
"changeType": "updated",
"fieldName": "hosel",
"oldValue": "plumber-neck",
"newValue": "crank-neck",
"changedAt": "2026-10-09T16:07:10.000Z",
"source": "backfill",
"prNumber": 72
},
{
"id": 4290,
"productId": "putter:odyssey:dfx-1w-ch:2025",
"category": "putter",
"changeType": "updated",
"fieldName": "hosel",
"oldValue": "plumber-neck",
"newValue": "crank-neck",
"changedAt": "2026-10-09T16:07:10.000Z",
"source": "backfill",
"prNumber": 72
}
],
"pagination": {
"limit": 3,
"cursor": null,
"nextCursor": "eyJvZmZzZXQiOjN9",
"total": 4292
}
},
"meta": {
"version": "v1.1",
"coverageTarget": {
"fromYear": 2016,
"throughYear": 2026
},
"requestId": "eq_sample"
}
}const res = await fetch("https://www.fitmyswing.com/api/v1/changelog?limit=3", {
headers: { "x-api-key": process.env.FITMYSWING_API_KEY! },
})
const { data } = await res.json()
for (const c of data.entries) console.log(c.changeType, c.productId, c.fieldName, c.oldValue, c.newValue)curl "https://www.fitmyswing.com/api/v1/changelog?limit=3" \ -H "x-api-key: fms_test_..."
import requests
r = requests.get("https://www.fitmyswing.com/api/v1/changelog?limit=3",
headers={"x-api-key": "fms_test_..."})
entries = r.json()["data"]["entries"]