Event data API
Plan with the real course, follow the real fleet, finish with the official result and the evidence behind it.
What this is
A read-only feed of official event data: the event and its classes and races, the entry list,
and the results. Every document is available as JSON, and entries and results are also
available as CSV. There is no key, no account, and no rate-limit form to fill in — a GET is the whole protocol.
It exists so that onboard navigation software, analytics tools, and club websites can read
what the organiser published rather than scrape a page or retype a screenshot. The format is
vendor-neutral and self-describing: every JSON body carries "schema": "event-data/v0" and a generated_at stamp, which is the instant
the document was rendered, not the instant the data changed.
Timestamps are UTC ISO-8601. start_date and end_date are calendar
dates, YYYY-MM-DD, with no time and no offset. Responses carry cache-control: public, max-age=60, s-maxage=120 — short on purpose, because results
move while a regatta is being scored.
Endpoints
| Method | Path | Media type |
|---|---|---|
| GET | /api/event-data/v0/events/{id} | application/json |
| GET | /api/event-data/v0/events/{id}/entries | application/json |
| GET | /api/event-data/v0/events/{id}/entries.csv | text/csv; charset=utf-8 |
| GET | /api/event-data/v0/events/{id}/results | application/json |
| GET | /api/event-data/v0/events/{id}/results.csv | text/csv; charset=utf-8 |
| GET | /api/event-data/v0/events/{id}/races/{race_id}/course | application/json |
| GET | /api/event-data/v0/events/{id}/races/{race_id}/course.gpx | application/gpx+xml; charset=utf-8 |
{id} is the event's uuid or any slug it has ever had. {race_id} is a race uuid from the event document, and it must belong to that
event — a race id from another event answers 404 rather than serving that other event's
course under this URL.
GET /events/{id}
The event, its classes, and its races. A class is regatta's race_group: it
carries both the class (PHRF A, J/105) and the division within it (Open, Corinthian). Entries reach races through classes, so a race lists class_ids rather than entries; an empty class_ids means the race is not scoped to a class.
event.status is one of registration_open, registration_closed, in_progress, completed. race.status is one of scheduled, postponed, sequence_started, preparatory, started, shortened, finished, abandoned, cancelled.
{
"schema": "event-data/v0",
"generated_at": "2026-06-20T15:04:05.000Z",
"event": {
"id": "11111111-1111-4111-8111-111111111111",
"slug": "summer-series-abcd1234",
"title": "Summer Series",
"location": "San Diego, CA",
"start_date": "2026-06-20",
"end_date": "2026-06-21",
"timezone": "America/Los_Angeles",
"status": "in_progress",
"description": null,
"website_url": null,
"organizing_authority": "SDYC",
"public_url": "https://regattapages.com/regattas/summer-series-abcd1234",
"updated_at": "2026-06-19T00:00:00.000Z"
},
"classes": [
{
"id": "22222222-2222-4222-8222-222222222222",
"name": "PHRF A",
"division": "Open",
"one_design": false,
"entry_count": 12
}
],
"races": [
{
"id": "33333333-3333-4333-8333-333333333333",
"label": "R1",
"sequence_number": 1,
"status": "finished",
"scheduled_start_time": null,
"start_time": "2026-06-20T18:00:00.000Z",
"rating_system": "PHRF Time-on-Time",
"distance_nm": 8.5,
"course_description": null,
"class_ids": ["22222222-2222-4222-8222-222222222222"],
"has_official_course": true,
"course_url": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/races/33333333-3333-4333-8333-333333333333/course",
"course_gpx_url": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/races/33333333-3333-4333-8333-333333333333/course.gpx"
}
],
"counts": { "classes": 1, "races": 1, "entries": 12 },
"links": {
"self": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234",
"entries": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/entries",
"entries_csv": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/entries.csv",
"results": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/results",
"results_csv": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/results.csv"
}
} public_url and the links block are built from the origin you requested,
so a request to one of this site's hostnames gets links on that same hostname.
Every race carries course_url and course_gpx_url whether or not a
course has been designated, and has_official_course says which. The flag is there
so a consumer walking the race list can decide without a request per race; the URLs answer 404 when the flag is false, which is the same statement.
GET /events/{id}/entries
The approved entry list, with the scratch-sheet fields the public entries page already shows. sail_number is the display form; sail_number_country and sail_number_value are the same number in parts, for consumers that need them apart.
rating_status is unset, proposed, or accepted. rating is null whenever the status is proposed: a proposed rating has not been agreed with the competitor and appears
nowhere on the site.
{
"schema": "event-data/v0",
"generated_at": "2026-06-20T15:04:05.000Z",
"event": {
"id": "11111111-1111-4111-8111-111111111111",
"slug": "summer-series-abcd1234",
"title": "Summer Series",
"public_url": "https://regattapages.com/regattas/summer-series-abcd1234"
},
"entries": [
{
"id": "44444444-4444-4444-8444-444444444444",
"boat_name": "Aeolus",
"sail_number": "USA 123",
"sail_number_country": "USA",
"sail_number_value": 123,
"design": "J/105",
"skipper": "A. Skipper",
"yacht_club": "SDYC",
"home_port": "San Diego",
"classes": [
{
"id": "22222222-2222-4222-8222-222222222222",
"name": "PHRF A",
"division": "Open"
}
],
"rating": 78,
"rating_status": "accepted"
}
],
"counts": { "entries": 1 }
}GET /events/{id}/entries.csv
One row per entry. Columns, in order:
entry_id, boat_name, sail_number, sail_number_country, sail_number_value, design, skipper, yacht_club, home_port, classes, divisions, rating, rating_status classes and divisions are |-joined and positionally
aligned with each other. Every cell is quoted, and a cell whose first non-whitespace character
is =, +, - or @ is prefixed with an apostrophe
so a spreadsheet treats it as text rather than a formula.
GET /events/{id}/results
The standings, the per-race results, and the scoring configuration that produced them. Scoring
is RRS Appendix A low-point throughout, so low_point is always true; it is published so a consumer never has to assume it. scoring_penalty_points is null when the event uses the RRS 44.3(c) default of 20% of the DNF score.
races here lists only the races the scorer scored. The event document lists every
scheduled race, so the two lists can differ mid-regatta. penalty is one of NONE, DNC, DNS, OCS, ZFP, UFD, BFD, SCP, NSC, DNF, RET, DSQ, DNE, DGM, RDG, DPI.
{
"schema": "event-data/v0",
"generated_at": "2026-06-22T02:00:00.000Z",
"event": {
"id": "11111111-1111-4111-8111-111111111111",
"slug": "summer-series-abcd1234",
"title": "Summer Series",
"public_url": "https://regattapages.com/regattas/summer-series-abcd1234"
},
"result_version": {
"source": "snapshot",
"snapshot_id": "55555555-5555-4555-8555-555555555555",
"version": 3,
"state": "official",
"published_at": "2026-06-22T01:00:00.000Z"
},
"scoring": {
"low_point": true,
"a5_3_in_use": true,
"min_races_before_drop": 4,
"max_drops": 1,
"tiebreaker_includes_discards": false,
"scoring_penalty_points": null,
"rating_systems": ["PHRF Time-on-Time"]
},
"races": [
{
"id": "33333333-3333-4333-8333-333333333333",
"label": "R1",
"start_time": "2026-06-20T18:00:00.000Z",
"rating_system": "PHRF Time-on-Time",
"distance_nm": 8.5
}
],
"series_standings": [
{
"place": 1,
"entry_id": "44444444-4444-4444-8444-444444444444",
"boat_name": "Aeolus",
"sail_number": "USA 123",
"rating": 78,
"total_points": 4,
"dropped_points": 3,
"net_points": 1,
"dropped_race_ids": [],
"race_points": [
{ "race_id": "33333333-3333-4333-8333-333333333333", "points": 1 }
]
}
],
"race_results": [
{
"race_id": "33333333-3333-4333-8333-333333333333",
"results": [
{
"entry_id": "44444444-4444-4444-8444-444444444444",
"boat_name": "Aeolus",
"sail_number": "USA 123",
"place": 1,
"points": 1,
"penalty": "NONE",
"elapsed_seconds": 4200,
"corrected_seconds": 4100
}
]
}
]
}GET /events/{id}/results.csv
Long format: one row per entry per race, with the series totals repeated on every row. Columns, in order:
entry_id, boat_name, sail_number, rating, series_place, total_points, dropped_points, net_points, race_id, race_label, race_start_time, race_place, race_points, race_dropped, penalty, elapsed_seconds, corrected_seconds The alternative — a column per race — needs a legend to say which race_3_points belongs to which race, and the legend does not survive the file. Repeating the series columns costs
bytes and buys a file that pivots without one. An entry in an event with no scored races produces
a single row with the race columns empty.
GET /events/{id}/races/{race_id}/course
The official course for one race: the start, the marks in order with the side to leave each
one, the gates with their two ends named, and the finish. A course is here only when the event
organiser has designated one; a race with no designation answers 404, and has_official_course on the event document tells you in advance.
The geometry is frozen at designation. A course is drawn in a routing
project, where it is edited in place and its old versions are not retrievable. Designating
copies the geometry rather than pointing at it, so what you download at 05:00 and what a
protest committee reads at 21:00 are the same marks no matter what anyone has drawn since. course.source names the drawing it came from and the version it was at, as
provenance only — that reference may go stale, or become null if the drawing is deleted,
and neither changes the published course.
Amendments are the norm, so designation is append-only. Each act creates a
new revision, the highest revision is the official course, and every earlier one
stays readable. history lists them all with a URL each, and ?revision=N on this endpoint serves a superseded course for citation. A revision that is not a positive integer is a 400, not a quiet fall
back to the current course.
rounding_side is the side of the boat the mark must remain on: port means the mark stays to port. A gate's port_end and starboard_end are named rather than ordered in a list, because which end is which is
the meaning — crossing the other way does not complete the gate. The start is a point, not a line,
because that is what the source geometry models; a finish may be either.
{
"schema": "event-data/v0",
"generated_at": "2026-06-20T15:04:05.000Z",
"event": {
"id": "11111111-1111-4111-8111-111111111111",
"slug": "summer-series-abcd1234",
"title": "Summer Series",
"public_url": "https://regattapages.com/regattas/summer-series-abcd1234"
},
"race": {
"id": "33333333-3333-4333-8333-333333333333",
"label": "R1",
"sequence_number": 1,
"start_time": "2026-06-20T18:00:00.000Z"
},
"course": {
"format": "official-course/v1",
"name": "Around the island",
"revision": 2,
"label": "Amendment 1",
"note": "Shortened at Mark 3",
"designated_at": "2026-06-20T14:20:00.000Z",
"source": { "course_id": "55555555-5555-4555-8555-555555555555", "course_version": 4 },
"start": { "name": "Pin end", "lat": 32.712345, "lon": -117.173456 },
"marks": [
{
"kind": "mark",
"name": "Mark 1",
"lat": 32.75,
"lon": -117.25,
"rounding_side": "port"
},
{
"kind": "gate",
"name": "Leeward gate",
"port_end": { "lat": 32.72, "lon": -117.2 },
"starboard_end": { "lat": 32.74, "lon": -117.22 }
}
],
"finish": {
"kind": "line",
"name": "Finish",
"port_end": { "lat": 32.7, "lon": -117.16 },
"starboard_end": { "lat": 32.71, "lon": -117.17 }
}
},
"history": [
{
"revision": 1,
"label": null,
"note": null,
"designated_at": "2026-06-19T12:00:00.000Z",
"current": false,
"url": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/races/33333333-3333-4333-8333-333333333333/course?revision=1"
},
{
"revision": 2,
"label": "Amendment 1",
"note": "Shortened at Mark 3",
"designated_at": "2026-06-20T14:20:00.000Z",
"current": true,
"url": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/races/33333333-3333-4333-8333-333333333333/course?revision=2"
}
],
"links": {
"self": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/races/33333333-3333-4333-8333-333333333333/course",
"gpx": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234/races/33333333-3333-4333-8333-333333333333/course.gpx",
"event": "https://regattapages.com/api/event-data/v0/events/summer-series-abcd1234"
}
}GET /events/{id}/races/{race_id}/course.gpx
The same course as a GPX 1.1 route, for loading into a plotter. It accepts the same ?revision=N and answers the same 404s.
GPX cannot carry everything the JSON carries, and this is what it drops. If you are reasoning about the course rather than steering to it, read the JSON.
- Rounding sides. GPX 1.1 has no field for them — the single most important
thing about a racing mark, and the format has nowhere to put it. Each one travels twice
here: as
<rp:rounding_side>in the point's<extensions>, and in plain words in its<desc>("Leave to port"), because a plotter that ignores extensions still shows the description to a human. - Gates and finish lines. A gate is a directional line; a
<rtept>is a point. Emitting both ends as two route points would tell a plotter to sail to each in turn, which is not the course. So the route carries the midpoint, which draws the right shape, and the two real ends ride in the point's extensions as<rp:port_end>and<rp:starboard_end>. Judging a crossing needs the extensions, or the JSON. - The amendment history. The file states its own revision in
<rp:revision>and links back to the JSON, but it does not carry the other revisions.
The extension namespace is https://regattapages.com/xmlns/official-course/v1. Everything outside it is plain
GPX 1.1, so a reader that has never heard of this site still gets a usable route.
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="RegattaPages event-data/v0"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:rp="https://regattapages.com/xmlns/official-course/v1">
<metadata>
<name>Summer Series — R1</name>
<desc>Course: Around the island. Revision 2. Amendment 1. Shortened at Mark 3</desc>
<time>2026-06-20T15:04:05.000Z</time>
</metadata>
<rte>
<name>Summer Series — R1</name>
<extensions>
<rp:revision>2</rp:revision>
<rp:designated_at>2026-06-20T14:20:00.000Z</rp:designated_at>
</extensions>
<rtept lat="32.712345" lon="-117.173456">
<name>Pin end</name>
<desc>Start</desc>
<extensions><rp:role>start</rp:role></extensions>
</rtept>
<rtept lat="32.75" lon="-117.25">
<name>Mark 1</name>
<desc>Leave to port</desc>
<extensions>
<rp:role>mark</rp:role>
<rp:rounding_side>port</rp:rounding_side>
</extensions>
</rtept>
<rtept lat="32.73" lon="-117.21">
<name>Leeward gate</name>
<desc>Gate (midpoint; the two ends are in extensions)</desc>
<extensions>
<rp:role>gate</rp:role>
<rp:port_end lat="32.72" lon="-117.2" />
<rp:starboard_end lat="32.74" lon="-117.22" />
</extensions>
</rtept>
</rte>
</gpx>Identifiers
A uuid resolves, the current slug resolves, and any slug the event has ever had resolves too.
The canonical identifiers travel in the payload: event.id is the uuid and is
stable forever, and event.slug is the current permalink, stable until an organiser
renames the event. Store the uuid.
These endpoints never redirect, by design. The regatta pages under /regattas/* answer 308 to move a browser onto the current slug, because a person is reading
the address bar. A stored integration URL that starts answering 308 is a broken integration,
so an old slug serves the document directly and the payload tells you the current one.
What is published, and what is not
An event is available here exactly when it is published on /regattas/*: any
status except draft and cancelled, and first-party only — events
imported from YachtScoring share the same table but are not this site's to publish. Anything
else answers 404, including a draft event addressed by its correct uuid. There is
no authenticated mode and no query parameter that widens the gate.
Entries are gated a second time: only entries whose approval status is approved are published, the same list the public entry page and scratch sheet show.
A pending or rejected entry is not on the wire.
Results follow one ladder — published official snapshot, else published provisional snapshot,
else a live computation — and result_version says which one you got. A snapshot
has a uuid, a monotonic version, and a state of official or provisional. Most events have never had a snapshot
taken, so a live computation is the common case, and it is the same thing the site's own
results page shows. It is reported honestly rather than dressed up as a version:
"result_version": {
"source": "live",
"snapshot_id": null,
"version": null,
"state": null,
"published_at": null
} If you need an immutable citation — a result you can point at later and know it has not
changed — require source == "snapshot".
Errors
Success bodies are the document itself, with no wrapper: an interchange document belongs at
the root. Errors do use the site-wide wrapper, so a 404 reads:
{
"success": false,
"data": null,
"error": { "code": "not_found", "message": "Event not found" }
}Versioning
The version lives in the path, and in every body as "schema": "event-data/v0".
v0 is unstable for one season. Breaking changes are allowed in v0, and they will be announced on this page before they ship. Build against it, but pin nothing you cannot re-read.
v1 freezes once two external consumers exist or the season ends, whichever is later. From then on a field is never removed, renamed, or changed in meaning within a version — fields may only be added — and a breaking change means a new version at a new path.
If you are building against this, tell us at contact@regattapages.com. Two consumers is the number that closes v0, and we would rather hear about a shape that does not fit while it can still be changed.
Known gaps
A course is published only where an organiser has designated one. Course
geometry is not something the application derives — race.course_description is a
free-text column no application code writes, and it stays null in practice. The
course endpoints serve a geometry an organiser explicitly signed, so a race nobody has
designated a course for has none to publish and answers 404. Most historic races
are in that position and always will be.
A designated course says nothing about the start sequence or the class it applies to. One designation belongs to one race. An event that sends its classes round different courses needs one race per course, which is how the race list already models it; there is no per-class course within a single race.
Results may include entries that are not on the entry list. The scorer scores
every entry row for the event, whereas the entry list publishes only approved ones, so a boat
that raced before its entry was approved is scored but unlisted. Join on entry_id with a left join, not an inner one. boat_name and sail_number are repeated on every standing and every race result so a results consumer
never needs the entry list at all.
Also absent, and deliberately: crew lists, event documents such as the NOR and sailing instructions, jury decisions and redress (their effect is already in the points), ORC certificate references (nothing in the schema links a certificate to an entry), and tracker links. Registration answers, contact details, and payment status are behind admin auth and are not published at any status.
Changelog
- 2026-08-14 — official courses
- Two endpoints added:
…/races/{race_id}/courseand…/course.gpx. Races on the event document gainedhas_official_course,course_urlandcourse_gpx_url. Nothing was removed or renamed. The "no course endpoint" gap is closed: an organiser can now designate a course for a race, and each designation is a new revision rather than an overwrite. - 2026-08-14 — v0 launch
- First release. Five endpoints: event, entries, entries CSV, results, results CSV.