OpenPinsheet

The format

Version 1, a draft. One row per hole and day, the same columns in CSV and GeoJSON. Developers start at Use the files.

A record

On this date, on this hole, the cup was here.

That is all a record says. The point locates itself: whoever wants to know which course or which green it belongs to asks the coordinate, not an ID. The hole number is the one the club uses, and it describes the record without being a key.

Example, not real data
date,date_basis,hole,lat,lon,source,accuracy_m,front_m,left_m,input_front,input_offset,input_side,input_unit,channel,missing
2027-04-12,local,7,52.1234561,13.1234573,estimated,,12,9.5,12,5,right,m,openyardage,
2027-04-12,local,8,52.1241077,13.122915,mapped,,,,,,,,openyardage,
2027-04-12,local,9,,,,,,,,,,,,1

The last row is a hole where a pin was set that day but its position has been lost. It stays in the file, because a cup did stand there.

Sources

Positions say how they were obtained; entries made before a tool stored coordinates carry no source. An estimate is useful, but it is not a measurement; the column source keeps the two apart.

front edge 12 m 5 m example
estimated

Metres or yards from the front edge and from the left or right edge, as a greenkeeper reads them off. Converted to a coordinate against the green outline in OpenStreetMap on that day. What was typed stays in the record, so anyone can redo the conversion.

How well it matches the ground depends on how well the outline matches the green. That has not been measured, so an estimate carries no accuracy_m.

surveyed

GNSS with RTK or similar. accuracy_m is required: the horizontal accuracy the device reports.

mapped

Placed on a map or an aerial image. accuracy_m is optional.

detectedno partner delivers this yet

A camera or LiDAR finds the cup. accuracy_m is required.

phone GPSnot accepted

Two metres at best, which on a green is the difference between two pin positions.

Columns

One row per hole and day. The same names are used in the CSV and in the properties of the GeoJSON. Empty means unknown, never zero.

ColumnMeaning
When and where
dateCalendar date at the course, YYYY-MM-DD.
date_basislocal, or utc where the local date was not recorded. In New Zealand the two can be a day apart.
holeThe hole as the club names it: 7, later perhaps B3.
lat, lonWGS84 in degrees, up to seven decimals.
How it was obtained
sourceestimated, surveyed, mapped or detected. Empty for older entries made before a tool stored coordinates; then only front_m and left_m are given.
accuracy_mHorizontal accuracy in metres, where one was measured.
front_m, left_mMetres from the front and the left edge, seen from the line of play, read against the OpenStreetMap green of that day.
input_front, input_offsetWhat was typed: distance from the front and from the side edge, in the unit typed.
input_sideleft or right: which edge the offset is measured from.
input_unitm or yd.
Who vouches for it
channelThe channel the record came through, called a data partner on the home page. See Data partners.
missing1 if a pin was set that day but its position is lost. Everything from lat to channel is then empty.

Not in the data

  • No IDs for courses, holes, greens or people, and no OpenStreetMap IDs. They change; a coordinate does not.
  • No names and no e-mail addresses.
  • No time of day. At a small club, the time the pins were set is one person's working hours.
  • No pin sets, rotations or plans. Those belong to the tool a club works with.

Files

A day is published three days later, once no course anywhere can still enter pins for it. After that the file does not change. Corrections and withdrawals get files of their own, so what was published on a day can always be checked.

Layout
v1/index.json                 list of files, sizes, sha256, format version
v1/days/2027-04-12.csv
v1/days/2027-04-12.geojson
v1/years/2027.csv             all days of a year

Use the files

No account and no key. Everything lives under one address, and index.json lists every file with its size, row count and sha256 checksum.

Addresses
https://data.openpinsheet.com/v1/index.json
https://data.openpinsheet.com/v1/days/YYYY-MM-DD.csv
https://data.openpinsheet.com/v1/days/YYYY-MM-DD.geojson
https://data.openpinsheet.com/v1/years/YYYY.csv

Until the first day is published, index.json does not exist and the address answers 404.

index.json

Example
{
 "format": 1,
 "generated": "2027-04-15",
 "license": { "name": "Open Database License 1.0", "spdx": "ODbL-1.0", "url": "..." },
 "files": [
  { "path": "days/2027-04-12.csv", "kind": "day", "date": "2027-04-12",
    "bytes": 1234, "sha256": "...", "rows": 18 },
  { "path": "years/2027.csv", "kind": "year", "year": "2027", ... }
 ]
}

Caching and browsers

  • Day files never change. They are served with a one-year cache, so download each one once.
  • index.json and the year files change when a day is added and are cached for five minutes. Checking index.json once a day is enough: a new day appears at most once a day.
  • Any website may fetch the files from a browser; cross-origin requests are allowed.
  • Compare the sha256 in index.json with what you downloaded if you keep copies.

Python

Latest day, CSV
import csv, io, json, urllib.request

BASE = "https://data.openpinsheet.com/v1/"
index = json.load(urllib.request.urlopen(BASE + "index.json"))
days = [f for f in index["files"] if f["kind"] == "day" and f["path"].endswith(".csv")]
latest = max(days, key=lambda f: f["date"])

text = urllib.request.urlopen(BASE + latest["path"]).read().decode("utf-8-sig")
lines = (line for line in io.StringIO(text) if not line.startswith("#"))
for row in csv.DictReader(lines):
    print(row["date"], row["hole"], row["lat"], row["lon"], row["source"])

JavaScript

Latest day, GeoJSON
const BASE = "https://data.openpinsheet.com/v1/";
const index = await (await fetch(BASE + "index.json")).json();
const days = index.files.filter(f => f.kind === "day" && f.path.endsWith(".geojson"));
const latest = days.reduce((a, b) => (a.date > b.date ? a : b));

const day = await (await fetch(BASE + latest.path)).json();
for (const f of day.features) {
  console.log(f.properties.hole, f.properties.source, f.geometry?.coordinates);
}

Delivering pins

Pins come in through data partners, not through these files. A golf app or course tool that wants to deliver pins writes to info@openyardage.com.

Licence

The data is under the Open Database License 1.0, the licence of OpenStreetMap data. Anyone may use, copy and share it, including commercially. Whoever builds a database from it names the source and shares that database under the same licence. The licence cannot be withdrawn.

Attribution

Pin positions: OpenPinsheet and its channels, Open Database License 1.0.

Estimated positions are computed against green outlines from OpenStreetMap: map data © OpenStreetMap contributors (ODbL).

The second line is not a courtesy: a position computed against an OpenStreetMap outline is derived from OpenStreetMap, and the ODbL asks for that credit.