Unofficial · MIT licensed · TypeScript

Malaysia's rail network,
one library away.

Stations, schedules, fares, Komuter timetables, and live vehicle positions for the entire KTMB network — as a typed library, REST API, and MCP server. Powered by data.gov.my GTFS feeds.

$ npm i ktmb

Station search

Every stop on the network, fuzzy-matched.

156 stations across ETS, Intercity, Komuter, and Shuttle Tebrau — searchable by code, English name, or Bahasa Melayu name with Fuse.js scoring.

GET /v1/stations?q=KL 12ms

Schedules & fares

Plan an ETS journey
down to the seat class.

All ETS, Intercity, and Shuttle Tebrau departures within the GTFS calendar window — joined with fare data from KTMB's booking site.

GET /v1/schedules?from=KUL&to=BTW&date=2026-05-01 94ms

Komuter timetables

Klang Valley + Northern Sector,
departure-board ready.

Pick a line, pick a station — get the next 12 departures rolling forward from now. Calendar-aware; weekday and weekend services swap automatically.

Realtime · GTFS-RT

Where every train is, right now.

Live vehicle positions polled from data.gov.my's GTFS-Realtime feed — typed, validated, and re-emitted with consistent ISO-8601 +08:00 timestamps.

Padang Besar
Butterworth
Ipoh
KL Sentral
Kuantan
JB Sentral
ETS
Komuter
Intercity
Shuttle Tebrau
0 vehicles
Click a vehicle to inspect its live position.

Library

Three surfaces. One core.

A single read-only core powers the typed library, a Hono-based REST server, and a stdio MCP server for Claude Desktop and Claude Code.

import { GtfsLoader, createKtmb, ktmbGetAvailability,
         fetchVehiclePositions } from "ktmb";

// Load the GTFS Static feed once at boot.
const loader = new GtfsLoader(
  "https://api.data.gov.my/gtfs-static/ktmb"
);
const r = await loader.load();
if (!r.ok) throw new Error(r.error.message);

const ktmb = createKtmb({
  store: r.data,
  fareGetter: ktmbGetAvailability,
  realtimeFetcher: () =>
    fetchVehiclePositions(
      "https://api.data.gov.my/gtfs-realtime/vehicle-position/ktmb"
    ),
});

const stations = ktmb.stations.search("KL");
const trains = ktmb.schedules.listSchedules({
  from: "KUL", to: "BTW", date: "2026-05-01",
});

REST surface

Seven endpoints. One envelope.

Every response is either { ok: true, data } or { ok: false, error: { code, message } }. Typed errors map cleanly to HTTP statuses; outside_calendar_window is 422.

GET
/v1/stations?q=…
Fuzzy station search across English & Bahasa Melayu names.
GET
/v1/stations/:id
Single station lookup by code.
GET
/v1/schedules?from&to&date
ETS, Intercity, and Shuttle Tebrau departures for a date.
GET
/v1/schedules/:trainNo/availability
Fare classes and seats left, scraped politely.
GET
/v1/komuter/lines
All Komuter line ids and their stations.
GET
/v1/komuter/lines/:line/timetable
Calendar-aware Komuter departures for a station.
GET
/v1/realtime/vehicles
Live GTFS-RT vehicle positions, optionally filtered by route.