Get early access

DEVELOPER DOCUMENTATION  /  V1

API reference.

Search courses, retrieve complete tee scorecards and work with the response in your application.

Quickstart

Set the API origin and key for your environment. Replace the placeholders below; use the origin without a trailing slash or /v1.

environment · shell
export YARDAGE_API_URL="https://your-api-host.example"
export YARDAGE_API_KEY="your-api-key"

Search for a course by name. Each example prints the references and names returned by the API.

import json
import os
from urllib.parse import urlencode
from urllib.request import Request, urlopen

base_url = os.environ["YARDAGE_API_URL"].rstrip("/")
api_key = os.environ["YARDAGE_API_KEY"]

def get_json(path, params=None):
    query = "?" + urlencode(params) if params else ""
    request = Request(
        base_url + path + query,
        headers={"Authorization": f"Bearer {api_key}"},
    )
    with urlopen(request, timeout=15) as response:
        return json.load(response)

page = get_json("/v1/courses", {"q": "Verulam", "limit": 5})
for course in page["courses"]:
    print(course["course_ref"], course["display_name"])
if not page["courses"]:
    print("No matching courses.")

Python uses the standard library. JavaScript uses Node.js fetch; save it as example.mjs and run node example.mjs. Shell examples use curl 7.76+.

Run authenticated requests on your server. Keep the API key out of browser code and source control.

Authentication

Send exactly one Authorization: Bearer <key> header on every customer request. Credentials in query parameters are not supported. GET /healthz does not require a key.

The Python helper raises urllib.error.HTTPError for HTTP failures; its code and headers expose the status and retry information. The JavaScript helper throws an error with status and retryAfter.

Endpoints

Customer routes accept GET and HEAD, with no request body. HEAD returns the same status and representation headers as GET, without a response body.

List and search courses

GET / HEAD/v1/courses
ParameterDefaultRules
limit50Integer from 1 to 100. No leading zeros.
qOmittedCase-insensitive, literal substring search on the course name. Trimmed and Unicode-normalised; 1 to 100 characters.
cursorOmittedUse the previous response's next_cursor unchanged to request the next page.

Search matches course names, including partial names. It does not search towns or postcodes. Percent signs and underscores are literal characters, not wildcards. Unknown parameters, duplicates and invalid values return 400.

FieldTypeMeaning
coursesarrayCourse summaries in name and course-reference order. An empty array means no matches.
next_cursorstring | nullCursor for the next page. null marks the end.
dataset_versionstringDataset used to produce this response.

Get course details

GET / HEAD/v1/courses/{course_ref}

Use a course_ref from the list response. This route accepts no query parameters. It returns the course and its published cards together; there is no separate tee endpoint.

# Continue after the Python quickstart.
if page["courses"]:
    course_ref = page["courses"][0]["course_ref"]
    detail = get_json(f"/v1/courses/{course_ref}")
    print(detail["display_name"])
    for card in detail["cards"]:
        print(card["tee"], card["total"], card["par_total"])
        for hole in card["holes"]:
            print(hole["number"], hole["yards"],
                  hole["par"], hole["stroke_index"])

Python and JavaScript continue from their quickstart examples. For curl, set COURSE_REF to a reference returned by the list.

Pagination

Pass next_cursor as cursor until it is null. Keep the same search filter for every page. Treat the cursor as an opaque string.

# Reuse get_json from the Python quickstart.
params = {"q": "Verulam", "limit": 25}
while True:
    result = get_json("/v1/courses", params)
    for course in result["courses"]:
        print(course["course_ref"], course["display_name"])
    cursor = result["next_cursor"]
    if cursor is None:
        break
    params["cursor"] = cursor

The loop examples reuse the client helpers from quickstart. The curl example requests one subsequent page.

Cursors carry an expiry and become invalid when the dataset or publication state changes. If a cursor returns 400, check the query and restart from the first page without it. Reconcile records by course_ref when restarting.

Response fields

Responses use JSON. A detail response contains the four summary fields below plus the additional detail fields.

Course summary

FieldTypeMeaning
course_refstringOpaque course identifier: ref_ followed by 32 lowercase hexadecimal characters.
display_namestringThe course name.
stalebooleanAlways false in v1.
stale_sincestring | nullAlways null in v1.

Course detail

FieldTypeMeaning
cardsarrayPublished tee cards. Each card contains every hole in its layout.
source_urlstringThe club's own source URL: HTTPS, unless fetched_over_http is true, in which case it is the club's plain-http origin and path.
fetched_over_httpbooleanTrue when the site answers only over http and the card was fetched over that weaker transport, which the flag discloses.
retrieved_atstringUTC timestamp of source retrieval.
verified_atstringUTC timestamp of verification.
dataset_versionstringDataset used for this response.
completenessstringAlways "full" in the current contract.

Tee card: cards[]

FieldTypeMeaning
teestringPublished tee label. Select from cards in your application.
unitstringAlways "yards".
holesarray9 or 18 holes, ordered by hole number.
out, in, totalintegerFront-nine, back-nine and full-card yardage totals.
par_out, par_in, par_totalintegerThe corresponding par totals.

For a nine-hole card, in and par_in are 0. The front-nine totals equal the full-card totals.

Hole: cards[].holes[]

FieldTypeMeaning
numberintegerHole number, sequentially from 1 to 9 or 18.
yardsintegerLength from this tee, in yards.
parintegerPar for this hole.
stroke_indexinteger | nullStroke index for this hole and tee, or null on every hole when the card has none. A card never mixes present and null values.

yards and par are non-null. stroke_index is present on every hole of a card or null on every hole. Hole numbering starts at 1 for each card. Choose a tee using the labels returned in cards[].

View a complete card example: Verulam, White tees
card object · 18 holes
{
  "tee": "White",
  "unit": "yards",
  "holes": [
    { "number": 1, "yards": 361, "par": 4, "stroke_index": 11 },
    { "number": 2, "yards": 343, "par": 4, "stroke_index": 7 },
    { "number": 3, "yards": 311, "par": 4, "stroke_index": 17 },
    { "number": 4, "yards": 413, "par": 4, "stroke_index": 3 },
    { "number": 5, "yards": 375, "par": 4, "stroke_index": 5 },
    { "number": 6, "yards": 322, "par": 4, "stroke_index": 15 },
    { "number": 7, "yards": 433, "par": 4, "stroke_index": 1 },
    { "number": 8, "yards": 180, "par": 3, "stroke_index": 9 },
    { "number": 9, "yards": 542, "par": 5, "stroke_index": 13 },
    { "number": 10, "yards": 194, "par": 3, "stroke_index": 8 },
    { "number": 11, "yards": 534, "par": 5, "stroke_index": 14 },
    { "number": 12, "yards": 338, "par": 4, "stroke_index": 12 },
    { "number": 13, "yards": 372, "par": 4, "stroke_index": 10 },
    { "number": 14, "yards": 348, "par": 4, "stroke_index": 6 },
    { "number": 15, "yards": 369, "par": 4, "stroke_index": 2 },
    { "number": 16, "yards": 293, "par": 4, "stroke_index": 18 },
    { "number": 17, "yards": 127, "par": 3, "stroke_index": 16 },
    { "number": 18, "yards": 574, "par": 5, "stroke_index": 4 }
  ],
  "out": 3280,
  "in": 3149,
  "total": 6429,
  "par_out": 36,
  "par_in": 36,
  "par_total": 72
}

A recorded card, shown as one object from cards[]. This is not a live response.

Location search and per-tee course and slope ratings are planned for launch. They are not fields or parameters in this API contract.

Limits and errors

Rate limits are assigned per key. GET and HEAD each consume one quota unit. A 429 includes Retry-After as an integer number of seconds from 1 to 86,400. Wait that duration before retrying and cap the number of retries in your client.

StatusJSON bodyHandling
400{"error":"bad_request"}Check parameter names, values, duplicate keys and course-reference format. An invalid or expired cursor also returns 400.
401{"error":"unauthorized"}Check the bearer header and whether the key is valid, active and unexpired.
403{"error":"forbidden"}Check that the key has the required access scope.
404{"error":"not_found"}The course is unavailable. Unknown, unpublished, incomplete and suppressed references share this response.
405{"error":"method_not_allowed"}Use GET or HEAD on customer routes. The Allow header lists both.
429{"error":"rate_limited"}Wait for the Retry-After duration before retrying.
503{"error":"unavailable"}The service cannot fulfil the request. Retry with bounded backoff.

Customer responses use Cache-Control: private,no-store,max-age=0 and Vary: Authorization. They do not expose ETag or Last-Modified validators. Headers are bounded to 16 KiB and URLs to 4 KiB.

Health check

GET/healthz

This unauthenticated route returns {"status":"ok"} when available or {"error":"unavailable"} when unavailable. It exposes no course data. HEAD is not supported on this route.

curl · health check
curl --fail-with-body --silent --show-error --max-time 15 \
  "$YARDAGE_API_URL/healthz"

Service and data use

The service is provided on a best-effort basis, without a service-level agreement. Data is licensed for your own use and may not be redistributed, resold or re-served.

Join the waitlist