Reverse Engineering the TTC API
How I reverse engineered the Toronto Transit Commission's internal API
Hi! I wrote this blog post at 4am, and am currently very sleep deprived. I will finish and properly edit and clean it up later (when i feel like it) For now, I've told gpt-5 to fix a couple grammar issues and whatever, have fun reading :)
The spark
It's Nov 19, and I'm procrastinating studying for exams by scrolling Twitter. Over the last week, I've been seeing this LED matrix display with bus times (Tidbyt) getting some traction.
Then I see this tweet:
(TTC soon)
It can't be that hard to reverse engineer the TTC API, right? That thought threw me down the rabbit hole.
The obvious search
I started with a quick Google search, because someone's probably already done it, right? Nope. The only result was https://myttc.ca/developers, a nearly twenty‑year‑old project that doesn't seem to work anymore.
Pretty funny.
Poking around TTC and Bustime
So I started poking around the TTC website, recording and analyzing its network traffic. My search also surfaced https://bustime.ttc.ca/, which looked promising.

However, when I tried to call one of its API endpoints, I was denied access.

After a bit more Googling, I found https://bustime.ttc.ca/gtfsrt/ (GTFS‑RT = General Transit Feed Specification Real‑Time). It looked exactly like what I wanted.
I spun up a quick parser using protobuf and a couple of libraries, and I got some data out of it.
I then realized the GTFS‑RT data was incomplete and didn't include much of the data I needed (street names, bus lines, etc.).
The TTC routedetail endpoints
So I started digging through ttc.ca again.

I found https://www.ttc.ca/ttcapi/routedetail/GetNextBuses?routeId=510&stopCode=7347, looks like it's going to be a hit!

Nope—still not the fine‑grained data I wanted.
What about https://www.ttc.ca/ttcapi/routedetail/get?id=310?

That's more like it! I got something like this:
z.object({
message: z.string().nullable(),
is10MinutesNetwork: z.boolean(),
frequency: z.string().nullable(),
serviceLevel: z.object({
name: z.string().nullable(),
description: z.string().nullable(),
color: z.string().nullable(),
cssClassDashboard: z.string(),
cssClassConnectingBus: z.string(),
}),
mapUrl: z.string().nullable(),
direction: z.number(),
inService: z.boolean(),
helpInfo: z.string().nullable(),
legendInfo: z.string().nullable(),
id: z.number(),
gtfsId: z.string(),
agencyId: z.number(),
agency: z.string().nullable(),
shortName: z.string(),
longName: z.string(),
description: z.string(),
type: z.number(),
url: z.string(),
colour: z.string().nullable(),
textColour: z.string().nullable(),
active: z.boolean(),
trips: z.array(z.unknown()),
});... and https://www.ttc.ca/ttcapi/routedetail/bystopcode?stopcode=7347 returns the array of routes serving that stop.
The missing piece: predictions
So I should have everything I need to build the API, right? Right? ... Nope.
I still needed time estimates for each route at each stop. /GetNextBuses technically does that, but it's not very detailed nor (in my opinion) "safe" to parse. The GTFS‑RT vehicles endpoint lists vehicles, but I'd need to match it with the GTFS‑RT trips endpoint to estimate times. That felt too complicated and inefficient.
My mind kept wandering back to the Bustime website. If I could break its auth, I could get everything I wanted in one place.
Breaking Bustime's auth
Back on bustime.ttc.ca, I started by inspecting the call stack for an API call.
I quickly found this interesting snippet:
convertUrl(k) {
let G = j.N.serverUrl;
k.includes("restricted=true") ? G += "api/restricted/v1/" : G += "api/v3/";
const $ = "requestType=";
let K = k.substring(k.indexOf($) + $.length);
const ce = K.indexOf("&");
return K = K.substring(0, ce < 0 ? K.length : ce),
G += K + k + "&key=Qskvu4Z5JDwGEVswqdAVkiA5B&format=json", // what's this?!
G
}No... it can't be... a HARDCODED API KEY?!
LMFAO
But why, when I try https://bustime.ttc.ca/bustime/api/v3/getpredictions?...key=Qskvu4Z5JDwGEVswqdAVkiA5B..., it doesn't work?
After poking around some more, I found this:
intercept(o, t) {
let S = o;
return this.isGetTimeCall(S) || this.devAPIService.getLocalSynchronizedServerTime().subscribe(oe => {
const ye = oe.toUTCString();
S = S.clone({
headers: S.headers.set("X-Date", ye)
});
const Ue = this.getApiEndpoint(S)
, qe = this.hashData(Ue + ye, "SHA256");
S = S.clone({
headers: S.headers.set("X-Request-ID", qe.toString())
})
}
),
t.handle(S)
}
// ...
hashData(o, t) {
const _ = ap["Hmac" + t];
return _ ? _(o, "ZSqCAFdU7bwxHJUHKYfQUxKin06hMxCK").toString(ap.enc.Hex) : "" // oh no not again
}Hmm... what's this? Another hardcoded key? Yup... it's hashing the URL + date to get a request ID with a hardcoded secret key. lmao
After asking an LLM to help me generate some code to sign the requests:

It works! I got some data out of it!
A note on Bustime and Clever Devices
After a quick search for Qskvu4Z5JDwGEVswqdAVkiA5B to see if anyone else had found it, I landed on the only result:

Unpacking Clever Devices' BusTracker by Jack Weilage (worth a read!)
It turns out Bustime is a product sold by Clever Devices that allows transit agencies to track vehicle stats, etc. And they use the same hardcoded API key across agencies.
Epic
What's next
I'm packaging this into a free, unofficial TTC arrivals API. I'll update this blog post soon-ish with the details when it's done.