Skip to content

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)
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:

  • POST and PATCH merge: the rows you send are added or updated and every row you leave out is kept. Neither can delete anything.
  • PUT replaces the list with exactly what you send — that is its meaning, so a client that wants to change one row should use PATCH or send a single object. (Results for athletes who have attempts are then recomputed from their trials, so a results-only PUT can 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 as id) 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 lane and reaction_time, horizontal jumps/throws include round attempts, and vertical jumps include attempts at each height