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.
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.")const baseUrl = process.env.YARDAGE_API_URL;
const apiKey = process.env.YARDAGE_API_KEY;
if (!baseUrl || !apiKey) throw new Error("Set the API URL and key.");
async function getJSON(path, params = {}) {
const url = new URL(path, baseUrl);
url.search = new URLSearchParams(params).toString();
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(15000),
});
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${await response.text()}`);
error.status = response.status;
error.retryAfter = response.headers.get("Retry-After");
throw error;
}
return response.json();
}
const page = await getJSON("/v1/courses", { q: "Verulam", limit: 5 });
for (const course of page.courses) {
console.log(course.course_ref, course.display_name);
}
if (!page.courses.length) console.log("No matching courses.");curl --fail-with-body --silent --show-error --max-time 15 --get \
"$YARDAGE_API_URL/v1/courses" \
--header "Authorization: Bearer $YARDAGE_API_KEY" \
--data-urlencode "q=Verulam" \
--data-urlencode "limit=5"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
/v1/courses| Parameter | Default | Rules |
|---|---|---|
limit | 50 | Integer from 1 to 100. No leading zeros. |
q | Omitted | Case-insensitive, literal substring search on the course name. Trimmed and Unicode-normalised; 1 to 100 characters. |
cursor | Omitted | Use 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.
| Field | Type | Meaning |
|---|---|---|
courses | array | Course summaries in name and course-reference order. An empty array means no matches. |
next_cursor | string | null | Cursor for the next page. null marks the end. |
dataset_version | string | Dataset used to produce this response. |
Get course details
/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"])// Continue after the JavaScript quickstart.
if (page.courses.length) {
const courseRef = page.courses[0].course_ref;
const detail = await getJSON(`/v1/courses/${courseRef}`);
console.log(detail.display_name);
for (const card of detail.cards) {
console.log(card.tee, card.total, card.par_total);
for (const hole of card.holes) {
console.log(hole.number, hole.yards, hole.par, hole.stroke_index);
}
}
}# Set COURSE_REF to a course_ref returned by the list request.
curl --fail-with-body --silent --show-error --max-time 15 \
"$YARDAGE_API_URL/v1/courses/${COURSE_REF:?Set COURSE_REF first}" \
--header "Authorization: Bearer $YARDAGE_API_KEY"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// Reuse getJSON from the JavaScript quickstart.
const params = { q: "Verulam", limit: 25 };
while (true) {
const result = await getJSON("/v1/courses", params);
for (const course of result.courses) {
console.log(course.course_ref, course.display_name);
}
if (result.next_cursor === null) break;
params.cursor = result.next_cursor;
}# Set NEXT_CURSOR to the previous response's next_cursor value.
# Keep the same q filter. Stop when next_cursor is null.
curl --fail-with-body --silent --show-error --max-time 15 --get \
"$YARDAGE_API_URL/v1/courses" \
--header "Authorization: Bearer $YARDAGE_API_KEY" \
--data-urlencode "q=Verulam" \
--data-urlencode "limit=25" \
--data-urlencode "cursor=${NEXT_CURSOR:?Set NEXT_CURSOR first}"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
| Field | Type | Meaning |
|---|---|---|
course_ref | string | Opaque course identifier: ref_ followed by 32 lowercase hexadecimal characters. |
display_name | string | The course name. |
stale | boolean | Always false in v1. |
stale_since | string | null | Always null in v1. |
Course detail
| Field | Type | Meaning |
|---|---|---|
cards | array | Published tee cards. Each card contains every hole in its layout. |
source_url | string | The 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_http | boolean | True when the site answers only over http and the card was fetched over that weaker transport, which the flag discloses. |
retrieved_at | string | UTC timestamp of source retrieval. |
verified_at | string | UTC timestamp of verification. |
dataset_version | string | Dataset used for this response. |
completeness | string | Always "full" in the current contract. |
Tee card: cards[]
| Field | Type | Meaning |
|---|---|---|
tee | string | Published tee label. Select from cards in your application. |
unit | string | Always "yards". |
holes | array | 9 or 18 holes, ordered by hole number. |
out, in, total | integer | Front-nine, back-nine and full-card yardage totals. |
par_out, par_in, par_total | integer | The 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[]
| Field | Type | Meaning |
|---|---|---|
number | integer | Hole number, sequentially from 1 to 9 or 18. |
yards | integer | Length from this tee, in yards. |
par | integer | Par for this hole. |
stroke_index | integer | null | Stroke 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
{
"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.
| Status | JSON body | Handling |
|---|---|---|
| 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
/healthzThis 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 --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