sg-bus-api

Live Singapore bus arrival times, from LTA DataMall. Built for BTOlab; source on GitHub.

Try it

Enter a 5-digit bus stop code (it is printed on the bus stop pole).

Endpoints

RequestReturns
GET /arrivals/{stop}Next 3 buses for every service at a 5-digit stop, e.g. /arrivals/52031
GET /health{"ok": true} — for uptime checks; does not call LTA

Response

{
  "stop": "52031",
  "at": "2026-10-16T21:30:05+08:00",
  "services": [
    {"no": "88", "arrivals": ["2026-10-16T21:33:40+08:00", "2026-10-16T21:41:02+08:00", "2026-10-16T21:52:19+08:00"]},
    {"no": "157", "arrivals": ["2026-10-16T21:30:49+08:00"]}
  ]
}

at is the server time; work out minutes as arrival − at so a wrong device clock doesn't matter. Services are sorted by number; a service with no bus due is left out. Times are Singapore time (+08:00).

Examples

From a terminal:

curl https://sg-bus-api.vercel.app/arrivals/52031

From JavaScript (server-side, or a browser page on an allowed site):

const r = await fetch('https://sg-bus-api.vercel.app/arrivals/52031');
const { at, services } = await r.json();
for (const s of services) {
  const mins = Math.max(0, Math.floor((Date.parse(s.arrivals[0]) - Date.parse(at)) / 60000));
  console.log(s.no, mins ? mins + ' min' : 'Arr');
}

From Python:

import httpx
print(httpx.get("https://sg-bus-api.vercel.app/arrivals/52031").json())

Errors

StatusBodyMeaning
400{"error": "no query"}Query strings aren't accepted (they would bypass the cache)
422{"error": "invalid stop"}The stop code isn't 5 digits
502{"error": "upstream"}LTA didn't answer in time or answered badly; try again shortly
500{"error": "config"}The server's LTA key isn't set

Good to know

Answers are cached for 20 seconds, so polling faster than every 20–30 s gains nothing. Browsers can call the API only from allowed sites (CORS); servers and scripts can call it from anywhere. Please be gentle: requests are rate-limited.

Contains information from LTA DataMall, made available under the Singapore Open Data Licence.