Skip to content

API Documentation

Since summer 2020, OpenTrack is using a new API gateway at https://api.opentrack.run/

This uses the FastAPI web framework, connected to the main OpenTrack application database. This implements the OpenAPI specification, so there is a rich family of tools to help you use it.

Getting access

To access the API, you need an account on OpenTrack, so if you have not used it before, go to our site and sign up. The API generates a documentation page which you can click on and explore from a browser, to try the various queries. This is available in Swagger and ReDoc formats; we use the first one in screenshots below.

Connecting and making queries

We have illustrated our examples with the Unix CURL command, but you can also call APIs with wget or libraries in any language.

A very simple query you can do is to call our 'ping' API, to check it's alive; this requires no authentication:

$ curl -X GET "https://api.opentrack.run/api/v1/ping/" -H  "accept: application/json"

{
  "reply": "pong",
  "enviroment": "otr_0"
}

Or, with Python's requests library,

Authorizing and getting a token

Almost all API calls require a token. You make an initial request for a token, sending your username and password; and then use this token as a request header on subsequent requests. Tokens are valid for 24 hours.

Here is a complete runnable script to query competitions, with some filter parameters, using a token, and Python's requests library.

import requests

EMAIL = '<your email>'
PASSWORD = '<your password>'

r = requests.post(
    "https://api.opentrack.run/api/v1/auth/token", 
    data={"username": EMAIL, "password": PASSWORD})

resp = r.json()

token = resp["access_token"]

headers = {'Authorization': 'Bearer %s' % token}

r = requests.get(
    "https://api.opentrack.run/api/v1/competitions/", 
    params=dict(country="NOR", from_date="2020-09-01"),
    headers=headers
    )
competitions = r.json()
print("Retrieved %d competitions in Norway" % len(competitions))

Querying competition calendar entries

Our competition calendar has light, simple records for each competition. You can query these easily with request parameters. For example, this returns all competitions in Norway since 1st August 2020:

GET https://api.opentrack.run/api/v1/competitions?country=NOR&from_date=2020-08-01

[
  {
    "full_name": "Trond Mohn Games",
    "short_name": "TMG",
    "slug": "trond-mohn-games",
    "name_local": "Trond Mohn Games",
    "country_id": "NOR",
    "address": "Grimseidvegen 60, 5239 Rådal, Norge",
    "city": "Bergen",
    "venue_id": "c3c87364-0ab4-44b5-a518-b143f574c086",
    "latitude": 60.2905,
    "longitude": 5.3178,
    "altitude": 60,
    "date": "2021-05-29",
    "finish_date": "2021-05-29",
    "wa_rankings_category_id": null,
    "age_groups": [
      "Men, women"
    ],
    "national_id": "NOR",
    "basic_description": "",
    "contact_details": "Dag Rydland drydland@broadpark.no",
    "organiser_id": "e893fa86-8c2e-4f99-a9f1-b6fcfb0a787c",
    "website": "https://www.trondmohngames.no",
    "entry_link": "http://www.trondmohngames.no",
    "results_link": "http://en.trondmohngames.no/live-results/",
    "override_token": null,
    "id": "12f4b445-8a02-446c-98ec-0e7701623170",
    "year": 2021
  },
  .... # competitions omitted
 ]

If you wish to power a calendar and load data without authenticating, we also offer files built every night and stored in Amazon in exactly the same format. We publish these for the CURRENT and FOLLOWING year.

https://file.opentrack.run/live/euroath/european_calendar_2020.json
https://file.opentrack.run/live/euroath/european_calendar_2021.json
https://file.opentrack.run/live/euroath/domestic_calendar_GBR_2020.json
https://file.opentrack.run/live/euroath/domestic_calendar_GBR_2021.json

These are suitable for embedding or calling from other off-site pages (i.e. the storage bucket has no CORS headers), and will be available even when we are doing server maintenance

Creating new competitions and calendar entries

We can expose the API to national-level systems providers. In many countries (e.g. Germany, Spain, Italy, France) there is a "national system" which contains most or all competitions; we aim to make it easy for these people to register a competition in Opentrack, tell us their ID, and get our ID for the competition.

Your user account needs to be given admin privileges for the country in question. We do ths by assigning your user to a group called ADMIN_XXX on the server, where XXX is the ISO country code. Thus, if you want rights for Estonia, your user must be in the ADMIN_EST group.

Conceptually there MIGHT be a two-step process, although it is usually one, as we need to check very carefully that we are not creating duplicates.

Example

import requests
import json

EMAIL = '<your email>'
PASSWORD = '<your password>'

r = requests.post(
    "https://api.opentrack.run/api/v1/auth/token", 
    data={"username": EMAIL, "password": PASSWORD})

resp = r.json()

token = resp["access_token"]

headers = {'Authorization': 'Bearer %s' % token}

comp = {
    'full_name': 'Trond Mohn Games',
    'short_name': 'TMG',
    'slug': 'trond-mohn-games',
    'name_local': 'Trond Mohn Games',
    'country_id': 'NOR',
    'address': 'Grimseidvegen 60, 5239 Rådal, Norge',
    'city': 'Bergen',
    'venue_id': 'c3c87364-0ab4-44b5-a518-b143f574c086',
    'latitude': 60.2905,
    'longitude': 5.3178,
    'altitude': 60,
    'date': '2021-05-29',
    'finish_date': '2021-05-29',
    'wa_rankings_category_id': None,
    'age_groups': ['Men, women'],
    'national_id': 'NOR',
    'basic_description': '',
    'contact_details': 'Dag Rydland drydland@broadpark.no',
    'organiser_id': 'e893fa86-8c2e-4f99-a9f1-b6fcfb0a787c',
    'website': 'https://www.trondmohngames.no',
    'entry_link': 'http://www.trondmohngames.no',
    'results_link': 'http://en.trondmohngames.no/live-results/'
}

r = requests.post(
    "https://api.opentrack.run/api/v1/competitions/", 
    data=json.dumps(comp),
    headers=headers
    )

print("Created competiton with status_code %d" %  r.status_code)

Duplicates

As mentioned before, the API carefully checks there are not similar competitions already in our database. If it is the case, a special error message will be returned.

{
        "detail": {
            "error": "A competition with the same name and date exists",
            "override_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTYzMjUxMzEuMDcyNDExLCJuYmYiOjE2MDA3NzMxMzF9.HZDkjTH5RnsAu9U7AFFmvGb5smM0RWRqLE5nwuVKBBQ",
            "mathing_competitions": [
                {
                    "full_name": "Trond Mohn Games",
                    "short_name": "TMG",
                    "slug": "trond-mohn-games",
                    "name_local": "Trond Mohn Games",
                    "country_id": "NOR",
                    "address": "Grimseidvegen 60, 5239 Rådal, Norge",
                    "city": "Bergen",
                    "venue_id": "c3c87364-0ab4-44b5-a518-b143f574c086",
                    "latitude": 60.2905,
                    "longitude": 5.3178,
                    "altitude": 60,
                    "date": "2021-05-29",
                    "finish_date": "2021-05-29",
                    "wa_rankings_category_id": null,
                    "age_groups": [
                    "Men, women"
                    ],
                    "national_id": "NOR",
                    "basic_description": "",
                    "contact_details": "Dag Rydland drydland@broadpark.no",
                    "organiser_id": "e893fa86-8c2e-4f99-a9f1-b6fcfb0a787c",
                    "website": "https://www.trondmohngames.no",
                    "entry_link": "http://www.trondmohngames.no",
                    "results_link": "http://en.trondmohngames.no/live-results/",
                    "override_token": null,
                    "id": "12f4b445-8a02-446c-98ec-0e7701623170",
                    "year": 2021
                }
            ]
        }
    }

If this happens you have two options.

  • Option 1: the competition you wanted to create is a duplicate. Use the provided id (868aadc8-f9bb-49bc-952f-25c16eadb69e in the example) and use one of the other API endpoints to update the competition
  • Option 2: the competition you wanted to create is new. Use the provided override_token (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTYzMjUxMzEuMDcyNDExLCJuYmYiOjE2MDA3NzMxMzF9.HZDkjTH5RnsAu9U7AFFmvGb5smM0RWRqLE5nwuVKBBQ in the example) and add it as a new field in the comp dictionary above. NOTE: the token has a 3 days validity.

Uploading full results

Our Competition data model

Our logical model of a competition consists of a Competition record, with some nested arrays for Competitors and Events.

Competition - descriptive and configuration fields
    competitors:
        Competitor 1 - bib, name, age group, gender etc
        Competitor 2
        etc
    events:
        Event 1
        Event 2
        Event 3 - e.g. Senior Men's 100m
            Unit 1. (e.g. heat 1)
                Result or Starter 1.  e.g. bib=281, time=10.93
                Result or Starter 2
                Result or Starter 3
            Unit 2

The easiest way to see this (without authentication) is by browsing to a competition on our main site, data.opentrack.runhttps://data.opentrack.run/), and adding /json/ to the URL. After a few seconds you will download a big JSON URL.

As you browse our site, you can see the JSON for many elements by appending /json/ to the URL.

Uploading results when creating a new competition

Following the same process described here, we can upload a competiton and its results as follow:

comp = {
    'full_name': 'api-test long name',
    'short_name': 'api-test',
    'slug': 'api-test-slug',
    'name_local': 'api-test in italiano',
    'country_id': 'ITA',
    'address': 'Via dello Stadio 1, City',
    'venue_id': '051e8072-f667-4f10-aa82-b16a9317588e',
    'latitude': '20.123',
    'longitude': '-15.321',
    'altitude': '1200.14',
    'date': '2020-08-10',
    'finish_date': '2020-08-15',
    'wa_rankings_category_id': 'DF',
    'age_groups': ['ALL'],
    'national_id': 654321,
    'basic_description': 'Competition in Italy',
    'contact_details': 'example@example.it',
    'organiser_id': '179c0872-f761-4ed1-bb75-3b58a1368bac',
    'website': 'https://example.example',
    'entry_link': 'https://example.example/entry',
    'results_link': 'https://example.example/results',
    'override_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTI2MDkwODguNDM5MjA1LCJuYmYiOjE1OTcwNTcwODh9.hz-rUfoCNvrMVIOCnwUJhDAzyySS412iMG11Th6OWzk',
    "competitors": [
        {
            "age_group": "PM",
            "category": "PM",
            "competitor_id": "ZZ000002",
            "date_of_birth": "1998-08-19",
            "events_entered": [
                {
                    "event_code": "200",
                    "event_id": "004"
                },
                {
                    "event_code": "400",
                    "event_id": "006"
                }
            ],
            "first_name": "name",
            "gender": "M",
            "last_name": "surname",
            "team_id": "ITA"
        },
        {
            "age_group": "JM",
            "category": "JM",
            "competitor_id": "ZZ000011",
            "date_of_birth": "1999-10-08",
            "events_entered": [
                {
                    "event_code": "10KW",
                    "event_id": "043"
                },
                {
                    "event_code": "10KW",
                    "event_id": "243"
                }
            ],
            "first_name": "name2",
            "gender": "M",
            "last_name": "surname2",
            "team_id": "ALG"
        }
    ],
    "relay_teams": [
        {
            "event_id": "047",
            "name": "ALGERIA",
            "relay_team_id": "ALGA",
            "runners": [
                "ZZ000004",
                "ZZ000008",
                "ZZ000005",
                "ZZ000003"
            ],
            "team_id": "ALG"
        },
        {
            "event_id": "047",
            "name": "SPAIN",
            "relay_team_id": "ESPA",
            "runners": [
                "ZZ000081",
                "ZZ000053",
                "ZZ000080",
                "ZZ000054"
            ],
            "team_id": "ESP"
        }
    ],
    "teams": [
        {
            "team_id": "ALB",
            "team_name": "ALBANIA"
        },
        {
            "team_id": "ALG",
            "team_name": "ALGERIA"
        }
    ],
    "events": [
        {
            "age_groups": [
                "ALL"
            ],
            "event_id": "003",
            "event_code": "100",
            "name": "100 metres MEN",
            "genders": "M",
            "category": "M",
            "rounds": "2,1",
            "units": [
                {
                    "status": "finished",
                    "heat": 1,
                    "round": 1,
                    "results_status": "official",
                    "results": [
                        {
                            "place": 1,
                            "points": 0,
                            "bib": "ZZ000192",
                            "performance": "10.37"
                        },
                        {
                            "place": 2,
                            "points": 0,
                            "bib": "GJ002321",
                            "performance": "10.48"
                        },
                        {
                            "place": 3,
                            "points": 0,
                            "bib": "ZZ000110",
                            "performance": "10.48"
                        },
                        {
                            "place": 4,
                            "points": 0,
                            "bib": "ZZ000054",
                            "performance": "10.54"
                        },
                        {
                            "place": 5,
                            "points": 0,
                            "bib": "ZZ000003",
                            "performance": "10.70"
                        }
                    ]
                },
                {
                    "status": "finished",
                    "heat": 2,
                    "round": 1,
                    "results_status": "official",
                    "results": [
                        {
                            "place": 1,
                            "points": 0,
                            "bib": "ZZ000109",
                            "performance": "10.26"
                        },
                        {
                            "place": 2,
                            "points": 0,
                            "bib": "UB006913",
                            "performance": "10.47"
                        },
                        {
                            "place": 3,
                            "points": 0,
                            "bib": "ZZ000053",
                            "performance": "10.53"
                        },
                        {
                            "place": 4,
                            "points": 0,
                            "bib": "ZZ000218",
                            "performance": "10.55"
                        },
                        {
                            "place": 5,
                            "points": 0,
                            "bib": "ZZ000004",
                            "performance": "10.56"
                        },
                        {
                            "place": 6,
                            "points": 0,
                            "bib": "ZZ000250",
                            "performance": "10.66"
                        }
                    ]
                },
                {
                    "status": "finished",
                    "heat": 1,
                    "round": 2,
                    "results_status": "official",
                    "results": [
                        {
                            "place": 1,
                            "points": 0,
                            "bib": "ZZ000109",
                            "performance": "10.07"
                        },
                        {
                            "place": 2,
                            "points": 0,
                            "bib": "ZZ000192",
                            "performance": "10.37"
                        },
                        {
                            "place": 3,
                            "points": 0,
                            "bib": "ZZ000110",
                            "performance": "10.47"
                        },
                        {
                            "place": 4,
                            "points": 0,
                            "bib": "ZZ000218",
                            "performance": "10.48"
                        },
                        {
                            "place": 5,
                            "points": 0,
                            "bib": "GJ002321",
                            "performance": "10.48"
                        },
                        {
                            "place": 6,
                            "points": 0,
                            "bib": "ZZ000054",
                            "performance": "10.57"
                        },
                        {
                            "place": 7,
                            "points": 0,
                            "bib": "ZZ000053",
                            "performance": "10.62"
                        },
                        {
                            "place": 8,
                            "points": 0,
                            "bib": "UB006913",
                            "performance": "10.90"
                        }
                    ]
                }
            ]
        }
    ]
}

Upload results afterwards

If the competition is already in our database, it is possible to upload the results through our update endpoint, as in the example.

import requests
import json

EMAIL = '<your email>'
PASSWORD = '<your password>'

r = requests.post(
    "https://api.opentrack.run/api/v1/auth/token", 
    data={"username": EMAIL, "password": PASSWORD})

resp = r.json()

token = resp["access_token"]

headers = {'Authorization': 'Bearer %s' % token}

update_comp = {
    "competitors": [
        {
            "age_group": "PM",
            "category": "PM",
            "competitor_id": "ZZ000002",
            "date_of_birth": "1998-08-19",
            "events_entered": [
                {
                    "event_code": "200",
                    "event_id": "004"
                },
                {
                    "event_code": "400",
                    "event_id": "006"
                }
            ],
            "first_name": "name",
            "gender": "M",
            "last_name": "surname",
            "team_id": "ITA"
        },
        {
            "age_group": "JM",
            "category": "JM",
            "competitor_id": "ZZ000011",
            "date_of_birth": "1999-10-08",
            "events_entered": [
                {
                    "event_code": "10KW",
                    "event_id": "043"
                },
                {
                    "event_code": "10KW",
                    "event_id": "243"
                }
            ],
            "first_name": "name2",
            "gender": "M",
            "last_name": "surname2",
            "team_id": "ALG"
        }
    ],
    "relay_teams": [
        {
            "event_id": "047",
            "name": "ALGERIA",
            "relay_team_id": "ALGA",
            "runners": [
                "ZZ000004",
                "ZZ000008",
                "ZZ000005",
                "ZZ000003"
            ],
            "team_id": "ALG"
        },
        {
            "event_id": "047",
            "name": "SPAIN",
            "relay_team_id": "ESPA",
            "runners": [
                "ZZ000081",
                "ZZ000053",
                "ZZ000080",
                "ZZ000054"
            ],
            "team_id": "ESP"
        }
    ],
    "teams": [
        {
            "team_id": "ALB",
            "team_name": "ALBANIA"
        },
        {
            "team_id": "ALG",
            "team_name": "ALGERIA"
        }
    ],
    "events": [
        {
            "age_groups": [
                "ALL"
            ],
            "event_id": "003",
            "event_code": "100",
            "name": "100 metres MEN",
            "genders": "M",
            "category": "M",
            "rounds": "2,1",
            "units": [
                {
                    "status": "finished",
                    "heat": 1,
                    "round": 1,
                    "results_status": "official",
                    "results": [
                        {
                            "place": 1,
                            "points": 0,
                            "bib": "ZZ000192",
                            "performance": "10.37"
                        },
                        {
                            "place": 2,
                            "points": 0,
                            "bib": "GJ002321",
                            "performance": "10.48"
                        },
                        {
                            "place": 3,
                            "points": 0,
                            "bib": "ZZ000110",
                            "performance": "10.48"
                        },
                        {
                            "place": 4,
                            "points": 0,
                            "bib": "ZZ000054",
                            "performance": "10.54"
                        },
                        {
                            "place": 5,
                            "points": 0,
                            "bib": "ZZ000003",
                            "performance": "10.70"
                        }
                    ]
                },
                {
                    "status": "finished",
                    "heat": 2,
                    "round": 1,
                    "results_status": "official",
                    "results": [
                        {
                            "place": 1,
                            "points": 0,
                            "bib": "ZZ000109",
                            "performance": "10.26"
                        },
                        {
                            "place": 2,
                            "points": 0,
                            "bib": "UB006913",
                            "performance": "10.47"
                        },
                        {
                            "place": 3,
                            "points": 0,
                            "bib": "ZZ000053",
                            "performance": "10.53"
                        },
                        {
                            "place": 4,
                            "points": 0,
                            "bib": "ZZ000218",
                            "performance": "10.55"
                        },
                        {
                            "place": 5,
                            "points": 0,
                            "bib": "ZZ000004",
                            "performance": "10.56"
                        },
                        {
                            "place": 6,
                            "points": 0,
                            "bib": "ZZ000250",
                            "performance": "10.66"
                        }
                    ]
                },
                {
                    "status": "finished",
                    "heat": 1,
                    "round": 2,
                    "results_status": "official",
                    "results": [
                        {
                            "place": 1,
                            "points": 0,
                            "bib": "ZZ000109",
                            "performance": "10.07"
                        },
                        {
                            "place": 2,
                            "points": 0,
                            "bib": "ZZ000192",
                            "performance": "10.37"
                        },
                        {
                            "place": 3,
                            "points": 0,
                            "bib": "ZZ000110",
                            "performance": "10.47"
                        },
                        {
                            "place": 4,
                            "points": 0,
                            "bib": "ZZ000218",
                            "performance": "10.48"
                        },
                        {
                            "place": 5,
                            "points": 0,
                            "bib": "GJ002321",
                            "performance": "10.48"
                        },
                        {
                            "place": 6,
                            "points": 0,
                            "bib": "ZZ000054",
                            "performance": "10.57"
                        },
                        {
                            "place": 7,
                            "points": 0,
                            "bib": "ZZ000053",
                            "performance": "10.62"
                        },
                        {
                            "place": 8,
                            "points": 0,
                            "bib": "UB006913",
                            "performance": "10.90"
                        }
                    ]
                }
            ]
        }
    ]
}

comp_id = "868aadc8-f9bb-49bc-952f-25c16eadb69e"
r = requests.post(
    f"https://api.opentrack.run/api/v1/competitions/{comp_id}", 
    data=json.dumps(update_comp),
    headers=headers
    )

Query, Update, Delete a known competition

If you know the id of a competition, you can interact directly with the competition.

Query a competition

import requests

EMAIL = '<your email>'
PASSWORD = '<your password>'

r = requests.post(
    "https://api.opentrack.run/api/v1/auth/token", 
    data={"username": EMAIL, "password": PASSWORD})

resp = r.json()

token = resp["access_token"]

headers = {'Authorization': 'Bearer %s' % token}

comp_id = "868aadc8-f9bb-49bc-952f-25c16eadb69e"
r = requests.get(
    f"https://api.opentrack.run/api/v1/competitions/{comp_id}", 
    headers=headers
    )

Update a competition

import requests
import json

EMAIL = '<your email>'
PASSWORD = '<your password>'

r = requests.post(
    "https://api.opentrack.run/api/v1/auth/token", 
    data={"username": EMAIL, "password": PASSWORD})

resp = r.json()

token = resp["access_token"]

headers = {'Authorization': 'Bearer %s' % token}

update_comp = {
    'full_name': 'new_name'
}

comp_id = "868aadc8-f9bb-49bc-952f-25c16eadb69e"
r = requests.post(
    f"https://api.opentrack.run/api/v1/competitions/{comp_id}",
    data=json.dumps(update_comp),
    headers=headers
    )

Delete a competition

import requests

EMAIL = '<your email>'
PASSWORD = '<your password>'

r = requests.post(
    "https://api.opentrack.run/api/v1/auth/token", 
    data={"username": EMAIL, "password": PASSWORD})

resp = r.json()

token = resp["access_token"]

headers = {'Authorization': 'Bearer %s' % token}

comp_id = "868aadc8-f9bb-49bc-952f-25c16eadb69e"
r = requests.delete(
    f"https://api.opentrack.run/api/v1/competitions/{comp_id}", 
    headers=headers
    )

Test hardness endpoint

We provide a test endpoint to check whether your input respects our competition schema. The API call does not create a database entry, so feel free to use it intensively.

import requests
import json

EMAIL = '<your email>'
PASSWORD = '<your password>'

r = requests.post(
    "https://api.opentrack.run/api/v1/auth/token", 
    data={"username": EMAIL, "password": PASSWORD})

resp = r.json()

token = resp["access_token"]

headers = {'Authorization': 'Bearer %s' % token}

comp = {
    'full_name': 'Trond Mohn Games',
    'short_name': 'TMG',
    'slug': 'trond-mohn-games',
    'name_local': 'Trond Mohn Games',
    'country_id': 'NOR',
    'address': 'Grimseidvegen 60, 5239 Rådal, Norge',
    'city': 'Bergen',
    'venue_id': 'c3c87364-0ab4-44b5-a518-b143f574c086',
    'latitude': 60.2905,
    'longitude': 5.3178,
    'altitude': 60,
    'date': '2021-05-29',
    'finish_date': '2021-05-29',
    'wa_rankings_category_id': None,
    'age_groups': ['Men, women'],
    'national_id': 'NOR',
    'basic_description': '',
    'contact_details': 'Dag Rydland drydland@broadpark.no',
    'organiser_id': 'e893fa86-8c2e-4f99-a9f1-b6fcfb0a787c',
    'website': 'https://www.trondmohngames.no',
    'entry_link': 'http://www.trondmohngames.no',
    'results_link': 'http://en.trondmohngames.no/live-results/'
}

r = requests.post(
    "https://api.opentrack.run/api/v1/competitions/test", 
    data=json.dumps(comp),
    headers=headers
    )