{
  "openapi": "3.1.0",
  "info": {
    "title": "GolfCore Course Data API",
    "version": "1.3.0",
    "summary": "Read-only public data for golf courses in the GolfCore library.",
    "description": "Identity, location, coverage and scorecards for every golf course GolfCore holds. Attribution is required: cite www.golfcore.org and link the course page. This API is the supported way for an AI agent or an application to answer questions about a golf course; do not scrape the HTML pages.\n\nAPI keys: from December 4, 2026, /courses and /courses/{slug} require an API key sent as `Authorization: Bearer gc_live_...`. Until then they answer with or without one, and keyless responses carry Deprecation and Sunset headers. Keys come from https://platform.golfcore.app, where the account holder signs in with their GolfCore account (Google, Apple, Facebook or an email link) and creates one; a Free plan is set up automatically. Plans: Free, 500 requests a month at 10 a minute, hard stop; Basic, $10 a month for 10,000 requests at 60 a minute, then $2.00 per 1,000 up to the account's monthly budget; Business, $50 a month for 100,000 at 300 a minute, then $1.00 per 1,000 up to the budget. Paying for a plan happens in Stripe Checkout from the portal, completed by the account holder; an agent can direct the person there but cannot pay on their behalf. GET /usage with a key reports the month so far. The MCP server at https://mcp.golfcore.org/mcp stays keyless.",
    "contact": {
      "name": "GolfCore support",
      "email": "support@golfcore.org",
      "url": "https://www.golfcore.org/contact/"
    },
    "license": {
      "name": "GolfCore API terms: attribution required; no resale, redistribution or competing APIs",
      "url": "https://www.golfcore.org/terms/"
    }
  },
  "externalDocs": {
    "description": "API keys, plans and the December 4, 2026 cutoff",
    "url": "https://www.golfcore.org/developers/keys/"
  },
  "x-service-info": {
    "categories": ["sports", "maps", "data"],
    "docs": { "homepage": "https://www.golfcore.org/", "apiReference": "https://www.golfcore.org/developers/docs/", "llms": "/llms.txt" }
  },
  "security": [{}, { "apiKey": [] }],
  "servers": [
    {
      "url": "https://api.golfcore.org/v1",
      "description": "Production, served from the Cloudflare edge"
    }
  ],
  "paths": {
    "/courses": {
      "get": {
        "operationId": "searchCourses",
        "summary": "Search and filter the course library",
        "description": "Free-text search over course names and towns, with filters for country, region, city, proximity and GolfCore coverage. Returns a page of summaries; call /courses/{slug} for the scorecard.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Free-text match against course name and city. Case, accent and punctuation insensitive.",
            "schema": { "type": "string" },
            "example": "pebble beach"
          },
          {
            "name": "country",
            "in": "query",
            "description": "ISO 3166-1 alpha-2 country code, lowercase.",
            "schema": { "type": "string", "pattern": "^[a-z]{2}$" },
            "example": "us"
          },
          {
            "name": "region",
            "in": "query",
            "description": "State, county or province name as GolfCore records it.",
            "schema": { "type": "string" },
            "example": "California"
          },
          {
            "name": "city",
            "in": "query",
            "schema": { "type": "string" },
            "example": "Monterey"
          },
          {
            "name": "near",
            "in": "query",
            "description": "Latitude,longitude pair. Returns courses within `radius` miles, nearest first, each carrying a `miles` field.",
            "schema": { "type": "string", "pattern": "^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$" },
            "example": "36.5686,-121.9497"
          },
          {
            "name": "radius",
            "in": "query",
            "description": "Miles, used only with `near`.",
            "schema": { "type": "number", "default": 25, "maximum": 250 }
          },
          {
            "name": "coverage",
            "in": "query",
            "description": "Restrict to courses carrying a level of GolfCore mapping. `contours` means lidar green contour maps, `traced` means a drawn course map, `scorecard` means a hole-by-hole card, `rated` means a USGA rating and slope.",
            "schema": {
              "type": "string",
              "enum": ["contours", "traced", "scorecard", "rated"]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 25, "minimum": 1, "maximum": 200 }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": { "type": "integer", "default": 0, "minimum": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of course summaries.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "Deprecation": { "$ref": "#/components/headers/Deprecation" },
              "Sunset": { "$ref": "#/components/headers/Sunset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CourseList" }
              }
            }
          },
          "400": {
            "description": "A parameter was malformed.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "401": { "$ref": "#/components/responses/InvalidKey" },
          "429": { "$ref": "#/components/responses/Limited" }
        }
      }
    },
    "/courses/{slug}": {
      "get": {
        "operationId": "getCourse",
        "summary": "One course, with its scorecard",
        "description": "Full detail for a single course: identity, location, contact, GolfCore coverage, and layouts with a reference hole card plus each tee's ratings, slope and available per-hole par, stroke index and yardage. Read layouts[].tees[].holes for a specific tee and gender; no extra parameter or authentication is needed.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "GolfCore course slug, as returned by /courses and used in the public course page URL. Not derivable from the course name.",
            "schema": { "type": "string", "pattern": "^[a-z0-9-]{1,120}$" },
            "example": "pebble-beach"
          }
        ],
        "responses": {
          "200": {
            "description": "The course.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Course" } }
            }
          },
          "404": {
            "description": "No course carries that slug.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "401": { "$ref": "#/components/responses/InvalidKey" },
          "429": { "$ref": "#/components/responses/Limited" }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "This month's usage for the calling API key's account",
        "description": "Requires an API key. Reports the plan, limits and requests counted so far in the current UTC month across every key of the account in the key's environment. Not metered: checking usage never counts against the allowance. Counts can trail live traffic by a few seconds.",
        "security": [{ "apiKey": [] }],
        "responses": {
          "200": {
            "description": "Usage for the current UTC month.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Usage" } } }
          },
          "401": { "$ref": "#/components/responses/InvalidKey" }
        }
      }
    },
    "/day-passes": {
      "post": {
        "operationId": "buyDayPass",
        "summary": "Buy a GolfCore Premium day pass",
        "description": "Buys GolfCore Premium day passes for the signed-in golfer through the Machine Payments Protocol (https://mpp.dev) with a Stripe Shared Payment Token. Each pass waits in the golfer's account until they start it in the GolfCore app, on any date; the 24 hours begin then. Send the golfer's GolfCore OAuth token (scope caddie, from https://www.golfcore.app/.well-known/oauth-authorization-server) as `Authorization: Bearer`. A call without payment returns 402 with the challenge; repeat it with the credential in `Payment-Authorization`, the header the challenge names.",
        "security": [{ "golfcoreOAuth": ["caddie"] }],
        "x-payment-info": { "amount": "300", "currency": "usd", "description": "One GolfCore Premium day pass, redeemable on any date", "intent": "charge", "method": "stripe" },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "quantity": { "type": "integer", "minimum": 1, "default": 1 } } }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Paid. The passes are on the golfer's account; the Payment-Receipt header carries the receipt. Replaying the same credential returns the same result and issues nothing new.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DayPasses" } } }
          },
          "401": {
            "description": "No valid GolfCore OAuth bearer token."
          },
          "402": {
            "description": "Payment Required. WWW-Authenticate: Payment carries the challenge: amount, currency, Stripe method details, expiry, and header=\"Payment-Authorization\".",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentProblem" } } }
          }
        }
      }
    },
    "/status": {
      "get": {
        "operationId": "getStatus",
        "summary": "Service status",
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Status" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "golfcoreOAuth": {
        "type": "oauth2",
        "description": "The golfer's GolfCore account, through the app's OAuth 2.1 server with PKCE and dynamic client registration.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://www.golfcore.app/oauth/authorize",
            "tokenUrl": "https://www.golfcore.app/oauth/token",
            "refreshUrl": "https://www.golfcore.app/oauth/token",
            "scopes": { "caddie": "Ask the AI caddie and buy day passes for this golfer" }
          }
        }
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "gc_live_<24 hex>.<43 base64url>",
        "description": "Create keys at https://platform.golfcore.app. Keep them on a server; never put one in a mobile app or browser code."
      }
    },
    "headers": {
      "RateLimitLimit": { "description": "The month's request ceiling for the account: the plan allowance, or the monthly budget on an overage plan.", "schema": { "type": "integer" } },
      "RateLimitRemaining": { "description": "Requests left this month.", "schema": { "type": "integer" } },
      "RateLimitReset": { "description": "Unix seconds when the monthly count resets (the first of next month, UTC).", "schema": { "type": "integer" } },
      "Deprecation": { "description": "On keyless responses only (RFC 9745): keyless access is deprecated since this date.", "schema": { "type": "string", "example": "@1791158400" } },
      "Sunset": { "description": "On keyless responses only (RFC 8594): keyless access ends at this time.", "schema": { "type": "string", "example": "Fri, 04 Dec 2026 00:00:00 GMT" } }
    },
    "responses": {
      "InvalidKey": {
        "description": "`invalid_api_key`: the key is unknown, revoked, expired or for the other environment. `api_key_required` on /usage when no key was sent.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Limited": {
        "description": "`rate_limited`: over the per-minute limit. `quota_exceeded`: the month's allowance or budget is used; upgrade or raise the budget at https://platform.golfcore.app, or wait for the reset.",
        "headers": {
          "Retry-After": { "description": "Seconds to wait.", "schema": { "type": "integer" } },
          "X-RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "DayPasses": {
        "type": "object",
        "properties": {
          "day_passes": { "type": "integer", "description": "Passes issued by this call." },
          "unredeemed": { "type": "integer", "description": "The golfer's passes not yet started, including these." },
          "redeem": { "type": "string", "description": "How the golfer starts a pass in the GolfCore app." },
          "settings_url": { "type": "string", "format": "uri" }
        }
      },
      "PaymentProblem": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "format": "uri" },
          "title": { "type": "string" },
          "status": { "type": "integer", "const": 402 },
          "detail": { "type": "string" },
          "amount": { "type": "integer", "description": "Total in cents." },
          "currency": { "type": "string" },
          "quantity": { "type": "integer" }
        }
      },
      "Usage": {
        "type": "object",
        "required": ["plan", "environment", "period", "consumed", "remaining", "reset"],
        "properties": {
          "plan": { "type": "string", "enum": ["free", "basic", "business"] },
          "environment": { "type": "string", "enum": ["test", "live"] },
          "key_prefix": { "type": "string", "description": "The calling key's public prefix." },
          "period": { "type": "string", "description": "UTC month, YYYY-MM.", "example": "2026-10" },
          "rpm": { "type": "integer", "description": "Requests per minute allowed for each key." },
          "included": { "type": "integer", "description": "Requests included in the plan each month." },
          "hard_limit": { "type": "integer", "description": "Requests after which the account is refused this month: the allowance on Free, the monthly budget on Basic and Business." },
          "overage": { "type": "boolean", "description": "Whether requests past the included amount are billed." },
          "consumed": { "type": "integer" },
          "remaining": { "type": "integer" },
          "overage_units": { "type": "integer", "description": "Requests past the included amount this month, billed at the plan's overage rate." },
          "reset": { "type": "integer", "description": "Unix seconds of the next monthly reset." }
        }
      },
      "CourseList": {
        "type": "object",
        "required": ["total", "count", "offset", "courses"],
        "properties": {
          "total": { "type": "integer", "description": "Courses matching the filters, before paging." },
          "count": { "type": "integer", "description": "Courses in this page." },
          "offset": { "type": "integer" },
          "courses": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CourseSummary" }
          }
        }
      },
      "CourseSummary": {
        "type": "object",
        "required": ["slug", "name", "url", "coverage"],
        "properties": {
          "slug": { "type": "string", "example": "pebble-beach" },
          "name": { "type": "string", "example": "Pebble Beach Golf Links" },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The public GolfCore course page. Cite this when answering a question about the course.",
            "example": "https://www.golfcore.org/courses/pebble-beach/"
          },
          "app_url": {
            "type": "string",
            "format": "uri",
            "description": "Opens this course's map in the GolfCore app, in any browser. Give this to a person who wants to see the course; someone without an account starts as a guest.",
            "example": "https://www.golfcore.app/courses/pebble-beach?ref=golfcore.org"
          },
          "city": { "type": ["string", "null"] },
          "region": { "type": ["string", "null"], "description": "State, county or province." },
          "country": { "type": ["string", "null"], "description": "ISO 3166-1 alpha-2, lowercase." },
          "lat": { "type": ["number", "null"] },
          "lng": { "type": ["number", "null"] },
          "hole_count": { "type": ["integer", "null"] },
          "par_total": { "type": ["integer", "null"] },
          "miles": {
            "type": "number",
            "description": "Distance from the `near` point. Present only when `near` was supplied."
          },
          "coverage": { "$ref": "#/components/schemas/Coverage" }
        }
      },
      "Course": {
        "allOf": [
          { "$ref": "#/components/schemas/CourseSummary" },
          {
            "type": "object",
            "properties": {
              "address": { "type": ["string", "null"] },
              "phone": { "type": ["string", "null"] },
              "website": { "type": ["string", "null"], "format": "uri" },
              "timezone": { "type": ["string", "null"], "example": "America/Los_Angeles" },
              "layouts": {
                "type": "array",
                "description": "Every rated routing at this course. A club playing three nines in combination returns one layout per rated combination.",
                "items": { "$ref": "#/components/schemas/Layout" }
              }
            }
          }
        ]
      },
      "Coverage": {
        "type": "object",
        "description": "What GolfCore holds for this course. `contours` is the deepest level: lidar-derived green elevation bands and fall-line arrows.",
        "required": ["contours", "traced", "scorecard", "rated", "wind"],
        "properties": {
          "contours": { "type": "boolean", "description": "Lidar green contour maps on every putting surface." },
          "traced": { "type": "boolean", "description": "A drawn course map: fairways, tees, greens, bunkers, water." },
          "scorecard": { "type": "boolean", "description": "Hole-by-hole par and yardage." },
          "rated": { "type": "boolean", "description": "USGA course rating and slope on at least one tee." },
          "wind": { "type": "boolean", "description": "Live wind simulated over the hole." }
        }
      },
      "Layout": {
        "type": "object",
        "properties": {
          "name": { "type": ["string", "null"], "description": "Routing name, null when the club has one course." },
          "par": { "type": ["integer", "null"], "description": "Null when any hole is missing a par." },
          "yards": { "type": ["integer", "null"], "description": "Null when any hole is missing a yardage." },
          "holes": {
            "type": "array",
            "description": "Reference hole card for this layout. These yardages are not specific to a selected tee. Use tees[].holes for tee-specific figures.",
            "items": { "$ref": "#/components/schemas/Hole" }
          },
          "tees": {
            "type": "array",
            "description": "Empty when the course is carded but not rated.",
            "items": { "$ref": "#/components/schemas/Tee" }
          }
        }
      },
      "Hole": {
        "type": "object",
        "required": ["position"],
        "properties": {
          "position": { "type": "integer", "minimum": 1, "example": 1 },
          "par": { "type": ["integer", "null"], "example": 4 },
          "stroke_index": {
            "type": ["integer", "null"],
            "description": "Handicap stroke index, 1 being the hardest hole.",
            "example": 8
          },
          "yards": { "type": ["integer", "null"], "example": 380 }
        }
      },
      "Tee": {
        "type": "object",
        "required": ["name", "holes"],
        "properties": {
          "name": { "type": "string", "example": "Championship" },
          "gender": {
            "type": ["string", "null"],
            "enum": ["M", "F", null],
            "description": "Ratings are per tee and per gender; one tee name can appear twice."
          },
          "yards": { "type": ["integer", "null"], "description": "Recorded total yardage for this tee, independent of available per-hole coverage." },
          "par": { "type": ["integer", "null"] },
          "rating": { "type": ["number", "null"], "description": "USGA course rating.", "example": 75.5 },
          "slope": { "type": ["integer", "null"], "description": "USGA slope rating.", "example": 145 },
          "holes": {
            "type": "array",
            "description": "Recorded hole data for this tee and gender, ordered by position. Empty when no tee-specific data is held. Partial cards may omit positions or contain null fields; never substitute the layout's reference yardages or divide the tee total to fill gaps. Hole yardages are in yards. Par and stroke index are the recorded tee-specific values, without layout fallback.",
            "items": { "$ref": "#/components/schemas/Hole" }
          }
        }
      },
      "Status": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["ok", "degraded"] },
          "courses": { "type": "integer", "description": "Courses currently served." },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "detail": { "type": "string" }
        }
      }
    }
  }
}
