# MCP, llms.txt, and skill.md
Source: https://mtaapi.dev/docs/ai/overview
Connect AI tools to the MTA API docs through the hosted MCP server, llms.txt / llms-full.txt files, and an installable Claude skill — all generated automatically.
These docs are built to be consumed by AI tools, not just people. Every page is available in machine-readable form, plus there's a hosted MCP server and an installable Claude skill — all generated and kept up to date automatically, at no extra cost. Point your agent at any of the endpoints below to give it grounded, current knowledge of the `mta-js` SDK and the MTA API.
Connect Cursor, Claude, Codex, VS Code, and other MCP clients to search these docs live.
A machine-readable index of every page, for AI tools to discover and navigate.
Install these docs as a Claude skill in Claude Code and Claude Desktop.
Append `.md` to any docs URL to get the raw Markdown of that page.
## MCP server
The docs are exposed as a hosted [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server, so AI clients can search and read the documentation as a tool — answering questions about the SDK and API with up-to-date context. It exposes a **search** tool and a **filesystem-style navigation** tool over the docs.
**Server URL**
```text theme={null}
https://mtaapi.dev/docs/mcp
```
Add the server to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in your project:
```json theme={null}
{
"mcpServers": {
"mta-api": {
"url": "https://mtaapi.dev/docs/mcp"
}
}
}
```
Add the remote MCP server from your terminal:
```bash theme={null}
claude mcp add --transport http mta-api https://mtaapi.dev/docs/mcp
```
Add the server to `~/.codex/config.toml`:
```toml theme={null}
[mcp_servers.mta-api]
url = "https://mtaapi.dev/docs/mcp"
```
Or add it from your terminal:
```bash theme={null}
codex mcp add mta-api --url https://mtaapi.dev/docs/mcp
```
Remote (streamable HTTP) MCP servers require Codex's RMCP client. If the server isn't picked up, enable it in `~/.codex/config.toml`:
```toml theme={null}
experimental_use_rmcp_client = true
```
In **Settings → Connectors**, choose **Add custom connector**, then paste the server URL:
```text theme={null}
https://mtaapi.dev/docs/mcp
```
Add the server to `.vscode/mcp.json`:
```json theme={null}
{
"servers": {
"mta-api": {
"type": "http",
"url": "https://mtaapi.dev/docs/mcp"
}
}
}
```
Once connected, ask your assistant things like *“How do I fetch a subway arrival board with mta-js?”* and it will pull answers straight from these docs. A discovery endpoint is also published at `/.well-known/mcp` so compatible tools can locate the server automatically.
## llms.txt
Following the [llms.txt standard](https://llmstxt.org), two machine-readable files are generated automatically and stay in sync with these docs:
| File | URL | Contents |
| --------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `llms.txt` | [`https://mtaapi.dev/docs/llms.txt`](https://mtaapi.dev/docs/llms.txt) | A structured index of every page with titles, links, and descriptions — a sitemap for LLMs. |
| `llms-full.txt` | [`https://mtaapi.dev/docs/llms-full.txt`](https://mtaapi.dev/docs/llms-full.txt) | The entire documentation site concatenated into one file for direct context loading. |
Use `llms.txt` when a tool should discover and fetch only the relevant pages, and `llms-full.txt` when you want to drop the whole corpus into a model's context window.
## Claude skill
These docs are also published as an installable [Claude skill](https://www.anthropic.com/news/skills), described by a `skill.md` manifest that summarizes the SDK's capabilities, methods, and best practices for agents:
```text theme={null}
https://mtaapi.dev/docs/skill.md
```
Add it to any agent that supports the [agent skills](https://agentskills.io) spec with the skills CLI:
```bash theme={null}
npx skills add https://mtaapi.dev/docs
```
This loads the MTA API capabilities into the agent's context so it can reference the guides and reference material on demand while you work. MCP-compatible tools can also read the `skill.md` content directly.
## Markdown for any page
Every page is available as raw Markdown — just append `.md` to its URL. For example:
```text theme={null}
https://www.mtaapi.dev/docs/guides/subway-arrivals.md
```
This is handy for piping a single page into a prompt or script without any HTML.
All of the above — the MCP server, `llms.txt`, `llms-full.txt`, `skill.md`, and per-page Markdown — are generated and hosted automatically. There's nothing to configure and no extra cost.
# mta.alerts.current() — active MTA service alerts
Source: https://mtaapi.dev/docs/api-reference/alerts
GET /api/v1/alerts
Reference for mta.alerts in mta-js. Retrieve current MTA service alerts—delays, planned work, and route suspensions—for subway or bus.
The `mta.alerts` namespace surfaces active MTA service alerts pulled from the agency's GTFS-Realtime alerts feed. Use `mta.alerts.current` to retrieve delays, planned service changes, and route suspensions for either the subway or bus network, filtered to only what's happening right now.
## `mta.alerts.current(params)`
Returns all active service alerts for the specified transit mode. Alerts include human-readable header and description text, affected routes, a severity classification, and the time window during which the alert applies.
### Parameters
The transit mode to fetch alerts for. Accepted values are `'subway'` or `'bus'`. Pass `'subway'` to get alerts affecting subway routes, or `'bus'` to get alerts affecting bus routes.
### TypeScript signature
```typescript theme={null}
type AlertMode = 'subway' | 'bus'
interface AlertsCurrentParams {
mode: AlertMode
}
interface ServiceAlert {
id: string
headerText: string
descriptionText: string
affectedRoutes: string[]
severity: string
startTime: number // Unix timestamp (seconds)
endTime: number | null // null if the alert has no scheduled end
}
interface AlertsCurrentResponse {
alerts: ServiceAlert[]
}
mta.alerts.current(params: AlertsCurrentParams): Promise
```
### Response fields
An array of active service alerts for the requested mode, ordered by `startTime` descending. Returns an empty array if there are no active alerts.
The unique identifier for this alert, assigned by the MTA.
A short summary of the alert, suitable for display in a list or notification.
The full alert body with details about affected service and any alternative instructions.
An array of route IDs impacted by this alert (for example, `['A', 'C', 'E']`).
The severity level of the alert. See the severity table below for possible values and their meanings.
The Unix timestamp (in seconds) when the alert becomes or became active.
The Unix timestamp (in seconds) when the alert is scheduled to end, or `null` if no end time has been set.
### Severity values
The `severity` field uses the following string values, from least to most severe:
| Value | Meaning |
| -------------- | -------------------------------------------------------- |
| `'INFO'` | General informational notice with no service impact. |
| `'WARNING'` | Minor disruption; service is affected but operating. |
| `'SEVERE'` | Significant disruption such as major delays or reroutes. |
| `'NO_SERVICE'` | The affected route or portion of it is suspended. |
`'NO_SERVICE'` alerts often accompany planned weekend work or emergency shutdowns. Filter on this severity value to surface outages that require riders to seek alternative routes.
### Code example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const response = await mta.alerts.current({ mode: 'subway' })
const severe = response.alerts.filter(
(alert) => alert.severity === 'SEVERE' || alert.severity === 'NO_SERVICE'
)
for (const alert of severe) {
console.log(`[${alert.severity}] ${alert.headerText}`)
console.log(`Affected routes: ${alert.affectedRoutes.join(', ')}`)
console.log(alert.descriptionText)
console.log('---')
}
```
### Example response
```json theme={null}
{
"alerts": [
{
"id": "lmm:planned_work:127307",
"headerText": "A trains skip Howard Beach–JFK Airport",
"descriptionText": "Due to track maintenance, A trains will not stop at Howard Beach–JFK Airport (A27) from 11:30 PM Friday to 5 AM Monday. Take the AirTrain from Jamaica station as an alternative.",
"affectedRoutes": ["A"],
"severity": "WARNING",
"startTime": 1748991000,
"endTime": 1749214800
},
{
"id": "lmm:service_change:130841",
"headerText": "No F trains between 47–50 Sts–Rockefeller Ctr and Jay St–MetroTech",
"descriptionText": "F trains are suspended between 47–50 Sts–Rockefeller Ctr and Jay St–MetroTech due to an emergency signal repair. Use the A, C, or E trains as alternatives.",
"affectedRoutes": ["F"],
"severity": "NO_SERVICE",
"startTime": 1748994600,
"endTime": null
}
]
}
```
# mta.bus.vehicles() — live bus positions by route
Source: https://mtaapi.dev/docs/api-reference/bus
GET /api/v1/bus/arrivals
Reference for mta.bus in mta-js. Retrieve live bus vehicle positions, GPS coordinates, heading, next stop, and occupancy for any MTA route.
The `mta.bus` namespace provides access to real-time bus data sourced from the MTA's Bus Time API. Use `mta.bus.arrivals` to get the next predicted buses at a specific stop, and `mta.bus.vehicles` to see where every active bus on a route is right now, including its coordinates, bearing, next stop, and passenger occupancy status.
## `mta.bus.arrivals(params)`
Returns upcoming bus arrival predictions for a single stop. Pass an optional `route` to limit results to one route serving that stop.
### Parameters
The MTA bus stop ID to query (for example, `'308214'`). Look up stop IDs with `mta.stops.near` or from the MTA's published stop data.
Optional route filter (for example, `'M23'`). When omitted, all routes serving the stop are returned.
Maximum number of upcoming arrivals to return. Must be between `1` and `100`.
When `true`, the response includes the raw upstream feed payload alongside the normalized arrivals. Useful for debugging or accessing fields not yet surfaced by the normalized API.
### Code example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const response = await mta.bus.arrivals({
stopId: '308214',
route: 'M23',
limit: 5,
})
for (const arrival of response.arrivals) {
console.log(`${arrival.route} — ${arrival.headsign} — vehicle ${arrival.vehicleId}`)
}
```
## `mta.bus.vehicles(params)`
## `mta.bus.vehicles(params)`
Returns active bus vehicles. Pass `route` to scope to a single route, or `vehicleId` to look up a specific vehicle. Data is refreshed approximately every 30 seconds and reflects the most recent position reported by each vehicle's onboard GPS unit.
### Parameters
Optional bus route ID to filter on (for example, `'B63'` for the Brooklyn route along Fifth Avenue, or `'M15'` for the Manhattan crosstown route on First and Second Avenue). Route IDs are case-sensitive and match MTA's published route identifiers.
Optional MTA vehicle ID to look up a single vehicle (for example, `'MTA_8741'`).
Maximum number of vehicles to return. Must be between `1` and `100`.
When `true`, the response includes the raw upstream feed payload alongside the normalized vehicle list.
### TypeScript signature
```typescript theme={null}
interface BusVehiclesParams {
route?: string
vehicleId?: string
limit?: number
includeRaw?: boolean
}
interface BusVehicle {
vehicleId: string
lat: number
lon: number
bearing: number
nextStop: string
occupancyStatus: string
}
interface BusVehiclesResponse {
route: string
vehicles: BusVehicle[]
}
mta.bus.vehicles(params: BusVehiclesParams): Promise
```
### Response fields
The route ID that was queried, echoed back in the response.
An array of active vehicles on the route. Returns an empty array if no vehicles are currently in service.
The unique identifier for the vehicle, matching the MTA's fleet numbering system.
The vehicle's current latitude in decimal degrees (WGS 84).
The vehicle's current longitude in decimal degrees (WGS 84).
The vehicle's direction of travel in degrees, measured clockwise from true north (0–359).
The name or ID of the next scheduled stop for this vehicle.
The current passenger load level. Possible values: `'EMPTY'`, `'MANY_SEATS_AVAILABLE'`, `'FEW_SEATS_AVAILABLE'`, `'STANDING_ROOM_ONLY'`, `'CRUSHED_STANDING_ROOM_ONLY'`, `'FULL'`.
### Code example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const response = await mta.bus.vehicles({ route: 'B63', limit: 10 })
console.log(`${response.vehicles.length} buses active on route ${response.route}`)
for (const vehicle of response.vehicles) {
console.log(
`Bus ${vehicle.vehicleId} at (${vehicle.lat}, ${vehicle.lon}) ` +
`heading ${vehicle.bearing}° — next stop: ${vehicle.nextStop} ` +
`— occupancy: ${vehicle.occupancyStatus}`
)
}
```
### Example response
```json theme={null}
{
"route": "B63",
"vehicles": [
{
"vehicleId": "MTA_8741",
"lat": 40.6765,
"lon": -73.9927,
"bearing": 342,
"nextStop": "5 AV/ATLANTIC AV",
"occupancyStatus": "MANY_SEATS_AVAILABLE"
},
{
"vehicleId": "MTA_8804",
"lat": 40.6621,
"lon": -73.9891,
"bearing": 162,
"nextStop": "5 AV/65 ST",
"occupancyStatus": "FEW_SEATS_AVAILABLE"
}
]
}
```
Use `bearing` together with `lat` and `lon` to render animated bus icons on a map that point in the correct direction of travel.
# mta.bus.arrivalBoard() — nearby bus departure board
Source: https://mtaapi.dev/docs/api-reference/bus-arrival-board
GET /api/v1/bus/arrival-board
Reference for GET /api/v1/bus/arrival-board. Find nearby bus stops and group their upcoming arrivals by route in a single request.
Returns the nearest bus stops to a `lat` / `lon`, with each stop's upcoming arrivals grouped by route — the bus counterpart to the [subway arrival board](/docs/api-reference/subway-arrival-board).
Control the board with `limitStops` (max 20, default 8) and `limitArrivals` per route (max 10, default 3). Pass an optional `route` to restrict every stop to a single line. Each entry is `{ stop, distanceMeters, routes: [{ route, headsign?, arrivals[] }] }`.
### SDK
```typescript theme={null}
const board = await mta.bus.arrivalBoard({
lat: 40.7421,
lon: -73.9914,
route: 'M23',
limitStops: 1,
limitArrivals: 1,
})
```
See the [Arrival Boards guide](/docs/guides/arrival-boards).
# mta.bus.routeStops() — ordered stops on a route
Source: https://mtaapi.dev/docs/api-reference/bus-route-stops
GET /api/v1/bus/routes/{route}/stops
Reference for GET /api/v1/bus/routes/{route}/stops. Get the ordered stops a bus route serves, optionally hydrated with arrivals.
Returns the ordered stops a bus route serves. Pass the route in the path (for example `M23`) and an optional `direction` filter. Set `includeArrivals=true` to attach live arrivals to each stop (bounded by `limitArrivals` and `limitStops`).
The response is a `RouteStopsResponse`: `{ route, mode, directions: [{ direction, headsigns?, stops: [...] }] }`, where each stop carries an optional `arrivals` array when hydrated.
### SDK
```typescript theme={null}
const routeStops = await mta.bus.routeStops({ route: 'M23' })
for (const pattern of routeStops.directions) {
console.log(pattern.direction, pattern.stops.length, 'stops')
}
```
See the [Routes & Stations guide](/docs/guides/routes-and-stations).
# mta.subway.direction() — resolve a destination to a direction
Source: https://mtaapi.dev/docs/api-reference/direction
GET /api/v1/subway/direction
Reference for mta.subway.direction in mta-js. Resolve a rider-facing destination string into a typed north/south direction, terminal, and display label.
The `mta.subway.direction` method resolves a rider-facing destination string (such as `"Union Sq"`) into a typed direction of travel, the terminal the train heads toward, and a ready-to-display label. Use it when a rider tells you *where they want to go* and you need to translate that into the `north` / `south` direction the realtime feed uses — for example, to pre-select the correct direction in an arrivals UI.
This method requires a hosted API key. It resolves destinations against the hosted API's static GTFS route order and is not available in direct-feed mode.
## `mta.subway.direction(params)`
### Parameters
The subway route the rider is traveling on (for example, `'L'`).
The stop ID the rider is departing from (for example, `'L08'` for Bedford Av).
A rider-facing destination string to resolve against the route's stop order (for example, `'Union Sq'`). Matching tolerates casing and common station-name variations.
### TypeScript signature
```typescript theme={null}
interface SubwayDirectionQuery {
route: SubwayRoute
fromStopId: SubwayStopId
destination: string
}
// Subway feed directions are always north/south, even on east-west lines
type SubwayResolvedDirection = 'north' | 'south'
interface SubwayDirectionResolution {
route: Route
destination: string // the string you passed in
normalizedDestination: string // cleaned form used for matching
resolved: boolean // whether a direction was determined
direction?: SubwayResolvedDirection
displayDirection?: string // e.g. "toward 8 Av"
terminal?: string // terminal station the train heads toward
fromStop?: Stop
destinationStop?: Stop
matches?: Stop[] // candidate stops when the match is ambiguous
reason?: string // why resolution failed, when resolved is false
}
mta.subway.direction(params: SubwayDirectionQuery): Promise
```
The method always resolves to a `SubwayDirectionResolution` object — it does not return `null`. Branch on `resolved`: when `true`, `direction`, `displayDirection`, `terminal`, and `destinationStop` are populated; when `false`, `reason` explains why, and `matches` may list ambiguous candidates.
### Response fields
The resolved route object: `id`, plus optional `shortName`, `longName`, `color`, `textColor`, and `type`.
The destination string exactly as passed in.
The cleaned/normalized form of the destination used for matching.
Whether a direction was successfully determined. When `false`, inspect `reason` and `matches`.
The resolved feed direction: `'north'` or `'south'`. Present when `resolved` is `true`.
A human-readable label such as `"toward 8 Av"`. Present when `resolved` is `true`.
The name of the terminal station the train heads toward in the resolved direction.
The resolved origin stop object for `fromStopId`.
The resolved destination stop object that matched `destination`.
Candidate stops when the destination is ambiguous and could not be narrowed to a single stop.
A short explanation of why resolution failed. Present when `resolved` is `false`.
### Code example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
// A rider at Bedford Av (L08) wants to reach Union Sq — which way do they go?
const resolution = await mta.subway.direction({
route: 'L',
fromStopId: 'L08',
destination: 'Union Sq',
})
if (resolution.resolved) {
console.log(resolution.direction) // "north"
console.log(resolution.displayDirection) // "toward 8 Av"
console.log(resolution.terminal) // "8 Av"
// Feed the resolved direction straight into arrivals
const arrivals = await mta.subway.arrivals({
stopId: 'L08',
route: 'L',
direction: resolution.direction,
})
console.log(arrivals)
} else {
console.warn(`Couldn't resolve a direction: ${resolution.reason}`)
}
```
### Example response
```json theme={null}
{
"route": { "id": "L", "shortName": "L", "color": "#A7A9AC" },
"destination": "Union Sq",
"normalizedDestination": "union sq",
"resolved": true,
"direction": "north",
"displayDirection": "toward 8 Av",
"terminal": "8 Av",
"fromStop": { "id": "L08", "name": "Bedford Av" },
"destinationStop": { "id": "L02", "name": "14 St-Union Sq" }
}
```
NYC Subway realtime feeds use NYCT's `north` / `south` stop directions, even on east-west lines like the L. `mta.subway.direction` always resolves to one of those two feed directions, and [`mta.subway.arrivals`](/docs/api-reference/subway) accepts the same `direction` value — along with the rider-facing `east` / `west` and `uptown` / `downtown` aliases.
This method is only available with a hosted `apiKey`. It calls the hosted endpoint and resolves against the managed static GTFS snapshot; it is not available in direct-feed mode.
# mta-js SDK reference: all namespaces and methods
Source: https://mtaapi.dev/docs/api-reference/overview
Explore the full mta-js SDK method surface, organized by namespace: subway arrivals, bus positions, service alerts, and nearby stop lookups.
The mta-js SDK exposes four namespaces on the `MTA` client — `subway`, `bus`, `alerts`, and `stops` — each covering a different aspect of NYC transit data. The `subway` namespace has two methods (`arrivals()` and `direction()`); the others have one each. Every namespace method is async, returns typed results, and requires only a single initialized client to access the full API surface.
`subway.arrivals()` — real-time arrival predictions for any subway stop and route.
`subway.direction()` — resolve a rider's destination to a north/south direction.
`bus.arrivals()` and `bus.vehicles()` — live arrivals and vehicle positions for any MTA bus route.
`alerts.current()` — current service alerts including delays and planned work.
`stops.near()` — find nearby subway and bus stops by geographic coordinates.
## Client initialization
Create a single `MTA` instance at startup and reuse it throughout your application. The constructor accepts an options object — pass an `apiKey` from [mtaapi.dev](https://www.mtaapi.dev) for the hosted API, or `busTimeKey` + `databaseUrl` to run self-hosted.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
```
The TypeScript signature for the constructor is:
```typescript theme={null}
interface MTAOptions {
apiKey?: string // hosted mtaapi.dev API key
apiBaseUrl?: string // override hosted API base URL
busTimeKey?: string // self-hosted: MTA BusTime key
databaseUrl?: string // self-hosted: SQLite or libSQL/Turso URL
databaseAuthToken?: string
databaseLocalPath?: string
}
class MTA {
constructor(options: MTAOptions): MTA
subway: SubwayNamespace // arrivals(), direction()
bus: BusNamespace // arrivals(), vehicles()
alerts: AlertsNamespace // current()
stops: StopsNamespace // near()
}
```
### Core exported types
The following types are exported from `mta-js` and cover the full shape of SDK responses.
```typescript theme={null}
// Typed ID utilities — accept any known value with autocomplete, while staying
// permissive for future routes/stops. The pattern is: KnownRoute | (string & {})
type AutocompleteString = TKnown | (string & {})
// Known* unions are code-generated from the hosted GTFS snapshot (./generated)
type RouteId = AutocompleteString
type SubwayRoute = AutocompleteString
type BusRoute = AutocompleteString
type StopId = AutocompleteString
type SubwayStopId = AutocompleteString
type BusStopId = AutocompleteString
type TransitMode = 'subway' | 'bus' | 'lirr' | 'metro-north'
// Feed directions — NYCT uses north/south even on east-west lines
type Direction = 'north' | 'south' | 'east' | 'west' | 'unknown'
type SubwayResolvedDirection = 'north' | 'south'
// Headsigns keyed by direction; values are arrays of headsign strings
type DirectionHeadsigns = Record
interface Route {
id: string
shortName?: string
longName?: string
color?: string
textColor?: string
type?: number
}
interface Stop {
id: string
name: string
displayName?: string
lat?: number
lon?: number
parentStation?: string
parentId?: string
mode?: TransitMode
}
// A Route plus the headsign/direction metadata served at a given stop
type ServedRoute = Route & {
headsigns?: string[]
directionHeadsigns?: DirectionHeadsigns
directions?: number[]
}
// Returned by mta.stops.near()
type NearbyStop = Stop & {
distanceMeters?: number
servedRoutes?: ServedRoute[]
routeMatch?: boolean
routeHeadsigns?: string[]
directionHeadsigns?: DirectionHeadsigns
note?: string
}
// Returned by mta.subway.arrivals() and mta.bus.arrivals()
interface Arrival {
mode: TransitMode
route: Route
stop: Stop
direction: Direction
destination?: string
displayDirection?: string
headsign?: string
arrivalTime: string // ISO 8601
departureTime?: string // ISO 8601
minutes: number
tripId?: string
realtime: boolean
source: 'mta-gtfs-rt' | 'mta-bustime'
raw?: unknown
}
// Query + result for mta.subway.direction()
interface SubwayDirectionQuery {
route: SubwayRoute
fromStopId: SubwayStopId
destination: string
}
interface SubwayDirectionResolution {
route: Route
destination: string
normalizedDestination: string
resolved: boolean
direction?: SubwayResolvedDirection
displayDirection?: string
terminal?: string
fromStop?: Stop
destinationStop?: Stop
matches?: Stop[]
reason?: string
}
```
Store your API key in an environment variable rather than hardcoding it. Use a `.env` file locally and your deployment platform's secret manager in production.
## Error handling
All SDK methods throw a typed `MtaError` when the request fails. Wrap calls in a `try/catch` block and inspect the `code` property to handle specific failure modes.
```typescript theme={null}
import { MTA, MtaError } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
try {
const arrivals = await mta.subway.arrivals({ stopId: 'A27', route: 'A' })
console.log(arrivals)
} catch (err) {
if (err instanceof MtaError) {
switch (err.code) {
case 'INVALID_API_KEY':
console.error('Check your MTA_API_KEY environment variable.')
break
case 'STOP_NOT_FOUND':
console.error('The stop ID does not exist.')
break
case 'RATE_LIMITED':
console.error('Too many requests — slow down and retry.')
break
default:
console.error(`MTA error ${err.code}: ${err.message}`)
}
} else {
throw err
}
}
```
Common error codes returned by the SDK:
| Code | Description |
| ------------------ | -------------------------------------------------------------- |
| `INVALID_API_KEY` | The API key is missing, malformed, or revoked. |
| `STOP_NOT_FOUND` | The requested stop ID does not exist. |
| `ROUTE_NOT_FOUND` | The requested route ID does not exist. |
| `RATE_LIMITED` | You have exceeded the allowed request rate. |
| `FEED_UNAVAILABLE` | The upstream MTA feed is temporarily unavailable. |
| `NETWORK_ERROR` | A network-level failure occurred before the request completed. |
# mta.routes.list() — list subway and bus routes
Source: https://mtaapi.dev/docs/api-reference/routes
GET /api/v1/routes
Reference for GET /api/v1/routes. List the catalog of subway and bus routes, with names, colors, and transit mode.
Returns the catalog of routes in the hosted stops snapshot. Pass a comma-separated `modes` filter (for example `subway,bus`) to scope the list, or omit it for every mode.
Each entry is a `Route` plus a `mode`: `{ id, shortName?, longName?, color?, textColor?, type?, mode }`. Use it to populate a route picker or hydrate route metadata client-side.
### SDK
```typescript theme={null}
const routes = await mta.routes.list({ modes: ['subway', 'bus'] })
for (const route of routes) {
console.log(route.shortName ?? route.id, route.mode)
}
```
See the [Routes & Stations guide](/docs/guides/routes-and-stations).
# mta.stops.near() — find stops by geolocation
Source: https://mtaapi.dev/docs/api-reference/stops
GET /api/v1/stops/near
Reference for mta.stops methods in mta-js. Find nearby MTA subway and bus stops sorted by distance from any latitude and longitude.
The `mta.stops` namespace lets you discover MTA stops near any point on the map. Use `mta.stops.near` to find subway stations, bus stops, or both, sorted by distance from a given latitude and longitude. You can also pass a `route` to return only stops served by that route — for example, only `L` train stops or only `M23` SBS bus stops within walking distance. This is the recommended starting point when you need stop IDs for use with `mta.subway.arrivals` or `mta.bus.arrivals`, or to power a "stops near me" feature in your application.
## `mta.stops.near(params)`
Returns a list of MTA stops within a configurable radius of the provided coordinates, sorted by distance ascending. You can limit results to subway stops, bus stops, or both by passing the `modes` parameter, and further filter to stops served by a specific `route`.
### Parameters
The latitude of the center point for the search, in decimal degrees (WGS 84). For example, `40.7128` for lower Manhattan.
The longitude of the center point for the search, in decimal degrees (WGS 84). For example, `-74.0060` for lower Manhattan.
An array of transit modes to include in the results. Accepted values are `'subway'` and `'bus'`. Pass `['subway']` to return only subway stations, `['bus']` to return only bus stops, or `['subway', 'bus']` to return both. Defaults to both modes if omitted.
Optional route filter. When supplied, only stops served by this route are returned. For example, `'L'` returns nearby stops served by the L train, and `'M23'` returns nearby M23 SBS bus stops. Route IDs are case-sensitive and match MTA's published identifiers.
When `true`, each returned stop includes route metadata: a `servedRoutes` array of `ServedRoute` objects and a `directionHeadsigns` map. Defaults to `false` to keep responses small when you only need stop identity and distance.
Search radius in meters. Defaults to `500`. Increase this when querying sparse areas or to widen a "stops near me" view.
Maximum number of stops to return. Must be between `1` and `100`. Results remain sorted by distance ascending.
### TypeScript signature
```typescript theme={null}
type TransitMode = 'subway' | 'bus' | 'lirr' | 'metro-north'
interface StopsNearParams {
lat: number
lon: number
modes?: TransitMode[]
route?: string
includeRoutes?: boolean
radiusMeters?: number
limit?: number
}
interface Stop {
id: string
name: string
displayName?: string // human-friendly label (may differ from GTFS name)
lat?: number
lon?: number
parentStation?: string // GTFS parent_station ID
parentId?: string // resolved parent station ID
mode?: TransitMode
}
interface Route {
id: string
shortName?: string
longName?: string
color?: string
textColor?: string
type?: number
}
// Headsigns keyed by direction, e.g. { "north": ["8 Av"], "south": ["Canarsie"] }
type DirectionHeadsigns = Record
// A route serving a stop — a Route plus headsign/direction metadata
type ServedRoute = Route & {
headsigns?: string[]
directionHeadsigns?: DirectionHeadsigns
directions?: number[]
}
type NearbyStop = Stop & {
distanceMeters?: number
servedRoutes?: ServedRoute[] // requires includeRoutes: true
routeMatch?: boolean // true when the stop matches the `route` filter
routeHeadsigns?: string[] // headsigns for the filtered route
directionHeadsigns?: DirectionHeadsigns
note?: string
}
mta.stops.near(params: StopsNearParams): Promise
```
### Response fields
An array of nearby stops, sorted by `distanceMeters` ascending. Returns an empty array if no stops are found within the search radius.
The MTA stop ID. Use this as the `stopId` parameter in `mta.subway.arrivals`.
The GTFS stop name (e.g. `'Jay St-MetroTech'`).
A human-friendly display label that may differ slightly from the raw GTFS name (e.g. `'Jay St–MetroTech'`).
The GTFS `parent_station` ID, when the stop is a child platform of a larger station.
The resolved parent station ID. Use this to group platform-level stops under the same station.
The latitude of the stop in decimal degrees (WGS 84).
The longitude of the stop in decimal degrees (WGS 84).
The transit mode: `'subway'` or `'bus'`.
Straight-line distance from the queried coordinates to this stop, in meters. Always present on results from `mta.stops.near`.
Routes serving this stop. Each is a `Route` (`id`, `shortName`, `longName`, `color`, `textColor`, `type`) extended with optional `headsigns`, `directionHeadsigns`, and `directions`. Only included when `includeRoutes: true` is passed.
When a `route` filter was supplied, indicates whether this stop is served by that route.
Headsigns for the filtered `route` at this stop, when a `route` filter was supplied.
A `Record` mapping each direction to its headsigns (e.g. `{ "north": ["8 Av"], "south": ["Canarsie"] }`). Useful for building direction-picker UIs. Only included when `includeRoutes: true` is passed.
An optional advisory note about the stop.
### Code example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
// Find subway stops near Prospect Park in Brooklyn
const stops = await mta.stops.near({
lat: 40.6602,
lon: -73.9690,
modes: ['subway'],
includeRoutes: true,
})
for (const stop of stops) {
const distance = Math.round(stop.distanceMeters)
const routes = stop.servedRoutes?.map((r) => r.shortName).join(', ') ?? ''
console.log(`${stop.displayName ?? stop.name} (${stop.id}) — ${distance}m away — routes: ${routes}`)
}
```
### Example response
```json theme={null}
[
{
"id": "F21",
"name": "Prospect Park",
"displayName": "Prospect Park",
"lat": 40.6612,
"lon": -73.9622,
"mode": "subway",
"distanceMeters": 612,
"servedRoutes": [
{
"id": "F",
"shortName": "F",
"color": "#FF6319",
"directionHeadsigns": {
"north": ["Jamaica-179 St"],
"south": ["Coney Island-Stillwell Av"]
}
},
{
"id": "G",
"shortName": "G",
"color": "#6CBE45",
"directionHeadsigns": {
"north": ["Court Sq"],
"south": ["Church Av"]
}
}
]
},
{
"id": "D24",
"name": "Parkside Av",
"displayName": "Parkside Av",
"lat": 40.6554,
"lon": -73.9614,
"mode": "subway",
"distanceMeters": 870,
"servedRoutes": [
{ "id": "B", "shortName": "B", "color": "#FF6319" },
{ "id": "Q", "shortName": "Q", "color": "#FCCC0A" }
]
}
]
```
### Route-aware example
Filter to nearby stops served by a specific route. This is useful when you want to find the closest stop on the M23 SBS or the nearest L train station without sorting through every nearby stop yourself.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
// Find nearby M23 SBS bus stops within 800m, with route metadata
const stops = await mta.stops.near({
lat: 40.7356,
lon: -73.9804,
modes: ['bus'],
route: 'M23',
includeRoutes: true,
radiusMeters: 800,
limit: 5,
})
for (const stop of stops) {
const routes = stop.servedRoutes?.map((r) => r.shortName).join(', ') ?? ''
console.log(`${stop.displayName ?? stop.name} — ${Math.round(stop.distanceMeters)}m — ${routes}`)
}
```
Pass the `id` from a result directly into `mta.subway.arrivals` or `mta.bus.arrivals` to build a two-step "find stops near me, then show arrivals" flow without any additional configuration.
# mta.stops.byIds() — batch stop lookup
Source: https://mtaapi.dev/docs/api-reference/stops-batch
GET /api/v1/stops
Reference for GET /api/v1/stops. Resolve multiple stop IDs in a single request, preserving request order, optionally with served routes.
Resolves many stops at once. Pass a comma-separated `ids` list (for example `A27,L06,308214`) to hydrate a saved-stops list or a map viewport without one request per ID.
The response preserves one result per requested ID, in order: `{ requestedId, found, stop?, servedRoutes? }`. When `found` is `false`, the ID didn't resolve. Set `includeRoutes=true` to attach `servedRoutes` for each resolved stop. For nearby/geo search, use the [Stops reference](/docs/api-reference/stops).
### SDK
```typescript theme={null}
const results = await mta.stops.byIds({
ids: ['A27', 'L06', '308214'],
includeRoutes: true,
})
for (const result of results) {
if (result.found) console.log(result.stop.name)
else console.log(`${result.requestedId} not found`)
}
```
See the [Routes & Stations guide](/docs/guides/routes-and-stations).
# mta.subway.arrivals() — real-time subway predictions
Source: https://mtaapi.dev/docs/api-reference/subway
GET /api/v1/subway/arrivals
Reference for mta.subway in mta-js. Fetch real-time arrival predictions for any subway stop and route, with Unix timestamps and direction of travel.
The `mta.subway` namespace gives you access to real-time arrival data sourced directly from the MTA's GTFS-Realtime feeds. Use `mta.subway.arrivals` to retrieve the next trains arriving at any subway stop for a specific route, including trip IDs, predicted arrival timestamps, and direction of travel.
## `mta.subway.arrivals(params)`
Returns real-time arrival predictions for a given stop and route combination. Arrival times are Unix timestamps in seconds and reflect the MTA's latest feed update, typically refreshed every 30 seconds.
### Parameters
The MTA stop ID for the station you want to query (for example, `'A27'` for Howard Beach–JFK Airport on the A line). You can look up stop IDs using [`mta.stops.near`](/docs/api-reference/stops) or the [MTA's static GTFS data](https://api.mta.info).
The route ID to filter arrivals by (for example, `'A'`, `'1'`, or `'N'`). Optional — when omitted, arrivals across all routes serving the stop are returned. When supplied, only arrivals for that route are returned.
Optional direction filter. Accepts the feed values `'north'`, `'south'`, `'east'`, `'west'`, `'unknown'`, plus the rider-facing aliases `'uptown'` (→ north) and `'downtown'` (→ south).
Maximum number of arrivals to return, between `1` and `100`. Defaults to `20`.
When `true`, each arrival includes the underlying GTFS-RT entity on a `raw` field. Defaults to `false`.
### TypeScript signature
```typescript theme={null}
interface SubwayArrivalQuery {
stopId: SubwayStopId
route?: SubwayRoute
direction?: Direction | 'uptown' | 'downtown'
limit?: number
includeRaw?: boolean
}
interface Arrival {
mode: TransitMode // 'subway' | 'bus' | 'lirr' | 'metro-north'
route: Route
stop: Stop
direction: Direction // 'north' | 'south' | 'east' | 'west' | 'unknown'
destination?: string // headsign / terminal station name
displayDirection?: string // e.g. "toward 8 Av" or "northbound"
headsign?: string
arrivalTime: string // ISO 8601
departureTime?: string // ISO 8601
minutes: number
tripId?: string
realtime: boolean
source: 'mta-gtfs-rt' | 'mta-bustime'
raw?: unknown // present when includeRaw: true
}
mta.subway.arrivals(params: SubwayArrivalQuery): Promise
```
### Response fields
An array of upcoming arrivals, ordered by `arrivalTime` ascending. Returns an empty array if no arrivals are scheduled in the current feed window.
Transit mode: always `'subway'` for this endpoint.
The route object: `id` (required), plus optional `shortName`, `longName`, `color`, `textColor`, and `type`.
The stop object: `id` and `name` (required), plus optional `displayName`, `lat`, `lon`, `parentStation`, `parentId`, and `mode`.
Direction of travel: `'north'`, `'south'`, `'east'`, `'west'`, or `'unknown'`.
The headsign / terminal station name (e.g. `"Inwood-207 St"`). Present when the MTA feed includes trip headsign data.
A human-readable direction label (e.g. `"toward 8 Av"` or `"northbound"`). Falls back to a `"bound"` string when no headsign is available.
The predicted arrival time as an ISO 8601 string.
Minutes until arrival, clamped to 0.
The GTFS trip ID for this train run.
Whether this arrival comes from a live GTFS-RT feed (`true`) or a scheduled estimate (`false`).
### Code example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const arrivals = await mta.subway.arrivals({
stopId: 'L08',
route: 'L'
})
for (const arrival of arrivals) {
const label = arrival.displayDirection ?? arrival.destination ?? 'unknown direction'
console.log(
`${arrival.route.shortName} — ${label} — ${arrival.minutes} min`
)
}
```
### Example response
```json theme={null}
[
{
"mode": "subway",
"route": { "id": "A", "shortName": "A", "color": "#0039A6" },
"stop": { "id": "A27", "name": "Howard Beach-JFK Airport", "displayName": "Howard Beach–JFK Airport" },
"direction": "south",
"destination": "Ozone Park-Lefferts Blvd",
"displayDirection": "toward Ozone Park-Lefferts Blvd",
"arrivalTime": "2026-05-29T18:37:00.000Z",
"minutes": 4,
"tripId": "AFA25GEN-A078-Sunday-00_000600_A..S03R",
"realtime": true,
"source": "mta-gtfs-rt"
},
{
"mode": "subway",
"route": { "id": "A", "shortName": "A", "color": "#0039A6" },
"stop": { "id": "A27", "name": "Howard Beach-JFK Airport", "displayName": "Howard Beach–JFK Airport" },
"direction": "north",
"destination": "Inwood-207 St",
"displayDirection": "toward Inwood-207 St",
"arrivalTime": "2026-05-29T18:49:00.000Z",
"minutes": 16,
"tripId": "AFA25GEN-A076-Sunday-00_003600_A..N03R",
"realtime": true,
"source": "mta-gtfs-rt"
}
]
```
The MTA feed does not always include arrivals for every route at every stop. If `arrivals` is empty, the train may not be active in the current feed window — check during service hours and verify the `stopId` and `route` combination is valid.
## `mta.subway.direction(params)`
The `mta.subway` namespace also exposes `mta.subway.direction()`, which resolves a rider-facing destination string (such as `"Union Sq"`) into the `north` / `south` feed direction, the terminal the train heads toward, and a display label — handy for pre-selecting the right direction before calling `arrivals`. See the dedicated [Direction reference](/docs/api-reference/direction) for full parameters, types, and examples.
# mta.subway.arrivalBoard() — nearby subway departure board
Source: https://mtaapi.dev/docs/api-reference/subway-arrival-board
GET /api/v1/subway/arrival-board
Reference for GET /api/v1/subway/arrival-board. Find nearby subway stations and group their upcoming arrivals by direction in a single request.
Returns the nearest subway stations to a `lat` / `lon`, with each station's upcoming arrivals already grouped by direction. It combines nearby-station discovery and arrivals into one request — ideal for a lobby departure board or a "stations near me" screen.
Control the board with `limitStations` (max 20, default 5) and `limitArrivals` per direction (max 10, default 3). Pass an optional `route` to restrict every station to a single line. Each entry is `{ station, distanceMeters, directions: [{ direction, headsign?, arrivals[] }] }`.
### SDK
```typescript theme={null}
const board = await mta.subway.arrivalBoard({
lat: 40.7356,
lon: -73.9906,
limitStations: 5,
limitArrivals: 3,
})
```
See the [Arrival Boards guide](/docs/guides/arrival-boards) for an end-to-end walkthrough.
# mta.subway.routeStations() — ordered stations on a route
Source: https://mtaapi.dev/docs/api-reference/subway-route-stations
GET /api/v1/subway/routes/{route}/stations
Reference for GET /api/v1/subway/routes/{route}/stations. Get the ordered stations a subway route serves, optionally hydrated with arrivals.
Returns the ordered stations a subway route serves. Pass the route in the path (for example `L`) and an optional `direction` to order stations in the rider's direction of travel. Set `includeArrivals=true` to attach live arrivals to each station (bounded by `limitArrivals` and `limitStops`).
The response is a `RouteStopsResponse`: `{ route, mode, directions: [{ direction, headsigns?, stops: [...] }] }`, where each stop carries an optional `arrivals` array when hydrated.
### SDK
```typescript theme={null}
const stations = await mta.subway.routeStations({ route: 'L', direction: 'north' })
for (const pattern of stations.directions) {
for (const stop of pattern.stops) {
console.log(stop.name)
}
}
```
See the [Routes & Stations guide](/docs/guides/routes-and-stations).
# Get your API key and authenticate requests
Source: https://mtaapi.dev/docs/authentication
Learn how to get your free MTA API key, store it as an environment variable, and pass it to the mta-js client to authenticate every request.
mta-js uses an API key from [mtaapi.dev](https://www.mtaapi.dev) — the hosted version of this SDK — to authenticate every data request. You pass the key once when you create the `MTA` client, and the SDK handles the rest automatically. This page explains how to get a key, store it safely, and troubleshoot authentication failures.
Never hardcode your API key directly in source code. If you commit a key to a public repository, anyone can use it to make requests on your behalf. Always load keys from environment variables.
## Get your API key
API keys are free and issued through [mtaapi.dev](https://www.mtaapi.dev). Using the hosted API is the **fastest and easiest way** to use mta-js — no MTA developer registration, no BusTime key, no GTFS imports.
1. Go to [mtaapi.dev](https://www.mtaapi.dev) and sign up for a free account.
2. Copy your API key from the dashboard.
Your key is a long alphanumeric string. Keep it somewhere safe — you'll add it to your environment in the next step.
Prefer to self-host and call MTA feeds directly? See [Direct MTA feeds](/docs/#direct-mta-feeds) on the Quick Start page. The rest of this page assumes you're using the hosted [mtaapi.dev](https://www.mtaapi.dev) API.
## Configure your environment
Store your API key as an environment variable so it stays out of your source code.
Add the following line to a `.env` file at the root of your project:
```bash .env theme={null}
MTA_API_KEY=your_api_key_here
```
Then make sure `.env` is listed in your `.gitignore`:
```bash .gitignore theme={null}
.env
```
Most JavaScript frameworks (Next.js, Remix, Astro) and runtimes load `.env` files automatically. For plain Node.js projects, install [dotenv](https://github.com/motdotla/dotenv) and call `require('dotenv').config()` at the top of your entry file, or use the `--env-file` flag available in Node.js 20+.
## Initialize the client
Pass your API key to the `MTA` constructor as `apiKey`. With an `apiKey` set, requests are routed to `https://www.mtaapi.dev/api/v1` automatically.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
```
Create one client instance and reuse it across your application. Instantiating multiple clients is unnecessary and won't improve performance.
If you're working in a serverless environment (Vercel Functions, Cloudflare Workers, AWS Lambda), initialize the client inside the request handler to ensure your environment variables are available at invocation time:
```typescript theme={null}
import { MTA } from 'mta-js'
export async function GET(request: Request) {
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const arrivals = await mta.subway.arrivals({ stopId: 'A27', route: 'A' })
return Response.json(arrivals)
}
```
## Authentication errors
If your API key is missing, incorrect, or expired, the API returns an HTTP error and mta-js surfaces it as a thrown error.
| Status code | Meaning |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | The API key was not provided or is malformed. Check that `MTA_API_KEY` is set in your environment and that you're passing it correctly to the constructor. |
| `403 Forbidden` | The API key is recognized but not authorized to access the requested resource. Your key may have expired or been revoked. |
**Troubleshooting steps:**
1. Confirm the environment variable is set by logging `process.env.MTA_API_KEY` before initializing the client. It should not be `undefined`.
2. Double-check that the key you copied from [mtaapi.dev](https://www.mtaapi.dev) does not have leading or trailing whitespace.
3. If you recently regenerated your key, update it in every environment where it's used (local `.env`, hosting platform secrets, CI/CD variables).
4. If the problem persists, log in to [mtaapi.dev](https://www.mtaapi.dev) to verify your key is still active.
# Stop IDs, route IDs, and the MTA data model
Source: https://mtaapi.dev/docs/concepts/data-model
Learn how mta-js represents stops, routes, vehicles, and alerts—and how to find and use MTA stop IDs and route IDs in your SDK requests.
`mta-js `uses MTA's standard identifiers for stops and routes. These identifiers come directly from MTA's GTFS static dataset—the same dataset that powers Google Maps, Apple Maps, and most third-party transit apps. Understanding how stop IDs, route IDs, and transit modes work is essential for querying the right data and interpreting the responses you receive.
## Stop IDs
Every subway and bus stop in the MTA system has a unique stop ID defined in MTA's GTFS static feed. Stop IDs are alphanumeric strings that identify a specific physical platform or stop location.
For subway stops, the ID typically combines a letter prefix and a numeric suffix. For example, `A27` identifies the Howard Beach–JFK Airport station on the A line. The same physical station can have multiple stop IDs if it serves multiple lines or has separate platforms for each direction.
```typescript theme={null}
// Fetch arrivals at Howard Beach (A line, southbound platform)
const arrivals = await mta.subway.arrivals({
stopId: 'A27',
route: 'A'
})
```
`stopId` and `route` parameters across the SDK are typed as smart-string unions (e.g. `SubwayStopId`, `SubwayRoute`) — you get autocomplete for known IDs without being locked out of new or experimental values. See [Typed route and stop IDs](#typed-route-and-stop-ids) below.
You can look up stop IDs in MTA's official GTFS reference, available at [api.mta.info](https://api.mta.info). The `stops.txt` file in the GTFS static download contains every stop ID, name, and geographic coordinate in the system.
Bus stop IDs follow a similar convention and are found in the same GTFS static dataset, in the `stops.txt` file.
## Route IDs
Route IDs identify a specific subway line or bus route. mta-js uses these identifiers in method parameters to filter results to a single route.
**Subway route IDs** match the line letter or number you see on signage and subway maps:
| Route | Description |
| ------------------ | -------------------------- |
| `A`, `C`, `E` | A/C/E lines (8th Avenue) |
| `B`, `D`, `F`, `M` | B/D/F/M lines (6th Avenue) |
| `1`, `2`, `3` | 1/2/3 lines (7th Avenue) |
| `L` | L line (Canarsie) |
| `G` | G line (Crosstown) |
**Bus route IDs** use the MTA route designation, which includes a borough prefix and route number:
| Route | Description |
| ------ | ----------------------------------- |
| `B63` | B63 bus (Brooklyn, 5th Avenue) |
| `M15` | M15 bus (Manhattan, 1st/2nd Avenue) |
| `Q58` | Q58 bus (Queens) |
| `Bx12` | Bx12 bus (Bronx, Pelham Pkwy) |
`route` parameters across the SDK are typed as smart-string unions. Subway-specific methods narrow to `SubwayRoute`, bus-specific methods to `BusRoute`, and pan-mode methods (alerts, stops) accept `RouteId`. You get autocomplete for valid IDs without losing the freedom to pass any string. See [Typed route and stop IDs](#typed-route-and-stop-ids) below.
```typescript theme={null}
// Subway: filter to a specific route — `route` is SubwayRoute
const subwayArrivals = await mta.subway.arrivals({ stopId: '120', route: '1' })
// Bus: get vehicle positions for a route — `route` is BusRoute
const busPositions = await mta.bus.vehicles({ route: 'M15' })
```
## Transit modes and directions
```typescript theme={null}
type TransitMode = 'subway' | 'bus' | 'lirr' | 'metro-north'
type Direction = 'north' | 'south' | 'east' | 'west' | 'unknown'
```
Methods that scope to a specific system accept a `mode` (or `modes`) parameter. The two most commonly used values are:
* `'subway'` — the NYC subway system
* `'bus'` — the NYC bus network (local, express, and SBS routes)
`'lirr'` and `'metro-north'` are also valid for service alerts. `mta.subway.arrivals` additionally accepts `'uptown'` and `'downtown'` as direction aliases — they're normalized into the underlying `Direction` value for you.
```typescript theme={null}
// Fetch only subway service alerts
const subwayAlerts = await mta.alerts.current({ mode: 'subway' })
// Fetch stops near a location, searching both subway and bus
const nearby = await mta.stops.near({
lat: 40.6501,
lon: -73.9496,
modes: ['subway', 'bus']
})
```
## Timestamps
Time values in `mta-js` responses are returned as **ISO 8601 strings** (e.g. `'2026-05-26T17:12:29.787Z'`), so you can pass them straight to `new Date()` without unit conversion.
```typescript theme={null}
const { arrivals } = await mta.subway.arrivals({ stopId: 'A27', route: 'A' })
for (const arrival of arrivals) {
const arrivalDate = new Date(arrival.arrivalTime)
console.log(`${arrival.route.id} arrives at ${arrivalDate.toLocaleTimeString()} (in ${arrival.minutes} min)`)
}
```
Each `Arrival` also includes a precomputed `minutes` field for the common "minutes until arrival" use case, so you usually don't have to do the math yourself.
## TypeScript interfaces
`mta-js` ships with full TypeScript type definitions. Every response is a structured object — routes and stops are nested, not flat strings.
```typescript theme={null}
interface Route {
id: string
shortName?: string
longName?: string
color?: string
textColor?: string
type?: number
}
interface Stop {
id: string
name: string
lat?: number
lon?: number
parentStation?: string
mode?: TransitMode
}
type NearbyStop = Stop & {
distanceMeters?: number
servedRoutes?: Route[]
routeMatch?: boolean
routeHeadsigns?: string[]
note?: string
}
interface Arrival {
mode: TransitMode
route: Route
stop: Stop
direction: Direction
headsign?: string
arrivalTime: string // ISO 8601 timestamp
departureTime?: string // ISO 8601 timestamp
minutes: number // Minutes until arrival
tripId?: string
realtime: boolean
source: 'mta-gtfs-rt' | 'mta-bustime'
raw?: unknown // Present when includeRaw: true
}
interface Vehicle {
mode: TransitMode
route: Route
vehicleId?: string
tripId?: string
stop?: Stop // Next or current stop
lat?: number
lon?: number
bearing?: number // Degrees clockwise from north
destinationName?: string
recordedAt?: string // ISO 8601 timestamp
source: 'mta-bustime'
raw?: unknown
}
interface Alert {
id: string
mode?: TransitMode
routes: Route[]
stops: Stop[]
header?: string
description?: string
url?: string
effect?: string
severity?: string
activePeriods: { start?: string; end?: string }[]
source: 'mta-gtfs-rt'
raw?: unknown
}
```
Query parameter shapes:
```typescript theme={null}
interface SubwayArrivalQuery {
stopId: SubwayStopId
route?: SubwayRoute
direction?: Direction | 'uptown' | 'downtown'
limit?: number
includeRaw?: boolean
}
interface BusArrivalQuery {
stopId: BusStopId
route?: BusRoute
limit?: number
includeRaw?: boolean
}
interface BusVehicleQuery {
route?: BusRoute
vehicleId?: string
limit?: number
includeRaw?: boolean
}
interface AlertQuery {
mode?: TransitMode
route?: RouteId
stopId?: StopId
includeRaw?: boolean
}
interface StopsNearQuery {
lat: number
lon: number
modes?: TransitMode[]
route?: RouteId
includeRoutes?: boolean
radiusMeters?: number
limit?: number
}
```
All types are exported from `mta-js` for use in your own annotations:
```typescript theme={null}
import type {
Arrival,
Alert,
Vehicle,
Stop,
NearbyStop,
Route,
TransitMode,
Direction,
} from 'mta-js'
```
## Typed route and stop IDs
`mta-js` exports literal union types generated from the live hosted stops snapshot, plus **"smart string" wrappers** that let you pass any string while still getting autocomplete for known values. Every `stopId` and `route` parameter on the SDK uses these wrappers, so your editor suggests valid IDs as you type — but you can still pass new or experimental IDs without fighting the type system.
```typescript theme={null}
import type {
// Smart-string types used by SDK params (autocomplete + accept any string)
RouteId,
SubwayRoute,
BusRoute,
StopId,
SubwayStopId,
BusStopId,
// Underlying generated literal unions (strict)
KnownRoute,
KnownSubwayRoute,
KnownBusRoute,
KnownStopId,
KnownSubwayStopId,
KnownBusStopId,
} from 'mta-js'
```
| Type | Definition | Use for |
| -------------- | ------------------- | ---------------------------- |
| `RouteId` | \`KnownRoute | Any route ID (subway + bus). |
| `SubwayRoute` | \`KnownSubwayRoute | Subway-only route IDs. |
| `BusRoute` | \`KnownBusRoute | Bus-only route IDs. |
| `StopId` | \`KnownStopId | Any stop ID. |
| `SubwayStopId` | \`KnownSubwayStopId | Subway-only stop IDs. |
| `BusStopId` | \`KnownBusStopId | Bus-only stop IDs. |
The `(string & {})` intersection is the TypeScript trick that preserves autocomplete for the known literals while still allowing any string at the type level.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
// Autocomplete suggests 'L', 'L08', etc., but unknown strings are also accepted
await mta.subway.arrivals({ stopId: 'L08', route: 'L' })
await mta.bus.vehicles({ route: 'M23-SBS' })
```
If you want **stricter compile-time enforcement** that rejects any value not in the snapshot, annotate against the `Known*` unions directly:
```typescript theme={null}
import type { KnownSubwayRoute, KnownSubwayStopId } from 'mta-js'
async function getArrivals(stopId: KnownSubwayStopId, route: KnownSubwayRoute) {
return mta.subway.arrivals({ stopId, route })
}
await getArrivals('L08', 'L') // ✓ valid
// await getArrivals('L08', 'XX') // ✗ TS error: not assignable to KnownSubwayRoute
```
These literal unions are **regenerated from the hosted snapshot** at `mtaapi.dev`, so they always match the routes and stops the API currently knows about. Update `mta-js` to pick up newly added routes or stops.
Prefer the narrowest type that matches your call site — `KnownSubwayRoute` over `KnownRoute` when you're only working with subway, etc. The narrower the type, the more autocomplete and the fewer runtime surprises.
# mta-js architecture: client model and namespaces
Source: https://mtaapi.dev/docs/concepts/overview
Understand mta-js's unified client model, how it connects to MTA's GTFS-RT data feeds, and how the four namespaces are organized.
mta-js wraps MTA's real-time GTFS feeds into a single, consistent interface. You create one `MTA` client, and from it you access four namespaces—`subway`, `bus`, `alerts`, and `stops`—each responsible for a distinct slice of NYC transit data. Under the hood, the SDK fetches live protocol buffer feeds from MTA's servers, decodes them, and returns typed JavaScript objects you can work with immediately.
## The MTA client
Everything starts with a single client instance. Pass an `apiKey` from [mtaapi.dev](https://www.mtaapi.dev) and the client handles authentication and request routing for you.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
```
You only need one instance per application. Create it once — at module load time or inside a shared singleton — and reuse it across all your requests.
You can also call MTA feeds directly by passing `busTimeKey` instead of `apiKey`. See [Direct MTA feeds](/docs/configuration/setup) for the full options.
Your API key is sent with every request. Keep it in an environment variable and never commit it to source control.
## Namespaces
The four namespaces cover the full scope of MTA's publicly available real-time data. Each namespace exposes focused methods that map directly to a category of MTA feed.
Real-time arrival predictions for subway stops (`subway.arrivals()`), plus a destination-to-direction resolver ([`subway.direction()`](/docs/api-reference/direction)) that turns a rider's destination into a north/south direction.
Live vehicle positions for bus routes. Returns the current geographic location and status of every active bus on a given route.
Active service alerts for subway and bus. Includes delays, planned work, elevator outages, and emergency notices.
Stop discovery by geolocation. Given a latitude and longitude, returns nearby subway and bus stops sorted by distance.
## Data freshness
All four namespaces draw from MTA's GTFS Realtime (GTFS-RT) feeds—standardized protocol buffer streams that MTA publishes continuously. Subway arrival feeds typically refresh every 30 seconds. Bus position feeds update every 30 to 60 seconds. Alert feeds update as changes are published by MTA operations staff.
When you call any method on the client, mta-js fetches the latest available snapshot of the relevant feed and returns it decoded. The SDK does not maintain a persistent connection or push updates to your application—you are responsible for polling at an appropriate interval for your use case.
## Error handling
mta-js throws typed errors when a request fails, a feed is unavailable, or you pass invalid parameters. Wrap your calls in a `try/catch` block to handle these cases gracefully.
```typescript theme={null}
import { MTA, MtaError } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
try {
const arrivals = await mta.subway.arrivals({ stopId: 'A27', route: 'A' })
console.log(arrivals)
} catch (error) {
if (error instanceof MtaError) {
console.error(`MTA error ${error.code}: ${error.message}`)
} else {
throw error
}
}
```
Typed errors give you the error code and a human-readable message, making it straightforward to distinguish between a bad stop ID, a network failure, and a temporarily unavailable feed.
# Real-time data feeds and update frequency
Source: https://mtaapi.dev/docs/concepts/real-time-feeds
Learn how mta-js fetches live MTA GTFS-RT feeds, how often data refreshes, and what to expect when feeds are delayed or unavailable.
mta-js reads from MTA's GTFS Realtime (GTFS-RT) protocol buffer feeds to provide live transit data. Understanding how these feeds work—what they publish, how often they update, and when they can fail—helps you build applications that handle real-world conditions gracefully rather than assuming data is always fresh and complete.
## GTFS-RT feeds
GTFS Realtime is an open standard, defined by Google and maintained by the transit community, for publishing real-time transit updates on top of a GTFS static dataset. A GTFS-RT feed is a binary protocol buffer stream that a transit agency publishes at a fixed URL. Clients fetch the feed, decode it, and process the updates contained within.
MTA publishes separate GTFS-RT feeds for subway and bus:
* **Subway feeds** provide trip updates (arrival and departure predictions) and vehicle positions for each subway division. The A/C/E lines, for example, are on a different feed endpoint than the 1/2/3 lines. mta-js handles feed routing automatically—you query by stop ID or route ID and the SDK fetches the correct feed.
* **Bus feeds** provide vehicle positions and trip updates for the full bus network, including local, express, and Select Bus Service routes.
* **Alert feeds** publish service alerts covering both subway and bus in a single feed.
mta-js fetches, decodes, and normalizes these feeds into the TypeScript objects described in the [data model](/docs/concepts/data-model).
## Update frequency
Feed update frequency varies by mode. Plan your polling intervals around these approximate schedules:
| Feed | Typical update interval |
| --------------------- | ----------------------- |
| Subway arrivals | Every 30 seconds |
| Bus vehicle positions | Every 30–60 seconds |
| Service alerts | As changes occur |
Subway feeds are generally the most consistent. Bus feed latency can vary depending on network conditions and the volume of active vehicles. Alert feeds are event-driven rather than time-driven—a new alert may appear within seconds of MTA publishing it, or it may lag by a few minutes.
These intervals reflect MTA's typical publishing cadence and are not guaranteed. During high-traffic periods or system incidents, feeds may update more slowly or temporarily stop publishing.
## Data availability
Not all routes and stops have equal coverage in MTA's real-time feeds. Most subway lines publish arrival predictions reliably, but some service patterns—late-night shuttles, planned work reroutes, and certain express services—may produce sparse or missing predictions. Bus coverage is generally comprehensive for routes that run GTFS-compatible vehicles.
When a stop or route has no current data, mta-js returns an empty array rather than throwing an error. Always write your application to handle empty arrays gracefully:
```typescript theme={null}
const arrivals = await mta.subway.arrivals({ stopId: 'A27', route: 'A' })
if (arrivals.length === 0) {
console.log('No arrival predictions available for this stop right now.')
} else {
for (const arrival of arrivals) {
const eta = new Date(arrival.arrivalTime * 1000)
console.log(`${arrival.routeId} train arriving at ${eta.toLocaleTimeString()}`)
}
}
```
## Handling stale data
Because mta-js fetches a snapshot of the feed on each call, your application must poll to stay current. A single fetch gives you the state of the feed at that moment; it does not update automatically afterward.
Poll at an interval that matches the feed's update cadence. For subway arrivals, polling every 30 seconds keeps your data roughly in sync with MTA's feed. Polling more frequently wastes requests without gaining fresher data; polling much less frequently means your UI can show predictions that are significantly out of date.
Here is a simple polling pattern using `setInterval`:
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA(process.env.MTA_API_KEY)
async function fetchArrivals() {
try {
const arrivals = await mta.subway.arrivals({ stopId: 'A27', route: 'A' })
renderArrivals(arrivals)
} catch (error) {
console.error('Failed to fetch arrivals:', error)
// Continue polling — a single failure should not stop your loop
}
}
// Fetch immediately, then every 30 seconds
fetchArrivals()
const interval = setInterval(fetchArrivals, 30_000)
// To stop polling:
// clearInterval(interval)
```
In serverless environments where long-running intervals are not possible, trigger a fetch on each incoming request and rely on a cache layer—such as a short-lived Redis key or an edge cache—to avoid hammering MTA's feed endpoints on every user request.
mta-js caches MTA's GTFS static data locally to avoid re-downloading the full dataset on every startup. On first run, the SDK may take a few extra seconds to hydrate this cache. Subsequent starts use the cached data and initialize significantly faster.
# Configure the mta-js client and options
Source: https://mtaapi.dev/docs/configuration/setup
Customize the mta-js client with options for database backend, response cache duration, and request timeouts to fit your application's needs.
**The fastest way to use mta-js is the hosted API at [mtaapi.dev](https://www.mtaapi.dev).** Pass `apiKey` and you're done — no BusTime key, no GTFS imports, no stop search setup.
Self-host only when you need full control over the data path, want to deploy in an air-gapped environment, or have already invested in MTA developer credentials. Everything below is for that case.
In direct-feed mode, mta-js talks straight to MTA's realtime GTFS-RT and BusTime APIs. The public method names (`mta.subway.arrivals`, `mta.bus.arrivals`, `mta.bus.vehicles`, `mta.alerts.current`, `mta.stops.near`) stay the same — only the constructor changes.
Direct-feed mode has **no bundled database** in mta-js 2.0. There's no SQLite, no Turso, and no persistent GTFS storage. Stop lookups rely on the optional in-memory `staticData` seed described below. For production stop search, prefer the hosted API — it serves a compact Blob-backed snapshot so SDK consumers don't have to manage GTFS imports.
## Direct-feed client
Pass a `busTimeKey` instead of an `apiKey`:
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({
busTimeKey: process.env.MTA_BUS_KEY,
})
await mta.subway.arrivals({ stopId: 'L08', route: 'L' })
await mta.bus.arrivals({ stopId: '308214', route: 'M23' })
await mta.bus.vehicles({ route: 'B63' })
await mta.alerts.current({ mode: 'subway' })
```
Subway feeds and service alerts work without any key. `busTimeKey` is only required for bus realtime endpoints (`mta.bus.arrivals`, `mta.bus.vehicles`) — request one from the [MTA BusTime developer portal](https://bustime.mta.info/wiki/Developers/Index).
## Adding stop and route metadata
Without the hosted API, mta-js has no built-in GTFS database, so stop and route names won't resolve automatically. Pass a small in-memory `staticData` seed to get richer local metadata for the stops you care about:
```typescript theme={null}
const mta = new MTA({
busTimeKey: process.env.MTA_BUS_KEY,
staticData: {
stops: [
{
stop_id: 'L08',
stop_name: 'Bedford Av',
stop_lat: 40.717304,
stop_lon: -73.956872,
},
],
routes: [
{
route_id: 'L',
route_short_name: 'L',
route_long_name: '14 St-Canarsie Local',
},
],
},
staticDataMode: 'subway',
})
```
This is intended for a handful of well-known stops, internal tooling, or tests. **It is not a replacement for full GTFS static data.** If you need comprehensive stop search across the system, use the hosted API.
## Constructor options
| Option | Type | Description | |
| ---------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `apiKey` | `string` | Hosted [mtaapi.dev](https://www.mtaapi.dev) API key. When set, requests go to `https://www.mtaapi.dev` and other realtime options are ignored. | |
| `apiBaseUrl` | `string` | Override the hosted API base URL. Useful for tests, staging, or private deployments. Only applies with `apiKey`. | |
| `busTimeKey` | `string` | MTA BusTime API key. Required only for bus realtime endpoints in direct-feed mode. | |
| `staticData` | `object` | Optional in-memory seed of `stops` and `routes` for direct-feed mode. | |
| `staticDataMode` | \`'subway' | 'bus'\` | Which transit mode the `staticData` seed applies to. |
## Framework integration
```typescript theme={null}
// src/mta.ts
import { MTA } from 'mta-js'
export const mta = new MTA({
busTimeKey: process.env.MTA_BUS_KEY,
})
```
Direct-feed mta-js works in both Node.js runtime and Edge runtime — there's no SQLite to require Node APIs anymore.
```typescript theme={null}
// app/api/arrivals/route.ts
import { MTA } from 'mta-js'
const mta = new MTA({
busTimeKey: process.env.MTA_BUS_KEY,
})
export async function GET() {
const arrivals = await mta.subway.arrivals({ stopId: 'L08', route: 'L' })
return Response.json(arrivals)
}
```
```typescript theme={null}
import express from 'express'
import { MTA } from 'mta-js'
const app = express()
const mta = new MTA({ busTimeKey: process.env.MTA_BUS_KEY })
app.get('/arrivals', async (req, res) => {
const { stopId, route } = req.query as { stopId: string; route: string }
res.json(await mta.subway.arrivals({ stopId, route }))
})
```
Still deciding? **Start with [mtaapi.dev](https://www.mtaapi.dev)**. You can switch to direct-feed mode later without changing your application code — only the constructor options change.
# Build a live departure board
Source: https://mtaapi.dev/docs/guides/arrival-boards
Use mta.subway.arrivalBoard() and mta.bus.arrivalBoard() to render a departure board: the nearest stops to a coordinate, each with its next arrivals grouped by direction or route.
An arrival board answers "what's leaving near me, right now?" in a single call. Instead of finding nearby stops and then making one arrivals request per stop, `mta.subway.arrivalBoard()` and `mta.bus.arrivalBoard()` return the closest stations to a coordinate with their upcoming arrivals already attached and grouped — perfect for a lobby screen, a "near me" tab, or a kiosk.
Arrival boards require a hosted `apiKey`. See [Authentication](/docs/authentication) to get one.
## Prerequisites
* `mta-js` installed (`npm install mta-js`)
* An MTA API key set as `MTA_API_KEY` in your environment
Pass a `lat` / `lon` and how big you want the board. `limitStations` caps how many stations come back (max 20); `limitArrivals` caps arrivals per direction (max 10).
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const board = await mta.subway.arrivalBoard({
lat: 40.7356,
lon: -73.9906,
limitStations: 5,
limitArrivals: 3,
})
```
Each entry has a `station`, its `distanceMeters`, and a `directions` array. Each direction bundles a `direction`, an optional `headsign`, and that direction's `arrivals`.
```typescript theme={null}
for (const { station, distanceMeters, directions } of board) {
console.log(`${station.name} (${Math.round(distanceMeters)}m)`)
for (const dir of directions) {
for (const arrival of dir.arrivals) {
console.log(` ${arrival.route.shortName} ${dir.headsign ?? dir.direction} — ${arrival.minutes} min`)
}
}
}
```
`mta.bus.arrivalBoard()` is the bus counterpart. It uses `limitStops` instead of `limitStations`, and groups each stop's arrivals by `route` (with an optional `headsign`) rather than by compass direction.
```typescript theme={null}
const busBoard = await mta.bus.arrivalBoard({
lat: 40.7421,
lon: -73.9914,
route: 'M23',
limitStops: 3,
limitArrivals: 2,
})
for (const { stop, routes } of busBoard) {
for (const r of routes) {
console.log(`${stop.name} — ${r.route.shortName} ${r.headsign ?? ''}`)
}
}
```
Poll the board every 30 seconds to stay in sync with the realtime feed. Keep the limits small for a glanceable display — 5 stations with 3 arrivals each reads well on a single screen.
Prefer the raw REST shape? See the [Subway Arrival Board](/docs/api-reference/subway-arrival-board) and [Bus Arrival Board](/docs/api-reference/bus-arrival-board) API reference pages.
# Track live bus vehicle positions by route
Source: https://mtaapi.dev/docs/guides/bus-tracking
Use mta-js to get real-time vehicle positions for any MTA bus route, including GPS coordinates, heading, next stop, and occupancy status.
This guide shows you how to fetch live bus vehicle positions for a specific route. You will call `mta.bus.vehicles()` with a route ID, extract latitude and longitude from each vehicle, and display them in a list or on a map.
MTA bus route IDs combine a borough prefix with the route number. The prefix identifies which borough the route primarily serves.
| Prefix | Borough |
| ------ | ------------- |
| `B` | Brooklyn |
| `M` | Manhattan |
| `Q` | Queens |
| `Bx` | The Bronx |
| `S` | Staten Island |
Some common examples:
| Route ID | Route |
| -------- | -------------------------------- |
| `B63` | 5th Ave / Atlantic Ave, Brooklyn |
| `M15` | 1st / 2nd Ave, Manhattan |
| `Q58` | Myrtle Ave, Queens |
| `Bx12` | Fordham Road, The Bronx |
Pass the route ID as a string, using the exact casing shown above (e.g., `'Bx12'`, not `'BX12'`).
Initialize the `MTA` client and call `mta.bus.vehicles()` with the `route` you want to track.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const data = await mta.bus.vehicles({ route: 'B63' })
```
The response contains a `vehicles` array. Map over it to extract each bus's position and relevant metadata.
```typescript theme={null}
const positions = data.vehicles.map((vehicle) => ({
id: vehicle.vehicleId,
lat: vehicle.lat,
lon: vehicle.lon,
bearing: vehicle.bearing,
nextStop: vehicle.nextStop,
occupancy: vehicle.occupancyStatus,
}))
for (const bus of positions) {
console.log(
`Bus ${bus.id} — (${bus.lat}, ${bus.lon}) — next stop: ${bus.nextStop}`
)
}
```
**Example response:**
```json theme={null}
{
"route": "B63",
"vehicles": [
{
"vehicleId": "MTA_8432",
"lat": 40.6782,
"lon": -73.9442,
"bearing": 180,
"occupancyStatus": "MANY_SEATS_AVAILABLE",
"nextStop": "5th Ave & Atlantic Ave"
},
{
"vehicleId": "MTA_8917",
"lat": 40.6651,
"lon": -73.9890,
"bearing": 0,
"occupancyStatus": "FEW_SEATS_AVAILABLE",
"nextStop": "5th Ave & 39th St"
}
]
}
```
Use the `lat` and `lon` values to place markers on a mapping library of your choice. The `bearing` field gives the vehicle's heading in degrees (0 = north, 90 = east), which you can use to rotate a bus icon.
```typescript theme={null}
// Example: add markers to a Leaflet map
for (const bus of positions) {
L.marker([bus.lat, bus.lon])
.bindPopup(`Bus ${bus.id}
Next stop: ${bus.nextStop}`)
.addTo(map)
}
```
For a simple text list, sort vehicles by proximity to a reference point using the Haversine formula or a geospatial library.
## Complete example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
interface BusPosition {
vehicleId: string
lat: number
lon: number
bearing: number
occupancyStatus: string
nextStop: string
}
async function getBusPositions(route: string): Promise {
const data = await mta.bus.vehicles({ route })
return data.vehicles
}
async function displayBusRoute(route: string): Promise {
try {
const vehicles = await getBusPositions(route)
if (vehicles.length === 0) {
console.log(`No active buses found on route ${route}.`)
return
}
console.log(`${vehicles.length} active buses on route ${route}:\n`)
for (const bus of vehicles) {
console.log(`Bus ${bus.vehicleId}`)
console.log(`Position: ${bus.lat}, ${bus.lon}`)
console.log(`Heading: ${bus.bearing}°`)
console.log(`Next stop: ${bus.nextStop}`)
console.log(`Occupancy: ${bus.occupancyStatus}\n`)
}
} catch (error) {
console.error(`Failed to fetch vehicles for route ${route}:`, error)
}
}
await displayBusRoute('B63')
```
## Occupancy status
The `occupancyStatus` field reflects how full a bus is, based on passenger load data reported by the vehicle. The possible values follow the GTFS-RT occupancy standard:
| Value | Meaning |
| ---------------------------- | --------------------------------- |
| `EMPTY` | No passengers on board |
| `MANY_SEATS_AVAILABLE` | Plenty of open seats |
| `FEW_SEATS_AVAILABLE` | Seats limited |
| `STANDING_ROOM_ONLY` | No seats, standing room available |
| `CRUSHED_STANDING_ROOM_ONLY` | Very crowded |
| `FULL` | Bus is not accepting passengers |
| `NOT_ACCEPTING_PASSENGERS` | Out of service or not boarding |
Not all vehicles report occupancy data. When the field is absent or `null`, omit it from your UI rather than showing a default value.
Combine `mta.bus.vehicles()` with `mta.stops.near()` to show the upcoming stops along a vehicle's path. Fetch nearby stops using the bus's current `lat` and `lon`, then pass the closest `stopId` into `mta.subway.arrivals()` or render the stop list alongside the bus position.
```typescript theme={null}
const nearbyStops = await mta.stops.near({
lat: bus.lat,
lon: bus.lon,
modes: ['bus'],
})
```
# Turn a rider's destination into a direction
Source: https://mtaapi.dev/docs/guides/direction
Use mta.subway.direction() to resolve a rider-facing destination like "Union Sq" into the north/south feed direction, then feed it straight into subway arrivals.
NYC subway realtime feeds describe trains by NYCT's `north` / `south` direction — even on east–west lines like the L. But riders don't think in compass directions; they think "I'm trying to get to Union Sq." This guide shows you how to use `mta.subway.direction()` to translate a destination string into the feed direction, then use that direction to fetch the right arrivals.
`mta.subway.direction()` requires a hosted `apiKey`. It resolves destinations against the hosted API's static GTFS route order and is not available in direct-feed mode. See [Authentication](/docs/authentication) to get a key.
## Prerequisites
* `mta-js` installed (`npm install mta-js`)
* An MTA API key set as `MTA_API_KEY` in your environment
Call `mta.subway.direction()` with the `route` the rider is on, the `fromStopId` they're departing from, and the `destination` they typed. The method resolves the destination against the route's stop order and returns the `north` / `south` direction that heads toward it.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
// A rider at Bedford Av (L08) wants to reach Union Sq — which way do they go?
const resolution = await mta.subway.direction({
route: 'L',
fromStopId: 'L08',
destination: 'Union Sq',
})
```
All three parameters are required:
| Parameter | Type | Description |
| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `route` | `string` | The subway route the rider is traveling on (e.g. `'L'`). |
| `fromStopId` | `string` | The stop the rider is departing from (e.g. `'L08'` for Bedford Av). |
| `destination` | `string` | A rider-facing destination to resolve (e.g. `'Union Sq'`). Matching tolerates casing and common station-name variations. |
The method always resolves to a `SubwayDirectionResolution` object — it never returns `null`. Check the `resolved` flag before reading the direction.
```typescript theme={null}
if (resolution.resolved) {
console.log(resolution.direction) // "north"
console.log(resolution.displayDirection) // "toward 8 Av"
console.log(resolution.terminal) // "8 Av"
} else {
console.warn(`Couldn't resolve a direction: ${resolution.reason}`)
// resolution.matches may list ambiguous candidate stops
}
```
When `resolved` is `true`, the `direction`, `displayDirection`, `terminal`, and `destinationStop` fields are populated. When it's `false`, `reason` explains why, and `matches` may contain ambiguous candidate stops you can disambiguate in your UI.
Pass the resolved `direction` straight into `mta.subway.arrivals()` to show only the trains heading the rider's way.
```typescript theme={null}
if (resolution.resolved) {
const arrivals = await mta.subway.arrivals({
stopId: 'L08',
route: 'L',
direction: resolution.direction,
})
for (const arrival of arrivals) {
console.log(`${arrival.route.shortName} — ${arrival.displayDirection} — ${arrival.minutes} min`)
}
}
```
## Complete example
This helper takes a rider's origin, route, and destination and returns the next trains heading the right way — or a helpful message when the destination can't be resolved.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
async function arrivalsTowardDestination(
route: string,
fromStopId: string,
destination: string,
) {
const resolution = await mta.subway.direction({ route, fromStopId, destination })
if (!resolution.resolved) {
return {
ok: false as const,
reason: resolution.reason ?? 'Could not resolve a direction.',
candidates: resolution.matches ?? [],
}
}
const arrivals = await mta.subway.arrivals({
stopId: fromStopId,
route,
direction: resolution.direction,
})
return {
ok: true as const,
heading: resolution.displayDirection, // e.g. "toward 8 Av"
terminal: resolution.terminal, // e.g. "8 Av"
arrivals,
}
}
const result = await arrivalsTowardDestination('L', 'L08', 'Union Sq')
if (result.ok) {
console.log(`Take the train ${result.heading}:`)
for (const a of result.arrivals) {
console.log(` ${a.route.shortName} — ${a.minutes} min`)
}
} else {
console.warn(result.reason)
}
```
## Example resolution
```json theme={null}
{
"route": { "id": "L", "shortName": "L", "color": "#A7A9AC" },
"destination": "Union Sq",
"normalizedDestination": "union sq",
"resolved": true,
"direction": "north",
"displayDirection": "toward 8 Av",
"terminal": "8 Av",
"fromStop": { "id": "L08", "name": "Bedford Av" },
"destinationStop": { "id": "L02", "name": "14 St-Union Sq" }
}
```
## Handling ambiguous or unresolved destinations
When a destination string matches more than one stop — or none — `resolved` is `false`. Use `reason` for a short explanation and `matches` to offer the rider a choice.
```typescript theme={null}
const resolution = await mta.subway.direction({
route: 'L',
fromStopId: 'L08',
destination: '1 Av',
})
if (!resolution.resolved && resolution.matches?.length) {
console.log('Did you mean one of these stops?')
for (const stop of resolution.matches) {
console.log(` ${stop.name} (${stop.id})`)
}
}
```
`mta.subway.arrivals()` also accepts the rider-facing aliases `'uptown'` / `'downtown'` (and `'east'` / `'west'` on some lines), which it maps to the underlying feed direction for you. Use `mta.subway.direction()` when you only know *where the rider wants to go*, and the aliases when the rider already knows which way they're heading.
Subway feed directions are always `north` / `south`, even on east–west lines like the L. `mta.subway.direction()` resolves to one of those two values so it can be passed directly to [`mta.subway.arrivals()`](/docs/api-reference/subway). For the full parameter and type reference, see the [Direction API reference](/docs/api-reference/direction).
# Find MTA stops near any latitude and longitude
Source: https://mtaapi.dev/docs/guides/nearby-stops
Use mta-js to discover nearby subway and bus stops for a given latitude and longitude, with support for filtering by transit mode.
This guide shows you how to find MTA stops near a geographic coordinate using `mta.stops.near()`. You will obtain a latitude and longitude, query for nearby stops across one or more transit modes, and use the results to build features like "stops near me" or a stop picker for arrival lookups.
You need a latitude and longitude to query nearby stops. In a browser, use the Geolocation API to get the user's current position.
```typescript theme={null}
function getCurrentPosition(): Promise {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation is not supported by this browser.'))
return
}
navigator.geolocation.getCurrentPosition(
(position) => resolve(position.coords),
(error) => reject(error)
)
})
}
const coords = await getCurrentPosition()
const { latitude: lat, longitude: lon } = coords
```
For server-side code or testing, use hardcoded coordinates:
```typescript theme={null}
// Times Square, Manhattan
const lat = 40.7580
const lon = -73.9855
```
Pass `lat`, `lon`, and a `modes` array to `mta.stops.near()`. The `modes` array accepts `'subway'`, `'bus'`, or both.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const data = await mta.stops.near({
lat: 40.7580,
lon: -73.9855,
modes: ['subway', 'bus'],
})
```
**Example response:**
```json theme={null}
{
"stops": [
{
"stopId": "R16",
"name": "Times Sq-42 St",
"lat": 40.7549,
"lon": -73.9870,
"mode": "subway",
"routes": ["N", "Q", "R", "W"],
"distanceMeters": 348
},
{
"stopId": "902",
"name": "Times Sq-42 St",
"lat": 40.7553,
"lon": -73.9877,
"mode": "subway",
"routes": ["1", "2", "3"],
"distanceMeters": 412
},
{
"stopId": "MTA_305423",
"name": "8 Av & W 42 St",
"lat": 40.7572,
"lon": -73.9910,
"mode": "bus",
"routes": ["M42"],
"distanceMeters": 517
}
]
}
```
The response is typically already sorted by `distanceMeters`. Map over the stops to build a display list.
```typescript theme={null}
const { stops } = data
for (const stop of stops) {
const distance =
stop.distanceMeters < 1000
? `${stop.distanceMeters}m`
: `${(stop.distanceMeters / 1000).toFixed(1)}km`
console.log(`${stop.name} (${stop.mode.toUpperCase()})`)
console.log(` Routes: ${stop.routes.join(', ')}`)
console.log(` Distance: ${distance}`)
console.log(` Stop ID: ${stop.stopId}`)
}
```
Once you have a `stopId`, you can pass it directly to `mta.subway.arrivals()` to fetch real-time arrivals for that stop. This lets you build a "tap a stop, see arrivals" flow.
```typescript theme={null}
const nearestSubwayStop = stops.find((s) => s.mode === 'subway')
if (nearestSubwayStop) {
const arrivals = await mta.subway.arrivals({
stopId: nearestSubwayStop.stopId,
route: nearestSubwayStop.routes[0],
})
console.log(arrivals)
}
```
## Complete example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
interface NearbyStop {
stopId: string
name: string
lat: number
lon: number
mode: string
routes: string[]
distanceMeters: number
}
function getCurrentPosition(): Promise {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation is not supported by this browser.'))
return
}
navigator.geolocation.getCurrentPosition(
(position) => resolve(position.coords),
(error) => reject(error)
)
})
}
async function findNearbyStops(
modes: Array<'subway' | 'bus'> = ['subway', 'bus']
): Promise {
const coords = await getCurrentPosition()
const data = await mta.stops.near({
lat: coords.latitude,
lon: coords.longitude,
modes,
})
return data.stops
}
async function showNearbyArrivals(): Promise {
try {
const stops = await findNearbyStops(['subway'])
if (stops.length === 0) {
console.log('No subway stops found nearby.')
return
}
const nearest = stops[0]
console.log(`Nearest stop: ${nearest.name} (${nearest.distanceMeters}m away)`)
console.log(`Routes: ${nearest.routes.join(', ')}`)
// Fetch arrivals for the first route at the nearest stop
const arrivals = await mta.subway.arrivals({
stopId: nearest.stopId,
route: nearest.routes[0],
})
const now = Math.floor(Date.now() / 1000)
for (const arrival of arrivals.arrivals) {
const minutesAway = Math.round((arrival.arrivalTime - now) / 60)
console.log(` ${nearest.routes[0]} train — ${minutesAway} min`)
}
} catch (error) {
console.error('Failed to fetch nearby stops:', error)
}
}
await showNearbyArrivals()
```
## Route-aware lookups
When you already know which route the user cares about, pass a `route` to `mta.stops.near()` so the response only includes stops served by that route. Combine it with `radiusMeters` and `limit` to scope the search precisely.
```typescript theme={null}
// Nearest M23 SBS bus stops within 800m of 23rd & 3rd Ave
const nearbyM23 = await mta.stops.near({
lat: 40.7356,
lon: -73.9804,
modes: ['bus'],
route: 'M23',
includeRoutes: true,
radiusMeters: 800,
limit: 5,
})
for (const stop of nearbyM23.stops) {
console.log(`${stop.name} — ${Math.round(stop.distanceMeters)}m`)
}
```
This is useful for "where do I catch the L train from here?" flows: pass `route: 'L'` and `modes: ['subway']` to skip every other line in the response.
## Filtering by mode
Pass only the modes you need to keep the response focused. This reduces payload size and simplifies the results you render.
```typescript theme={null}
// Subway stops only
const subwayOnly = await mta.stops.near({
lat: 40.7580,
lon: -73.9855,
modes: ['subway'],
})
// Bus stops only
const busOnly = await mta.stops.near({
lat: 40.7580,
lon: -73.9855,
modes: ['bus'],
})
```
If your app shows a mode switcher (e.g., subway / bus tabs), call `mta.stops.near()` with the active mode rather than fetching all modes and filtering client-side.
## Using results with arrivals
You can chain `stops.near()` directly into `subway.arrivals()` to create a fully location-aware arrivals lookup.
```typescript theme={null}
async function arrivalsNearMe(route: string) {
const coords = await getCurrentPosition()
const { stops } = await mta.stops.near({
lat: coords.latitude,
lon: coords.longitude,
modes: ['subway'],
})
// Find the nearest stop that serves the requested route
const matchingStop = stops.find((s) => s.routes.includes(route))
if (!matchingStop) {
console.log(`No nearby stop serves the ${route} train.`)
return []
}
const data = await mta.subway.arrivals({
stopId: matchingStop.stopId,
route,
})
return data.arrivals
}
// Get upcoming A trains near the user's location
const arrivals = await arrivalsNearMe('A')
```
The MTA subway system covers the five boroughs, but stops are clustered in Manhattan and dense areas of Brooklyn and Queens. If you query for stops in a sparse area or far from a transit hub, the nearest stop could be over a kilometer away. Consider setting a maximum distance threshold in your UI and showing a message like "No stops within 800m" rather than silently returning a very distant result.
# List routes, stations, and stops
Source: https://mtaapi.dev/docs/guides/routes-and-stations
Use mta.routes.list(), mta.subway.routeStations(), mta.bus.routeStops(), and mta.stops.byIds() to build route pickers, route maps, and hydrate saved stops.
Beyond realtime arrivals, mta-js exposes the catalog behind the system: the list of routes, the ordered stations on a subway line, the ordered stops on a bus route, and batch lookups to resolve stop IDs you already have. Use these to build route pickers, draw a route map, or hydrate saved stop IDs into names and coordinates.
These endpoints require a hosted `apiKey`. See [Authentication](/docs/authentication) to get one.
## Prerequisites
* `mta-js` installed (`npm install mta-js`)
* An MTA API key set as `MTA_API_KEY` in your environment
`mta.routes.list()` returns the route catalog. Pass the modes you care about, or omit for everything.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const routes = await mta.routes.list({ modes: ['subway', 'bus'] })
for (const route of routes) {
console.log(`${route.shortName ?? route.id} — ${route.longName ?? ''} (${route.mode})`)
}
```
`mta.subway.routeStations()` returns the route's stop patterns. Pass a `direction` to order them in the rider's direction of travel.
```typescript theme={null}
const stations = await mta.subway.routeStations({ route: 'L', direction: 'north' })
for (const pattern of stations.directions) {
pattern.stops.forEach((stop, i) => console.log(`${i}. ${stop.name} (${stop.id})`))
}
```
`mta.bus.routeStops()` is the bus equivalent, returning ordered stops grouped by direction.
```typescript theme={null}
const routeStops = await mta.bus.routeStops({ route: 'M23' })
for (const pattern of routeStops.directions) {
console.log(`Direction ${pattern.direction}: ${pattern.stops.length} stops`)
}
```
When you have stored stop IDs (favorites, a saved trip), resolve them in one call with `mta.stops.byIds()`. Each result keeps its `requestedId` and a `found` flag; set `includeRoutes` to attach the routes serving each stop.
```typescript theme={null}
const results = await mta.stops.byIds({
ids: ['A27', 'L06', '308214'],
includeRoutes: true,
})
for (const result of results) {
if (result.found) {
const lines = result.servedRoutes?.map((r) => r.shortName ?? r.id).join(', ')
console.log(`${result.stop.name} — ${lines}`)
} else {
console.log(`${result.requestedId} — not found`)
}
}
```
These guides use the SDK. For the raw REST endpoints, see [Routes](/docs/api-reference/routes), [Subway Route Stations](/docs/api-reference/subway-route-stations), [Bus Route Stops](/docs/api-reference/bus-route-stops), and [Stops by ID](/docs/api-reference/stops-batch).
# Fetch MTA service alerts and disruptions
Source: https://mtaapi.dev/docs/guides/service-alerts
Fetch current MTA service alerts for subway and bus using mta-js. Filter by route and severity to surface delays and service changes in your app.
This guide shows you how to fetch current MTA service alerts covering delays, planned work, and service changes for subway and bus. You will call `mta.alerts.current()`, filter the results for the routes you care about, and display the relevant messages in your application.
Pass a `mode` of `'subway'` or `'bus'` to retrieve alerts for that transit type. You can call both and merge the results if your application covers multiple modes.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
// Fetch subway alerts
const subwayAlerts = await mta.alerts.current({ mode: 'subway' })
// Fetch bus alerts
const busAlerts = await mta.alerts.current({ mode: 'bus' })
```
**Example response:**
```json theme={null}
{
"alerts": [
{
"id": "lmm:alert:123456",
"headerText": "A/C trains running with delays",
"descriptionText": "A and C trains are delayed due to signal problems at Jay St-MetroTech. Allow additional travel time.",
"affectedRoutes": ["A", "C"],
"severity": "WARNING",
"startTime": 1715770800,
"endTime": 1715785200
},
{
"id": "lmm:alert:789012",
"headerText": "Planned work this weekend",
"descriptionText": "No N trains between Astoria and Times Square Sat 12am–5am and Sun 12am–5am.",
"affectedRoutes": ["N"],
"severity": "INFO",
"startTime": 1715904000,
"endTime": 1716076800
}
]
}
```
The `alerts` array may contain many entries. Filter it down to only the routes your users care about before displaying anything.
```typescript theme={null}
function getAlertsForRoute(
alerts: typeof subwayAlerts.alerts,
route: string
) {
return alerts.filter((alert) =>
alert.affectedRoutes.includes(route)
)
}
const aTrainAlerts = getAlertsForRoute(subwayAlerts.alerts, 'A')
```
You can also filter by severity to surface only the most urgent alerts:
```typescript theme={null}
const urgentAlerts = subwayAlerts.alerts.filter(
(alert) => alert.severity === 'WARNING' || alert.severity === 'SEVERE'
)
```
Use `headerText` as the alert title and `descriptionText` for the full detail. Always show both — `headerText` alone may not give users enough context to act on the disruption.
```typescript theme={null}
for (const alert of aTrainAlerts) {
console.log(`[${alert.severity}] ${alert.headerText}`)
console.log(alert.descriptionText)
console.log(`Affects: ${alert.affectedRoutes.join(', ')}`)
console.log('---')
}
```
Alerts can be issued or resolved at any time. Poll `mta.alerts.current()` on a regular interval to keep your UI up to date.
```typescript theme={null}
async function refreshAlerts(mode: 'subway' | 'bus') {
const data = await mta.alerts.current({ mode })
renderAlerts(data.alerts)
}
// Check for new alerts every 60 seconds
setInterval(() => refreshAlerts('subway'), 60_000)
```
Sixty seconds is a reasonable interval for alerts. Unlike arrival times, service alerts change less frequently, so you don't need to poll as aggressively.
## Complete example
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
interface Alert {
id: string
headerText: string
descriptionText: string
affectedRoutes: string[]
severity: string
startTime: number
endTime: number | null
}
function filterActiveAlerts(alerts: Alert[]): Alert[] {
const now = Math.floor(Date.now() / 1000)
return alerts.filter((alert) => {
const started = alert.startTime <= now
const notEnded = alert.endTime === null || alert.endTime > now
return started && notEnded
})
}
function filterByRoute(alerts: Alert[], route: string): Alert[] {
return alerts.filter((alert) => alert.affectedRoutes.includes(route))
}
async function displayAlertsForRoute(
route: string,
mode: 'subway' | 'bus'
): Promise {
try {
const data = await mta.alerts.current({ mode })
const active = filterActiveAlerts(data.alerts)
const relevant = filterByRoute(active, route)
if (relevant.length === 0) {
console.log(`No active alerts for route ${route}.`)
return
}
console.log(`${relevant.length} alert(s) affecting route ${route}:\n`)
for (const alert of relevant) {
console.log(`[${alert.severity}] ${alert.headerText}`)
console.log(alert.descriptionText)
console.log()
}
} catch (error) {
console.error('Failed to fetch alerts:', error)
}
}
await displayAlertsForRoute('A', 'subway')
```
## Filtering by route
When you need alerts for multiple routes at once, extend the filter to check any route in a set:
```typescript theme={null}
const routes = new Set(['A', 'C', 'E'])
const filteredAlerts = subwayAlerts.alerts.filter((alert) =>
alert.affectedRoutes.some((r) => routes.has(r))
)
```
This is useful for transit apps that let users follow several lines and want a unified alert feed across all of them.
Filter to active alerts before displaying anything. An alert with a `startTime` in the future is scheduled but not yet in effect, and an alert with a past `endTime` is resolved. Check both fields before rendering.
```typescript theme={null}
const now = Math.floor(Date.now() / 1000)
const activeAlerts = alerts.filter(
(alert) =>
alert.startTime <= now &&
(alert.endTime === null || alert.endTime > now)
)
```
Alert severity levels indicate how disruptive an event is:
* **`INFO`** — general notices, such as weekend service changes or planned work during off-peak hours
* **`WARNING`** — moderate disruptions, such as delays or reduced service frequency
* **`SEVERE`** — significant disruptions, such as suspended service or major reroutes
Use severity to prioritize which alerts you surface prominently in your UI. Consider showing `SEVERE` alerts in a banner or push notification, and `INFO` alerts inline with scheduled service information.
# Fetch and display real-time subway arrivals
Source: https://mtaapi.dev/docs/guides/subway-arrivals
Fetch live subway arrival predictions for any MTA stop and route using mta-js, process timestamps, and display upcoming trains in your app.
This guide walks you through fetching real-time subway arrivals for a specific stop and route. You will initialize the `MTA` client, call `mta.subway.arrivals()`, and process the response to display upcoming trains in your application.
## Prerequisites
* `mta-js` installed in your project (`npm install mta-js`)
* An MTA API key set as `MTA_API_KEY` in your environment
Every MTA subway stop has a unique stop ID. Stop IDs follow a letter-and-number format where the letter generally corresponds to the train line complex and the number identifies the specific station.
A few examples to get you oriented:
| Stop ID | Station |
| ------- | ----------------------------------------- |
| `A27` | Howard Beach–JFK Airport (A train) |
| `R16` | Times Square–42 St (N/Q/R trains) |
| `120` | 96 St (1/2/3 trains) |
| `D14` | Atlantic Av–Barclays Ctr (B/D/N/Q trains) |
You can look up stop IDs in the [MTA GTFS Static Data](https://api.mta.info/#/subwayRealTimeFeeds) or reference the `stops.txt` file in the MTA's GTFS feed. Pass the stop ID as a string exactly as it appears in those resources.
Import the `MTA` client and call `mta.subway.arrivals()` with the `stopId` and `route` you want to query.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
const data = await mta.subway.arrivals({
stopId: 'A27',
route: 'A',
})
```
Both `stopId` and `route` are required. The `route` value is the train letter or number as it appears on signage (e.g., `'A'`, `'1'`, `'N'`, `'L'`).
The response is an array of `Arrival` objects. Each entry includes the route and stop as structured objects, the predicted arrival time as an ISO 8601 string, a `minutes` field pre-computed for you, and optional `destination` / `displayDirection` fields for building human-readable UI.
```typescript theme={null}
for (const arrival of data) {
const label = arrival.displayDirection ?? arrival.destination ?? arrival.direction
console.log(`${arrival.route.shortName} — ${label} — ${arrival.minutes} min`)
}
```
Use `displayDirection` first (e.g. `"toward 8 Av"`), falling back to `destination` (the raw headsign), and finally the raw `direction` string.
**Example response:**
```json theme={null}
[
{
"mode": "subway",
"route": { "id": "A", "shortName": "A", "color": "#0039A6" },
"stop": { "id": "A27", "name": "Howard Beach-JFK Airport", "displayName": "Howard Beach–JFK Airport" },
"direction": "north",
"destination": "Inwood-207 St",
"displayDirection": "toward Inwood-207 St",
"arrivalTime": "2026-05-29T18:37:00.000Z",
"minutes": 4,
"tripId": "AFA25GEN-A078-Sunday-00_000600_A..N03R",
"realtime": true,
"source": "mta-gtfs-rt"
}
]
```
When no trains are scheduled—late at night or during a service gap—`arrivals` will be an empty array. Wrap your call in a try/catch and check for an empty result before rendering.
```typescript theme={null}
try {
const data = await mta.subway.arrivals({ stopId: 'A27', route: 'A' })
if (data.length === 0) {
console.log('No upcoming arrivals at this stop.')
return
}
for (const arrival of data) {
// display each arrival
}
} catch (error) {
console.error('Failed to fetch arrivals:', error)
}
```
## Complete example
The function below ties all the steps together into a reusable arrivals display function.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
interface ArrivalDisplay {
direction: string
minutesAway: number
tripId?: string
}
async function getSubwayArrivals(
stopId: string,
route: string
): Promise {
const arrivals = await mta.subway.arrivals({ stopId, route })
return arrivals
.filter((a) => a.minutes >= 0)
.map((a) => ({
tripId: a.tripId,
direction: a.displayDirection ?? a.destination ?? a.direction,
minutesAway: a.minutes,
}))
}
// Display arrivals at Howard Beach on the A train
const arrivals = await getSubwayArrivals('A27', 'A')
for (const arrival of arrivals) {
console.log(
`${arrival.direction} — arriving in ${arrival.minutesAway} min (trip ${arrival.tripId})`
)
}
```
MTA real-time data updates roughly every 30 seconds. Poll on the same interval to keep your UI current without overloading the API.
```typescript theme={null}
// Refresh arrivals every 30 seconds
setInterval(async () => {
const arrivals = await getSubwayArrivals('A27', 'A')
renderArrivals(arrivals)
}, 30_000)
```
The `direction` field uses NYCT's `'north'` / `'south'` values even on east-west lines. For the L train, `mta-js` also accepts rider-facing `'east'` / `'west'` aliases as query input and maps them to the underlying feed directions. For display, prefer `displayDirection` (e.g. `"toward 8 Av"`) or `destination` over the raw direction string.
If your rider enters *where they want to go* rather than a compass direction, use [`mta.subway.direction()`](/docs/api-reference/direction) to resolve a destination like `"Union Sq"` into the `north` / `south` value, then pass it straight to `mta.subway.arrivals({ ..., direction })`.
# Get started with mta-js
Source: https://mtaapi.dev/docs/index
mta-js is a TypeScript SDK for NYC transit data. Install it, get your MTA API key, and make your first real-time subway arrival request in minutes.
`mta-js` is a unified JavaScript/TypeScript SDK that wraps MTA's real-time feeds into one consistent, easy-to-use interface. Whether you're building a transit tracker, a commute planner, or a live arrival board, mta-js gives you subway arrivals, bus vehicle positions, service alerts, and geolocation-based stop lookups—all through a single `MTA` client.
## Get up and running
Add `mta-js` to your project using your preferred package manager.
```bash npm theme={null}
npm install mta-js
```
```bash yarn theme={null}
yarn add mta-js
```
```bash pnpm theme={null}
pnpm add mta-js
```
```bash bun theme={null}
bun add mta-js
```
Using Next.js with Turbopack? Add `mta-js` to `transpilePackages`. The SDK publishes TypeScript source, and this tells Next to compile it instead of treating `node_modules/mta-js/index.ts` as an unknown module type.
```javascript next.config.mjs theme={null}
/** @type {import("next").NextConfig} */
const nextConfig = {
transpilePackages: ["mta-js"],
}
export default nextConfig
```
Get an API key from [mtaapi.dev](https://www.mtaapi.dev) — our hosted MTA API. One key, all endpoints, no setup.
1. Sign up at [mtaapi.dev](https://www.mtaapi.dev).
2. Copy your API key from the dashboard.
3. Add it to your project as an environment variable:
```bash .env theme={null}
MTA_API_KEY=your_api_key_here
```
Never commit your API key to version control. Store it in an environment variable and load it at runtime. See [Authentication](/docs/authentication) for more detail.
Import `MTA` and create a client instance by passing your `apiKey`.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
```
With an `apiKey` set, requests are routed to `https://www.mtaapi.dev/api/v1` automatically. You only configure it once.
Call `mta.subway.arrivals()` with a stop ID and route to get real-time arrival predictions.
```typescript theme={null}
import { MTA } from 'mta-js'
const mta = new MTA({ apiKey: process.env.MTA_API_KEY })
// Next A trains at Howard Beach
const arrivals = await mta.subway.arrivals({
stopId: 'A27',
route: 'A'
})
console.log(arrivals)
```
A successful response is an array of `Arrival` objects:
```json theme={null}
[
{
"mode": "subway",
"route": { "id": "A", "shortName": "A", "color": "#0039A6" },
"stop": { "id": "A27", "name": "Howard Beach-JFK Airport", "displayName": "Howard Beach–JFK Airport" },
"direction": "north",
"destination": "Inwood-207 St",
"displayDirection": "toward Inwood-207 St",
"arrivalTime": "2026-05-29T18:32:00.000Z",
"minutes": 4,
"tripId": "AFA25GEN-1037-Sunday-00_106200_A..N70R",
"realtime": true,
"source": "mta-gtfs-rt"
},
{
"mode": "subway",
"route": { "id": "A", "shortName": "A", "color": "#0039A6" },
"stop": { "id": "A27", "name": "Howard Beach-JFK Airport", "displayName": "Howard Beach–JFK Airport" },
"direction": "north",
"destination": "Inwood-207 St",
"displayDirection": "toward Inwood-207 St",
"arrivalTime": "2026-05-29T18:47:00.000Z",
"minutes": 19,
"tripId": "AFA25GEN-1037-Sunday-00_109800_A..N70R",
"realtime": true,
"source": "mta-gtfs-rt"
}
]
```
## What you can build
Real-time arrival times for any subway stop and route.
Live vehicle positions for any bus route.
Delays, planned work, and service changes in real time.
Find subway and bus stops near any latitude/longitude.
Resolve a rider's destination into the right north/south direction.
Full SDK method reference with parameters and response types.
Prefer to call MTA feeds directly instead of using the hosted API? See [Direct MTA feeds and self-hosting](/docs/configuration/setup).