Units API¶
This API endpoint provides access to units (individual heats/rounds/pools) within events in competitions on OpenTrack. Each unit represents a single race, field event pool, or flight — for example, "100m Heat 1", "Long Jump Final", or "Shot Put Pool A".
Endpoint URL: https://{domain}/api/units/
For example: https://test-data.opentrack.run/api/units/
Quick Start Examples¶
# Obtain a token and list all units for an event via its natural key URL
TOKEN=$(curl -s -X POST "https://test-data.opentrack.run/api/get-auth-token/" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=api-test@mailinator.com&password=apitest' | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
curl "https://test-data.opentrack.run/api/x/2026/GBR/lac-open-2/event/T01/rounds/" \
-H "Authorization: Token $TOKEN"
import requests
# Obtain a token
resp = requests.post("https://test-data.opentrack.run/api/get-auth-token/",
data={"username": "api-test@mailinator.com",
"password": "apitest"})
token = resp.json()["token"]
# List all units for an event via its natural key URL
headers = {"Authorization": f"Token {token}"}
r = requests.get("https://test-data.opentrack.run/api/x/2026/GBR/lac-open-2/event/T01/rounds/",
headers=headers)
data = r.json()
for unit in data["results"]:
print(f'{unit["heat_name"]}: {unit["status"]}')
Example output:
Race 1 of 8: finished
Race 2 of 8: finished
Race 3 of 8: finished
...
Filtering across all competitions
The natural key URL above returns all the units of one specific event, and is deliberately kept simple — it does not take filter parameters. If you want to filter units more broadly (for example, all finished 100m units on the whole site), use the standard list endpoint with the competition UUID and filter parameters instead:
/api/units/?competition={competition_uuid}&event_code=100&status=finished&limit=5
API Playground¶
The API uses Django REST Framework's browsable API interface, which provides an interactive playground when accessed via a web browser. The playground allows you to:
- View and filter units
- Create new units
- Update and delete individual units
- Test different pagination limits
- View results in JSON or HTML format
To access the playground, simply navigate to the API URL while logged in with the appropriate credentials.
HTTP Methods¶
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/units/ |
List all units (paginated) |
POST |
/api/units/ |
Create a new unit |
GET |
/api/units/{id}/ |
Retrieve a specific unit |
PUT |
/api/units/{id}/ |
Fully update a specific unit |
PATCH |
/api/units/{id}/ |
Partially update a specific unit |
DELETE |
/api/units/{id}/ |
Delete a specific unit |
GET |
/api/x/{year}/{country}/{slug}/event/{event_id}/rounds/ |
List all units for an event (natural key) |
GET |
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/ |
Retrieve a unit by natural key |
PATCH |
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/ |
Partially update a unit by natural key |
PUT |
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/ |
Fully update a unit by natural key |
DELETE |
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/ |
Delete a unit by natural key |
PATCH |
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/results/{bib}/ |
Add or edit a single result by bib (returns the full unit) |
PUT |
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/results/{bib}/ |
Add or edit a single result by bib (returns the full unit) |
DELETE |
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/results/{bib}/ |
Delete a single result by bib |
Natural Key URL Pattern¶
As with competitions and events, units can be addressed using the natural key URL pattern instead of the MongoDB unit id. A unit is uniquely identified within its competition by the event identifier, round number, and heat number:
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/
For example:
https://test-data.opentrack.run/api/x/2026/GBR/lac-open-2/event/T01/1/1/
To list all the units (rounds and heats) belonging to an event, omit the round/heat:
https://test-data.opentrack.run/api/x/2026/GBR/lac-open-2/event/T01/rounds/
Example URLs use the test server
The example URLs in this section deliberately point at the test server (test-data.opentrack.run) so you can experiment with write operations (PATCH, PUT, DELETE) without risking live data. Use the live server domain or federation domain when you are ready to make real changes.
| Component | Example | Description |
|---|---|---|
year |
2026 |
The year of the competition (4 digits) |
country |
GBR |
The ISO 3-letter country code |
slug |
lac-open-2 |
The competition's URL slug |
event_id |
T01 |
The event identifier within the competition |
round |
1 |
The round number (1 = first round, 2 = semi-final, 3 = final, etc.) |
heat |
1 |
The heat number within the round |
The single-unit natural key URL supports the full range of operations: GET, PATCH, PUT and DELETE — so once you know the competition, event identifier, round and heat, you do not need to look up any database IDs.
Response Fields¶
Each unit record in the response contains the following fields:
Basic Information¶
| Field | Type | Description |
|---|---|---|
url |
URL | Link to this specific unit record |
id |
String | MongoDB-style UUID identifier for the unit (e.g., "337d675a-65e9-42e1-bed0-697a7df69c3b") |
event |
URL | Link to the parent event spec for this unit |
event_id |
String | Event identifier code (e.g., "T15") |
event_code |
String | Athletics event code (e.g., "100", "HJ", "LJ") |
event_name |
String | Display name of the parent event |
day |
Integer | Competition day this unit runs on |
round |
Integer | Round number (1 = first round, 2 = semi-final, 3 = final, etc.) |
round_name |
String | Display name for the round (e.g., "Final", "Semi-Final") |
heat |
Integer | Heat number within the round |
heat_name |
String | Display name for the heat (e.g., "Final", "Heat 1") |
unit_code |
String | Unit type code (e.g., "FINAL", "HEAT", "POOL") |
Scheduling & Status¶
| Field | Type | Description |
|---|---|---|
scheduled_start_time |
Time | Scheduled start time (e.g., "17:56") |
status |
String | Unit status: scheduled, in_progress, finished, cancelled |
results_status |
String | Results status: none, seeded, in_progress, provisional_results, official_results |
source |
String | Source filename for the results (e.g., "T15-3-01.lif" from FinishLynx) |
published |
Boolean | Whether the unit results are published publicly |
participant_count |
Integer | Number of participants in the unit (read-only) |
Performance Context¶
| Field | Type | Description |
|---|---|---|
distance |
Integer | Race distance in metres (for track events) |
wind_assistance |
String | Wind reading (e.g., "+1.6") |
precision |
Integer | Number of decimal places for performance times |
paces |
Array | Pacing schedule data (empty if none) |
split_distances |
Array | Split distances in metres |
splits_lap |
Integer | Distance between split recording points |
splits_start |
Integer | Distance before first split is recorded |
Override Flags¶
| Field | Type | Description |
|---|---|---|
override_places |
Boolean | Whether places have been manually overridden |
override_ce_scores |
Boolean | Whether combined event scores have been overridden |
original_results_status |
String | Original results status before any override |
Display Flags¶
| Field | Type | Description |
|---|---|---|
show_athlete_details |
Boolean | Whether to show athlete details (DOB, etc.) |
show_handicap |
Boolean | Whether to show handicap information |
show_points |
Boolean | Whether to show points/score |
show_raw_time |
Boolean | Whether to show raw (unedited) times |
show_reaction_time |
Boolean | Whether to show reaction times |
Field Event (Throws) Fields¶
Present on length units that have PolyField data. Both are read/write: send them in a PATCH or PUT to the unit (or with the POST that creates it) and they come back in the unit payload.
| Field | Type | Description |
|---|---|---|
polyfield_config |
String | Sector configuration the throws map draws: Discus, Hammer, Shot or Javelin, and the event codes DT, HT, WT, SP, JT are understood. Anything unrecognised or empty is drawn as a discus (1.25 m circle, 34.92° sector). When unset the event code decides, so a throws unit plots correctly with no configuration at all |
polyfield_geometry |
Object | Geometry supplied by the measuring client, stored as-is. The current map does not read it |
PATCH /api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/
{"polyfield_config": "Hammer"}
The recording endpoint accepts the same two fields alongside the attempts, which is how a measuring client sets the sector as it records (see Unit-level PolyField fields).
Winner Summary (Read-Only)¶
| Field | Type | Description |
|---|---|---|
winner_performance |
String | Performance of the winner (e.g., "10.32") |
winner_first_name |
String | First name of the winner |
winner_last_name |
String | Last name of the winner |
winner_team |
String | Team/club of the winner |
Timestamps (Read-Only)¶
| Field | Type | Description |
|---|---|---|
created |
Date-time | When the unit was created (ISO 8601, or null) |
modified |
Date-time | When the unit was last modified (ISO 8601, or null) |
Related IDs¶
| Field | Type | Description |
|---|---|---|
sql_unit_id |
Integer | Legacy SQL database unit identifier |
mongo_unit_id |
String | MongoDB unit identifier (same as id) |
Fetching Units¶
List All Units¶
GET /api/units/
Returns a paginated list of units. By default the list returns all units you have permission to see. Add ?my_competitions=1 to scope it to competitions you administer.
Retrieve a Specific Unit¶
By natural key (recommended):
GET /api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/
Example: GET /api/x/2026/GBR/lac-open-2/event/T01/1/1/
Returns a single unit record by its competition natural key, event identifier, round and heat.
By primary key:
GET /api/units/{id}/
Example: GET /api/units/1/
Returns a single unit record by its primary key ID. Both patterns return the same unit record.
Filter Units by Event¶
By natural key (recommended):
GET /api/x/{year}/{country}/{slug}/event/{event_id}/rounds/
Example: GET /api/x/2026/GBR/lac-open-2/event/T01/rounds/
By event spec ID:
GET /api/units/?event={event_spec_id}
Creating Units¶
Create a New Unit¶
POST /api/units/
Requires authentication with director/admin permissions. The request body should be a JSON object with fields such as event, day, round, heat, and scheduled_start_time.
Example:
{
"event": "https://test-data.opentrack.run/api/event/1/",
"day": 1,
"round": 1,
"heat": 1,
"heat_name": "Heat 1",
"scheduled_start_time": "10:00",
"status": "scheduled"
}
Updating Units¶
Full Update (PUT)¶
PUT /api/units/{id}/
Replaces the entire unit record. All required fields must be provided.
Partial Update (PATCH)¶
PATCH /api/units/{id}/
Only updates the fields provided in the request body.
Example - Update unit status:
{
"status": "finished",
"results_status": "provisional_results"
}
Direct Results Upload¶
For uploading results to a unit, you can also use the dedicated results endpoint:
PUT https://test-data.opentrack.run/api/unit/{mongo_unit_id}/
This accepts a payload of the fields you wish to overwrite. For example, to set results:
{
"results": [
{ "bib": "11", "performance": "3:52.8" }
]
}
The system will calculate places and update the race status to provisional once results arrive.
Managing Results on a Unit¶
Results are updated by merging against the existing list keyed on bib — sending a row updates or adds that bib, but never removes a bib that isn't in the payload.
Attempts behave the same way on a PATCH: trials rows are matched on bib + round (lengths) or bib + height (vertical jumps), the rows you send are added or updated, and every attempt you leave out is kept. Only PUT replaces the attempts with exactly what you send — which is what the recording screens rely on when they save the list they hold.
Update a result (PATCH)¶
Use a PATCH to the unit's natural key with a results array containing only the rows you want to change:
PATCH /api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/
{
"results": [
{ "bib": "11", "lane": 5 }
]
}
Each row is matched to the existing result by bib and only the fields you send are updated, so the other competitors and the rest of that bib's data are left untouched. New bibs in the array are appended.
Add or edit a single result (PATCH/PUT)¶
For a single result, use the dedicated per-bib endpoint instead of the whole results array. This works for any unit type (race/track, throws, horizontal jump, height):
PATCH /api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/results/{bib}/
{
"performance": "10.42",
"lane": 5
}
The bib comes from the URL; the body carries the result's attributes. If a result with that bib already exists it is edited in place and the response is 200; if not, a new result is created and the response is 201. Both return the full updated unit as JSON. The unit's participant_count is adjusted accordingly.
The same URL also removes a result
The .../results/{bib}/ URL supports PATCH/PUT (add or edit) and DELETE (remove) for that single result — see below.
Add or edit a single attempt (PATCH/PUT)¶
For field events, an individual attempt can be recorded on its own, without re-sending every attempt on the unit. This is the endpoint the OpenTrack recording screens use, and the one to use from a sector measuring client such as PolyField:
PATCH /api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/record/trials/
PATCH, POST and PUT all accept this body; a bare object writes only that row whichever verb you use.
A single throw or horizontal jump attempt:
{
"bib": "55",
"round": 3,
"result": "6.87",
"to_board": "0.12",
"polyfield": { "rx": "-0.42", "ry": "7.31" }
}
A single vertical jump attempt:
{
"bib": "55",
"height": "1.85",
"result": "o"
}
In the body:
| Field | Meaning |
|---|---|
bib |
Athlete's bib, as a string |
round |
Attempt number, 1-6 (lengths only) |
height |
Bar height in metres (vertical jumps only) |
result |
Distance in metres, or x for a foul (lengths); one of o, x, -, p (vertical jumps) |
to_board |
Optional. Distance to the take-off board, or overshoot if negative |
polyfield |
Optional. Landing position measured by PolyField, as {"rx": "...", "ry": "..."} |
The attempt is matched on bib + round (lengths) or bib + height (vertical jumps): an existing attempt with that key is replaced, otherwise it is appended. Every save creates a revision of the unit.
The response is the full updated unit — trials, results, results_status (plus heights for vertical jumps) — with a trial key holding the attempt that was just written, so a client can confirm the save without re-reading the unit.
POST and PATCH merge a list; PUT replaces it
The same URL still accepts the whole-list payload used by the recording pages: {"trials": [...], "results": [...], "results_status": "..."}. Sending trials, results or heights switches the endpoint into bulk mode, so never include those keys in a single-attempt body.
A single object ({"bib": ..., "round": ..., "result": ...}) always writes that one row, whichever verb you use.
Whole lists follow the verb:
POSTandPATCHmerge: the rows you send are added or updated and every row you leave out is kept. Neither can delete anything.PUTreplaces the list with exactly what you send — that is its meaning, so a client that wants to change one row should usePATCHor send a single object. (Results for athletes who have attempts are then recomputed from their trials, so aresults-onlyPUTcan keep rows you did not send.)
PATCH with this body adds or edits bib 777's result and keeps the other nine:
{"results": [{"bib": "777", "performance": "5.00"}]}
PUT with the same body replaces the whole list with that one row. Recording one attempt needs no list at all:
{"bib": "8", "round": 5, "result": "3.87"}
PolyField landing positions¶
polyfield.rx and polyfield.ry are the rotated landing coordinates in metres, stored exactly as PolyField's measuring equipment supplies them. They are what the Throws map on the event page plots, and they are optional: leave polyfield out for attempts recorded by hand, and such attempts simply do not appear on the map.
Unit-level PolyField fields¶
These two optional fields live on the unit rather than on an individual trial, and explain what the map draws. The unit resource both returns and accepts them; the recording endpoint also takes them alongside the rows — polyfield_config (or its alias config) and polyfield_geometry — and echoes them back:
| Field | Contents |
|---|---|
polyfield_config |
Name of the sector configuration, as supplied by the measuring client: Discus, Hammer, Shot or Javelin, and the matching event codes DT, HT, WT, SP, JT are understood. It selects the circle radius and sector angle. Anything unrecognised, or empty, is drawn as a discus: 1.25 m circle, 34.92° sector. |
polyfield_geometry |
Geometry as supplied by the client, stored as-is. The current map does not read it — the sector for each implement is built into the plot. |
Set the sector in the same body as the attempts, which is what a measuring client does for a non-standard circle (a seated club throw from the hammer circle, say):
{"config": "Hammer", "trials": [{"bib": "5", "round": 1, "result": "48.19", "polyfield": {"rx": "-1.89", "ry": "49.40"}}]}
When polyfield_config is absent the event code is used instead, so a throws event with coordinates plots correctly without either field being set.
Errors¶
| Response | When |
|---|---|
400 |
An attempt with bib and result but neither round nor height — without the key the attempt is not stored |
400 |
The body has none of the recognised keys (bib, trials, results, heights, results_status, polyfield_config) |
403 |
The account does not hold a director, official or recorder role on the competition |
Try it in the browsable API
Open the URL in a browser while logged in. The Raw data box on the POST, PATCH and PUT forms shows a greyed example payload (the vertical-jump shape on .../record/height-trials/), so you can see the expected body without consulting this page.
Set a field to blank¶
To clear a field (for example a race lane), send it with an explicit null value:
{
"results": [
{ "bib": "11", "lane": null }
]
}
On a race result lane is nullable, so the blank lane is saved and reads back as "lane": null.
Remove a result entirely¶
To drop a result completely, DELETE it by bib rather than patching it:
DELETE /api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/results/{bib}/
For example:
DELETE /api/x/2026/GBR/lac-open-2/event/T01/3/1/results/11/
This removes the matching result from the unit and decrements the unit's participant_count. A request for a bib that is not on the unit returns 404; on success the endpoint returns 204 with no body.
Deleting Units¶
Delete a Unit¶
DELETE /api/units/{id}/
Deletes the unit and all associated results data. This action is irreversible.
Filtering¶
You can filter the units list using the following query parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
event |
Integer | Filter by event spec ID | ?event=1 |
event_id |
String | Filter by event identifier code | ?event_id=T15 |
event_code |
String | Filter by athletics event code | ?event_code=100 |
day |
Integer | Filter by competition day | ?day=1 |
round |
Integer | Filter by round number | ?round=3 |
heat |
Integer | Filter by heat number | ?heat=1 |
status |
String | Filter by unit status | ?status=finished |
results_status |
String | Filter by results status | ?results_status=official |
my_competitions |
Flag | Scope to competitions you administer | ?my_competitions=1 |
Filter Examples¶
The natural key URL is deliberately simple: it returns all the units of one event and does not take filter parameters.
List all units for an event via its natural key URL:
/api/x/2026/GBR/lac-open-2/event/T01/rounds/
To filter units (by event, event code, round, heat, status, etc.), use the standard list endpoint:
/api/units/?event=1
Get all finals (round 3) for 100m events:
/api/units/?event_code=100&round=3
Get all completed units:
/api/units/?status=finished
Get all units with official results:
/api/units/?results_status=official_results
Ordering¶
You can order the results using the ordering parameter. Prefix with - for descending order.
| Field | Description |
|---|---|
id |
Order by unit ID |
day |
Order by competition day |
round |
Order by round number |
heat |
Order by heat number |
scheduled_start_time |
Order by scheduled start time |
status |
Order by status |
participant_count |
Order by number of participants |
Ordering Examples¶
Order by day and then scheduled time:
/api/units/?ordering=day,scheduled_start_time
Order by most participants first:
/api/units/?ordering=-participant_count
Pagination¶
The API uses limit/offset pagination to control the number of results returned.
| Parameter | Type | Description | Default |
|---|---|---|---|
limit |
Integer | Number of records to return | 50 |
offset |
Integer | Starting position in the result set | 0 |
Pagination Examples¶
Get the first 20 units:
/api/units/?limit=20
Get the next 20 units:
/api/units/?limit=20&offset=20
Reading Unit Data via Public JSON URLs¶
Unit data (including full results) can be accessed via the public JSON URL pattern. It requires you to be authenticated, like the rest of the API, but needs no special permissions — any logged-in user can read it:
https://test-data.opentrack.run/en-gb/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/json/
Example:
https://test-data.opentrack.run/en-gb/x/2026/GBR/lac-open-2/event/T01/1/1/json/
This returns the full unit data including nested results for each competitor. The structure varies between running, field, and vertical jump events.
Public JSON URLs vs the Natural Key API
These public JSON URLs are read-only and require authentication but no special permissions (any logged-in user). Note the path differs from the API natural key pattern: the public URLs use event/ (singular) and are served from /en-gb/x/..., while the API uses /api/x/.../event/....
If you need to read or modify a unit (e.g. PATCH a heat name, PUT results), use the authenticated natural key API instead: /api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/.
Unit Results Structure¶
{
"day": 1,
"distance": 100,
"event_code": "100",
"event_id": "T15",
"event_name": "SENM/U23M 100M",
"heat": 1,
"heat_name": "Final",
"id": "337d675a-65e9-42e1-bed0-697a7df69c3b",
"results_status": "official",
"round": 3,
"round_name": "Final",
"scheduled_start_time": "17:56",
"status": "finished",
"unit_code": "FINAL",
"wind_assistance": "+1.6",
"results": [
{
"place": 1,
"bib": "81",
"performance": "10.32",
"lane": 5,
"category": "U23M",
"first_name": "Michael",
"last_name": "Onilogbo",
"full_name": "Michael Onilogbo",
"team": "NEB",
"team_name": "Newham & Essex Beagles AC",
"nationality": "GBR",
"pb": "10.38",
"sb": "10.41",
"date_of_birth": "2004-03-12",
...
}
]
}
Each result in the unit API carries the same competitor-enriched fields as the public JSON — including team_name, category, full_name, team, gender, nationality, age_group, pb/sb/nr, and flag fields. The events_entered field is not included in the unit API response.
Fields in the results array may vary depending on the type of unit (sprint, throw, horizontal jump, vertical jump). The key fields always available are place, bib, and performance.
Date of birth is permission-gated
date_of_birth on each result is returned only to the competition's directors/admins, federation admins, and superusers. For any other authenticated user it is omitted from the results.
Combined Query Example¶
Get all finished units for event code "100" on day 1, ordered by round and heat:
/api/units/?event_code=100&day=1&status=finished&ordering=round,heat
Permissions¶
All API endpoints require authentication — there are no public endpoints. Log in with a valid API token or session cookie to access them. Authenticated users can list all units and view unit details; write operations are restricted to the competition's directors and admins (plus federation admins, who have access to all competitions within their federation), and only permitted until 7 days after the competition finishes.
| Method | Endpoint | Permission Required |
|---|---|---|
GET (list/detail) |
/api/units/ |
Any authenticated user |
POST |
/api/units/ |
Director/Admin of the competition (or federation admin for their federation), within 7 days after the competition finishes |
PUT / PATCH |
/api/units/{id}/ |
Director/Admin of the competition (or federation admin for their federation), within 7 days after the competition finishes |
DELETE |
/api/units/{id}/ |
Director/Admin of the competition (or federation admin for their federation), within 7 days after the competition finishes |
PUT |
/api/unit/{mongo_unit_id}/ |
Director/Admin of the competition (or federation admin for their federation), within 7 days after the competition finishes |
Notes¶
- Each unit represents one heat, round, or pool within an event. A competition creates units automatically when events with multi-round structures (e.g.,
rounds="8,3,1") are saved - The natural key pattern (
/api/x/{year}/{country}/{slug}/event/{event_id}/{round}/{heat}/) addresses a unit by its competition, event identifier, round and heat, and is the recommended way to reference a single unit - The
mongo_unit_id(same asid) is used in the legacy results upload endpoint:/api/unit/{mongo_unit_id}/ - Public JSON URLs (via the
/en-gb/x/.../json/pattern) require authentication but no special permissions (any logged-in user) and are suitable for embedding results in websites - For live updates, each unit has its own channel that emits data whenever the unit is saved — see the Live Data documentation for more information
- Results fields vary between event types. Track events include
laneandreaction_time, horizontal jumps/throws includeroundattempts, and vertical jumps include attempts at each height