Dive into Västtrafik public (+ not so public!) APIs

Having recently moved to Gothenburg, I was super impressed by the public transport App Västtrafik To Go; by far one of the best public transport apps I’ve seen. One nice feature is the real-time tracking of arrivals and departures, particularly comforting on a bleak mid-January morning waiting for a bus in that special type of cold that you only find on the west coast of Sweden (…and maybe Scotland).

However, In my specific use-case, I wanted to see multiple vehicles simultaneously – a kind of nerdy obsession with optimisation problems. Unfortunately this is not possible using the ‘departures’ tab of the Västtrafik To Go app, which only allows users to track a single service journey at a time.

After some cursory research, I discovered that Göteborgs Stad is all-in on open data and provides developer access to a wide range of its public transport APIs. One particular endpoint: /v4/positions, looked like it might solve all my problems in single API call, promising to give a position on all vehicles within a bounding box.

66c9d1f3230a57216e84a3b697d6516f_MD5.jpeg

Sadly, upon completing a mobile-based proof of concept, I discovered that this API is not actual ‘live locations’, but instead estimated positions – as was dutifully pointed out in the docs at Västtrafik – Utvecklarportalen (had I spent the time to read them!).


In real-world usage, the positions given by this API are far enough out, that they are not that useful. This was curious however, because I know for a fact that the mobile app allows users actual realtime positioning of a given vehicle.

Västtrafik’s (not-so) public API

This led to a bit of a deep dive into android dev tools, APKs, APIs and authentication practises. Using some simple tools, we can examine API endpoints used by the mobile app. At the time of writing, Västtrafik public Planera Resa v4 API is on v4 however, we notice that the app makes calls to another unpublished endpoint also at: /fpos/v1/positions/{gid}.

By running the APK in an emulator, we can interlope on its API-calls using a simple Fridahook. Inspecting the outgoing http request headers, we can observe the Client ID and Client Secret and from that generate an access token for OAuth2.0 authentication.

CleanShot 2025-03-29 at 22.33.46.png

In the first instance this was enough, and we would get a response similar to:

[
    {
        "journeyId": "9015014500605801",
        "atStop": false,
        "lat": 57.70742333333333,
        "long": 11.97174,
        "speed": 7.908040000000052,
        "updatedAt": "2025-03-29T21:44:10.2222172Z",
        "dataStillRelevant": true
    }
]

How do we find journeyId?

As ever, nothing is straightforward! In fact, to get journeyId (aka gid) you must interrogate the two other API endpoints.

Firstly, using /pr/v4/positions, with params as laid out in the docs. After which, we receive back a response (as per schema), this yields us a detailsReference

…Then, we can plug this into the endpoint: /pr/v4/journeys/{detailsReference}/details which yields us tripLegs -> serviceJourneys -> gid (as per schema). This gid represents a journeyId or rather, a vehicle on a journey, for which we can request the real-time information from v1-da/positions endpoint.

Now, this was the information we wanted – Real-time positions! However…

Updated endpoint; updated authentication

While this worked perfectly during initial exploration of the hidden endpoints mid-2024, upon picking this back up more recently I soon realised that authentication had been updated. Going through same process once again uncovered that the endpoint was modified (now: ext-api.vasttrafik.se/fpos/v1-da/positions/:gid) and this time used an alternative means of security.

Looking at the loaded java classes of the APK, it became apparent that the authentication now relied on a TOTP (Time-based one-time password). These work much like the 6-digit authentication codes we commonly see used with 2FA, like Google Authenticator – a one-time code that expires periodically (typically every 30s).

The common challenge for app developers in this situation is how to keep secrets secret, when ultimately they must be used by the app itself. This case was no different and the code and accompanying seed secret, used for generating this TOTP is found hard-coded into the APK (true as of Västtrafik To Go v.4.45.0).

Having recreated the code in a python one-liner, we can now run substituting in the seed value as follows:

python3 -c "import pyotp, base64, hashlib; secret=base64.b64decode(<<Base64Seed>>); print(pyotp.TOTP(base64.b32encode(secret).decode(), interval=30, digits=6, digest=hashlib.sha512).now())"

We can now simply plug this into the request header and so long as we update this every 30s, we can successfully interrogate the new endpoint. With a little testing, we see that rate-limiting kicks in when excessively called.

curl --location 'https://ext-api.vasttrafik.se/fpos/v1-da/positions/?journeyIds=9015014500605852' \
--header 'x-totp-code: 579140' \
--header 'Host: ext-api.vasttrafik.se' \

Conclusion

So we have successfully demonstrated recreating the means by which the Västtrafik To Go app gets real-time vehicle information. In future I would like to wrap this up into an actual real-time vehicle location service, from which we might also extrapolate some interesting data like: punctuality scores, total milage per year or fastest tram. For now though, I’m satisfied to have gotten to the bottom of this, in the process exploring: authentication protocols, code obfuscation and using API testing tools such as postman

Maintaining the integrity of public-serving APIs appears to be a tricky balance of accessibility and security. A simple API like this doesn’t need over-engineered security measures but keeping just enough of a deterrent to avoid abuse is smart. Please let me know if anything here was helpful to you – I’ll be more than happy to discuss!

Disclaimer: This article is for educational purposes only. The author does not encourage or condone misuse of public APIs. Exploring hidden APIs should always be done responsibly, respectfully and ethically.

Scroll to Top