Nov 2, 2026 / Gary Tokman
How to get NYC subway arrivals in JavaScript without parsing GTFS-realtime
In short
- Call
mta.subway.arrivals({ stopId, route })with mta-js and you get arrivals with the route, stop name, destination, direction text, and minutes. The hosted API does the work you would otherwise do yourself: decoding the protobuf feed, picking the right feed for each line, and mapping north and south directions. - Install:
npm i mta-js. One call:mta.subway.arrivals({ stopId: "L08", route: "L", limit: 5 }). - Destination to direction:
mta.subway.direction()turns "Union Sq" into a feed direction. Free option: mta-js is MIT licensed and can call MTA feeds directly.
What the raw MTA feed asks of you
The demo above is live: it calls mta.subway.arrivalBoard() near Union Sq, shown in "Find arrivals near a location" below.
The MTA publishes subway realtime data as GTFS-realtime, a protobuf format with NYCT-specific extensions. The MTA developer resources page describes the feeds and the protobuf files you need to read the extensions. Building directly on that means handling four things:
Decoding
The feed is binary protobuf, so you need the GTFS-realtime definitions plus the NYCT extension file.
Choosing a feed
Subway data is split by line group. Open source projects map routes to separate feed URLs, for example one for the L, one for A, C, and E, one for B, D, F, and M, and one for N, Q, R, and W.
Directions
Stops carry north and south directions, and those labels do not always match the geography. The L is the usual surprise: north runs into Manhattan and south runs into Brooklyn.
Freshness
The MTA subway reference says the feed is generated every 30 seconds, so you need caching and sensible polling.
Get arrivals with mta-js
Each arrival includes route metadata, the stop, a direction, an ISO arrivalTime, minutes, and a realtime flag. Display fields such as displayDirection and destination can be missing, so keep a fallback like the one above.
import { MTA } from "mta-js"const mta = new MTA({ apiKey: process.env.MTA_API_KEY })// Next L trains at Bedford Avconst arrivals = await mta.subway.arrivals({stopId: "L08",route: "L",limit: 5,})for (const arrival of arrivals) {const where = arrival.displayDirection ?? arrival.destination ?? "unknown direction"console.log(`${arrival.route.shortName} ${where} from ${arrival.stop.displayName ?? arrival.stop.name}: ${arrival.minutes} min`,)}
Turn a destination into a direction
Riders think in destinations, not north and south. mta.subway.direction() resolves a destination against GTFS route order:
const toward = await mta.subway.direction({route: "L",fromStopId: "L06",destination: "Union Sq",})// Returns a typed direction, the destination stop, and a label such as "8 Av"const arrivals = await mta.subway.arrivals({stopId: "L06",route: "L",direction: toward.direction,limit: 5,})
Find arrivals near a location
Log board once to see the response shape before you build a UI around it.
const board = await mta.subway.arrivalBoard({lat: 40.7359,lon: -73.9906,limitStations: 3,limitArrivals: 4,})
The same call over HTTP
The REST path is GET /api/v1/subway/arrivals. Send your key in an x-api-key header or as a bearer token. See the API reference for parameters and the subway arrivals guide.
Self-host or use the hosted API
mta-js is open source under the MIT license, and you can call MTA realtime feeds directly with it. Choose the hosted API when you want managed realtime endpoints, route-aware stop lookups, caching, and stable response shapes without running GTFS imports yourself. Plans start at $19 per month with a 7-day free trial. See pricing.
FAQ
mta.subway.arrivals({ stopId, route }). You get arrivals with minutes, stop names, and direction text, with no protobuf decoding on your side.https://www.mtaapi.dev/mcp with your API key, and the subway_arrivals tool returns the same arrivals as mta.subway.arrivals.Get an API key and fetch your first arrivals in a few minutes.