Use API with PowerShell

Since the whole ‘COVID-era’, I, like most of us IT-folk, work a lot more from home and only go to the office 1 or 2 days a week. When working from home I usually go to the gym around the end of the morning. It is around 4.8 kilometer from my house and I try to go by bicycle whenever I can (I am Dutch after all), so I do my warmup on the go. And when I say bicycle, I mean my old 90’s Trek Mountainbike which may or may not fall apart while riding it.

Old mountainbike

Why do I go to the gym at all? A bit of discipline, a bit of staying in shape, a bit of self-confidence. For every bike-ride over there I turn on the Sports-Tracker app to log the distance and time it takes. Looking at the results I was wondering how many kilometers I have done these last three years. Now of course I can use the app or the website for that, but where is the fun in that if I might be able to do it through an API.

This blog will describe how I found out how the API works an how you can grab the data from PowerShell.

To document the API or not to document the API
The first thing to do in these cases is to use Google to find out if there even is a public API. The support page of the website in question is also a good place to look. A lot of websites actually have pretty well documented API’s, but sadly Sports-Tracker is not one of them.
I managed to find a GitHub page with a Python script to export the GPX files. GPS Exchange Format files are XML files for each route with all the waypoints. You can import these in other applications to use the route for further analysis for example. But this isn’t what I wanted, since I’m only after the overall data for each trip (average speed, maximum speed, the amount of time it took and the distance).
I also managed to find a short blog that uses JavaScript to grab the data, but I want to keep my sanity, so that is not an option either. That one also required to put all the routes on the screen and then run the script from the browser developer console (so you are just grabbing the data from the browser screen rather than directly getting it from the source).

So it was up to me to see if I can grab the data. Let’s look at the Chrome developer console when looking up my previous trips.
I go the the Sports-Tracker dashboard, open the developer console (F12), click on the Network-tab and click on My Workouts. If you don’t see any data, just refresh the page and try again.
This should give you something like this:

Sports-Tracker screenshot 1
Every connection that is made to create this website is showed here. The images, the website icon, the data ,etc. Everything that you see on the website was grabbed by one of the connections you see here.

Most of these requests/connections can roughly be divided into two methods: GET and POST. GET gets something (captain obvious being obvious), POST sends something.
Also there must be some kind of authentication, but it’s not an option to ask for authentication on every page. Usually an API key (or token) is used for this. This API key is a long code that identifies and authenticates the user.
Even on websites that are free and don’t require the creation of an account, API keys are used in the background to block access to the API from third-parties (for performance reasons).

Developers, developers, developers (console)
Next up is to find the actual API request to get all the trips I made with Sports-Tracker. To find that API request you need to find a page that access some of your workouts. In this example I went to the ‘Diary’ and after that I chose the option ‘List’.

This page shows all my activities with Sports-Tracker. So to get that information the website performed an API request to get that data. Now we need to find the correct request. To do that we need to go back to the Network-tab in the developer console. If isn’t populated, just refresh the page with the list of workouts.

But which connection is the one grabs the data you are looking for? This is where it gets a bit finicky since not every website and API work the same way. So it’s just trial-and-error. Luckily this page isn’t too complex and I rather quickly found the one I was looking for (the selected one in the image above). How did I find it? This one doesn’t mention a picture, it mentions my workouts and includes filters in the URL.

Let’s take a closer look at it:

To access this information with PowerShell we are looking for two things:
1. What URL should we use to access the data?
2. What information should we put in the header (or rather, in what way should we authenticate?)

The first one is rather easy, since it shown in the first line.
We just need to mind the parameters after the question mark. Usually these are ok, but sometimes you need to edit these so it meets your needs.
This one has limited=true in the address, which I believe limits the output of the request, so I’m deleting that from the URL.

The second one is a bit more difficult (or less easy). Under request headers you can see every line that was included in the request. Usually you won’t need all of these. But you do need to add some type authorization to let the web server on the other end know who you are (or claim to be). In this case most headers are pretty arbitrary settings, except for Sttauthorization. This looks like something that could be an API key or token.

Bring out your code editors
Now you seem to have everything you need. Let’s test this in PowerShell.
I will try to explain every step I took.
Most of the time I use Visual Studio Code when scripting.
This is because VSCode clearly shows when you are doing something wrong or when your code could be better, so I highly recommend it.
But if you want to use something different (PowerShell ISE for example), that’s fine too.

Firstly, I put the token and the URL in variables so I can call them easily.
(And yes, I have edited the token you see here, my actual token is different.)

$Token = "4o5eamhl7a4k8cue7vk14fappk4j6vq2"
$URL = ""

Secondly, I need to setup the header. Since I think I only need the authorization and nothing else, I’m only including that.
Ensure that you setup the headers exactly the same as it’s shown in the developer console.

$Header = @{
    "Sttauthorization" = $Token

This should be enough to try the request. So let’s do it:

Invoke-WebRequest -Uri $URL -Headers $Header

StatusCode        : 200
StatusDescription : OK
Content           : {"error":null,"payload":[{"username":"chrisjeucken","sharingFlags":17,"activityId":2,"key":"1a935kr5m92mgjmo","workoutName":"07/ 
                    02/2024 11:13","startTime":1707300780107,"stopTime":1707309689979,"total...
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Connection: keep-alive
                    Request-Context: appId=cid-v1:834a0515-0f98-4df5-838e-6681b26dea96
                    Vary: Accept-Encoding, User-Agent
                    Strict-Transport-Security: m...
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Connection, keep-alive], [Request-Context, appId=cid-v1:834a0515-0f98-4df5-838e-6681b26dea96],   
                    [Vary, Accept-Encoding, User-Agent]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 1137873

This gives some feedback, so it should be working, but this is a lot of data of which most you don’t need or want.
But if you look in the content field you see times and distances, so let’s focus on that.
It also looks like JSON data and we need to take that into account to properly use it.

(Invoke-WebRequest -Uri $URL -Headers $Header).Content | ConvertFrom-Json

error payload
----- -------
      {@{activityId=2; startTime=1707300780107; totalTime=1642.84; totalDistance=9748.13; totalAscent=198.7; totalDescent=167.7; startPosition=; ...

It seems we need to drill down to the payload field:

((Invoke-WebRequest -Uri $URL -Headers $Header).Content | ConvertFrom-Json).Payload

username               : chrisjeucken
sharingFlags           : 17
activityId             : 2
key                    : 1a935kr5m92mgjmo
workoutName            : 07/02/2024 11:13
startTime              : 1707300780107
stopTime               : 1707309689979
totalTime              : 1642.84
totalDistance          : 9748.13
totalAscent            : 198.7
totalDescent           : 167.7
startPosition          : @{x=4.675821666666667; y=52.14969166666667}
stopPosition           : @{x=4.675675; y=52.149835}
centerPosition         : @{x=4.660395; y=52.13629}
maxSpeed               : 13.67
---- (I have cut the results here to show mercy to your scroll button/wheel) ----

That seems to be the data I am looking for.
Let’s put it into an array and check how many trips it found:

$Results = ((Invoke-WebRequest -Uri $URL -Headers $Header).Content | ConvertFrom-Json).Payload

The request grabbed 432 workouts, which matches the amount shown under Diary -> List.
Since it’s an PowerShell array you can edit this data in any way you like.

Getting rid of excess baggage
Most of the time I use Sports-Tracker for cycling, but I have also used it for some short hiking routes. In this case I’m only interested in the cycling trips. But which field handles the workout type? If I look at one result ($Results | Select-Object -First 1) I suspect the activityId value handles this. Let’s count each instance to be sure:

$Results | Group-Object -Property activityId

Count Name                      Group
----- ----                      -----
  383 2                         {@{username=chrisjeucken; sharingFlags=17; activityId=2; key=1a935kr5m92mgjmo; workoutName=07/02/2024 11:13; star... 
   13 11                        {@{username=chrisjeucken; sharingFlags=17; activityId=11; key=6fse8bqflefroiqg; workoutName=17/12/2023 14:00; sta... 
    4 1                         {@{username=chrisjeucken; sharingFlags=17; activityId=1; key=eaoeoc0dddvh1qo3; workoutName=12/03/2023 12:13; star...
   25 0                         {@{username=chrisjeucken; sharingFlags=17; activityId=0; key=9rcrff9olqc34pbv; workoutName=3/8/15 4:27 PM; startT... 
    1 15                        {@{username=chrisjeucken; sharingFlags=0; activityId=15; key=21kmbln5r3i4eduj; workoutName=6/22/19 14:02; startTi... 
    1 49                        {@{username=chrisjeucken; sharingFlags=0; activityId=49; key=8i1a9eo2tpomjl6k; workoutName=1/12/19 09:51; startTi... 
    2 26                        {@{username=chrisjeucken; sharingFlags=0; activityId=26; key=fd60m2oo0nn8hvgm; workoutName=11/16/18 18:37; startT... 
    2 4                         {@{username=chrisjeucken; sharingFlags=0; activityId=4; key=f8poppmqoioqam4r; workoutName=1/28/17 10:59; startTim... 
    1 13                        {@{username=chrisjeucken; sharingFlags=0; activityId=13; key=o553d0ajd1mf583l; workoutName=10/19/14 4:49 AM; star...

There are 383 workouts which have ‘2’ as activityId, so I’m guessing this is it.
What are the other ones? No need to find out now, but you can always use the workoutName value (that includes the date and time) to find it on the Sports-Tracker website and check what kind of activity it was.
Let’s grab every workout with activityId 2 and drop the rest:

$Results = $Results | Where-Object {$_.activityId -eq 2}

If I look at fields like maxSpeed and avgSpeed it seems the unit of measurement is different. After some comparing with the actual shown results on the website it looks these are in meters per second. Also, the distances are in meters and not kilometers. So let’s take that into account when calculating.

Calculate the calculations, while calculating
Now that I have the data, know which is relevant and what units are used, let’s finish this up.
I was looking for the total distance covered from my house to the gym, and it would also be nice to get my average speed.
But since not all trips are to the gym, some filtering is needed. Now I could use the startPosition but this isn’t exactly the same every time I start.
Since I’m lazy , I will just use the total distance then: everything between 9.5 and 11 kilometers should be the trips I’m looking for (some will be different trips but I’m not looking for exact figures here).

$TargetTrips = $Results | Where-Object {($_.totalDistance -gt 9500.00) -and ($_.totalDistance -lt 11000.00)}

How to make the calculations? The Measure-Object cmdlet is the answer. This one is often overlooked while it can be so powerful. And it’s also really easy to use:
Total distance (add all totalDistance values and divide by 1000 to get kilometers):

($TargetTrips.totalDistance | Measure-Object -Sum).Sum / 1000

Average speed (get an average of all avgSpeed values and multiply by 3.6 to go from meters per second to kilometers per hour):

($TargetTrips.avgSpeed | Measure-Object -Average).Average * 3.6

Maximum speed (get the highest number of the maxSpeed values and multiply by 3.6 to go from meters per second to kilometers per hour):
Note: I’m also filtering anything that seems superhuman or a GPS glitch.

$HighestSpeedAll = (($TargetTrips | Where-Object {$_.maxSpeed -lt 16.00}).maxSpeed | Measure-Object -Maximum).Maximum * 3.6

(Could still be a GPS glitch though)

Wrapping up and slacking off
Of course there are more possible calculations that are interesting, but this blog post is a bit long already.
I will include more variations in the overal script that is shared below. And no, I’m not including altitudes since these were trips in Holland. We don’t do elevations.

I hope this was an interesting read and shows you how simple some of these API’s are to use, even if they are undocumented.

DISCLAIMER: Once again: I’m in no way an expert PowerShell scripter, so it might not be the most pretty or efficient code, but it gets the job done. If you are looking for better (PowerShell) code, contact Guy Leech, although he might be busy being angry at me for using ‘Write-Host’.

NOTE TO SPORTS-TRACKER.COM: The API key/token doesn’t seem to change over time or when you re-authenticate. So if it’s compromised the only way out is changing your password. This seems like something you might need to address.

The complete script:

# SCRIPT INFO -------------------
# --- Get workouts from Sports Tracker ---
# By Chris Jeucken
# v0.1
# -------------------------------

# VARIABLES ---------------------
    $Token = "d55ew8s1d3cj78wzy9d3vuebssl0v0ek"
    $URL = ""
    $ActivityID = 2
# -------------------------------

# HEADERS -----------------------
    $Header = @{
        "Sttauthorization" = $Token
# -------------------------------

# SCRIPT ------------------------
# Grab all workouts
    $Results = ((Invoke-WebRequest -Uri $URL -Headers $Header).Content | ConvertFrom-Json).Payload

# Grab only trips with specified Activity ID
    $ResultsActID = $Results | Where-Object {$_.activityId -eq $ActivityID}

# Grab all trips with a total distance between 9500 and 11000 meters
    $TargetTrips = $ResultsActID | Where-Object {($_.totalDistance -gt 9500.00) -and ($_.totalDistance -lt 11000.00)}
    Write-Host $TargetTrips.Count "workouts found with Activity ID $ActivityID and a total distance between 9500 and 11000 meters"

# Calculate total distance of all trips combined (in kilometers)
    $TotalDistanceAll = ($TargetTrips.totalDistance | Measure-Object -Sum).Sum / 1000
    Write-Host "Total distance of all trips combined: $TotalDistanceAll kilometers"

# Calculate average speed over all trips (in kilometers per hour)
    $AverageSpeedAll = ($TargetTrips.avgSpeed | Measure-Object -Average).Average * 3.6
    Write-Host "Average speed over all trips: $AverageSpeedAll km/h"

# Calculate average energy consumption (in kcal)
    $EnergyConsumptionAll = ($TargetTrips.energyConsumption | Measure-Object -Sum).Sum
    Write-Host "Total energy consumption: $EnergyConsumptionAll kcal"

# Get highest speed (in kilometers per hour and filtering everything that seems superhuman or a GPS glitch)
    $HighestSpeedAll = (($TargetTrips | Where-Object {$_.maxSpeed -lt 16.00}).maxSpeed | Measure-Object -Maximum).Maximum * 3.6
    Write-Host "Maximum speed: $HighestSpeedAll km/h (can still be a GPS glitch)"

# Calculate the total time on the bicycle (in hours/minutes/seconds)
    $TotalTimeAll = ($TargetTrips.totalTime | Measure-Object -Sum).Sum
    $TotalTimeConvert = ([TimeSpan]::fromseconds($TotalTimeAll)).ToString("dd\:hh\:mm\:ss")
    Write-Host "Total time on the mountainbike: $TotalTimeConvert (days:hours:minutes:seconds)"
# -------------------------------


Leave a Reply

Your email address will not be published. Required fields are marked *