MBTA Boston: Bus & Train Arrivals, Timetables, Alerts avatar

MBTA Boston: Bus & Train Arrivals, Timetables, Alerts

Pricing

from $1.40 / 1,000 transit row returneds

Go to Apify Store
MBTA Boston: Bus & Train Arrivals, Timetables, Alerts

MBTA Boston: Bus & Train Arrivals, Timetables, Alerts

Next departures at any MBTA stop or station as rows: minutes away, headsign, direction, live flag and delay against the timetable. Slots the source does not predict are filled from the schedule. Other modes: timetable by date, service alerts, live vehicles, stop and route dictionaries.

Pricing

from $1.40 / 1,000 transit row returneds

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

17 days ago

Last modified

Security

Limited permissions

Share

When does the next bus or train leave this stop, and is the line disrupted right now? This actor answers both as plain rows from Boston's official transit API (api-v3.mbta.com): minutes away, headsign, direction, live-or-scheduled flag, delay against the timetable, vehicle, stop and route ids. Every slot the agency does not predict — late night, a trip two hours out, a stop without realtime coverage — is filled from the published timetable, so a run at 03:00 still writes rows instead of an empty dataset. Six modes share one input: arrivals, timetable, alerts, vehicles, stops and routes. No API key, no proxy, no browser.

Need it tailored to your workflow? Missing a field, a filter, another source or a different output format? Open an issue and describe what you need — we read every request and adapt the actor.

Use cases

  • Stop or platform display: poll arrivals for one station and one direction every minute and show minutesAway, headsign and the live flag on a screen in a lobby, café or office.
  • Station departure board for one mode: South Station carries commuter rail, subway and buses — filter Modes of transport to commuter-rail and you get the board a rail passenger wants, with serviceDate and track direction.
  • "What leaves near me" for an app or an agent: give a latitude and longitude, get every stop within walking distance with a real distanceMeters and the next departures from each.
  • Service-status dashboard: alerts writes every disruption in effect with effect, cause, severity, the routes and stops affected and the active window; add Changed in the last N hours plus Only rows not seen before for a daily watch that never reports the same alert twice.
  • Accessibility and step-free routing: filter alerts to ELEVATOR_CLOSURE, ESCALATOR_CLOSURE, ACCESS_ISSUE and STATION_ISSUE to see which lifts are out before sending someone to a station.
  • Timetable export and analysis: schedule writes every scheduled trip at a stop for one service date — trip id, stop sequence, local and UTC times, pick-up and drop-off flags — ready for a spreadsheet or a punctuality study.
  • Building the ids first: stops and routes are the two dictionaries; run them once, keep the ids, and feed them into every other mode.

Input

Every field is optional. Values shown in the Console form as examples are prefills, not defaults — only behaviour settings have defaults, so no run is silently narrowed to a place you did not ask for.

FieldTypeDefaultAllowed values / notes
modestringarrivalsarrivals, schedule, alerts, vehicles, stops, routes — see Modes
stopQuerystring— (prefill Park Street)A stop or station name as riders say it. Resolved against the source's own stop list, stations first; the id picked is written to the log and to SUMMARY
stopIdsstring[]—Source ids instead of a name: place-pktrm, place-sstat, 70075, 1936. A station id covers all its platforms
routeIdsstring[]—Red, Orange, Blue, Mattapan, Green-B…Green-E, bus numbers (1, 66, 111, SL1), commuter rail (CR-Fitchburg…). See Route ids
routeTypesstring[]— (all)light-rail, subway, commuter-rail, bus, ferry
latitudenumber—−90…90. Give it together with longitude instead of naming a stop
longitudenumber—−180…180 (Boston is about −71)
radiusMetersinteger60050–5000. Real metres: the actor converts to the degrees the source wants, then measures each stop itself
directionIdstringanyany, 0, 1. Each route names its own two directions — mode routes lists them
minutesAheadinteger1205–1440. Mode arrivals only: how far ahead departures may lie
serviceDatestring— (today in Boston)YYYY-MM-DD, mode schedule. arrivals always lists the next departures from now and ignores it (with a warning)
startTimestring— (whole day)HH:MM Boston local, mode schedule. Past midnight counts on: 25:00 is 1 a.m.
includeScheduledbooleantrueMode arrivals: fill every slot without a prediction from the timetable and compute delaySeconds where both exist
alertEffectsstring[]— (all)See Alert effects
alertSeverityMininteger— (all)0–10; 3 drops most information notices
alertLifecyclesstring[]— (all)NEW, ONGOING, ONGOING_UPCOMING, UPCOMING
activeOnlybooleantrueOnly alerts whose active period covers this moment
sinceHoursinteger— (no filter)1–720. Keep only alerts updated inside this many hours, counted in UTC
onlyNewbooleanfalseWrite only rows whose key was not delivered by an earlier run with the same mode, place and route filters. A run with nothing new writes one billed found: false row
maxItemsinteger501–2000. The run also stops at your Maximum cost per run and says so
fieldsstring[]— (all)Keep only these fields, in this order. rowType and found are always written (and message on a found: false row)
apiKeystring (secret)—Optional. Your own free key from the agency raises the request limit from 20 to 1000 per minute; sent as a header, never logged, never written to a row

Route ids are read in any letter case, as the line's name (Red Line) or with one clear typo ("Ornage" → Orange); stop names are matched the way riders write them ("park st" → Park Street). Every such reading is named in the status message and in SUMMARY.corrections. A route id that does not exist, or that could be two routes (Green-A is as close to Green-B as to Green-C, bus 117 is one digit from several others), ends the run with a message naming the id and the closest ones — a typo never turns into a different route. A stop name nothing matches gives one found: false row. The fields with a fixed list of values (mode, routeTypes, directionId, alertEffects, alertLifecycles) are checked by the Apify input form itself: a value outside the list is refused before the run starts, together with the allowed values. Names in fields are checked before the run too: a different letter case or one clear slip is read as the column and reported, an unknown name is left out with a warning, and a list of which the chosen mode fills nothing (say minutesAway in mode alerts) is refused with that mode's columns — rows holding only rowType and found would still be billed. The search is never widened behind your back.

Reference

Modes

modeWhat one row isNeedsTypical fields
arrivalsThe next departure of one trip from one stopa stop, a coordinate or a routeminutesAway, timeLocal, headsign, predicted, delaySeconds
scheduleOne scheduled trip at one stop on one service date — over several stops, the earliest trips of all of thema stop, a coordinate or a routeserviceDate, timeLocal, tripId, stopSequence, pickUp
alertsOne published disruptionnothingheader, effect, severity, routeIdsAffected, activePeriodStart
vehiclesOne vehicle in service with its positionnothing (filter by route)latitude, longitude, currentStatus, bearing
stopsOne stop or stationnothing (filter by route or coordinate)stopId, stopName, municipality, wheelchairBoarding
routesOne route or linenothingrouteId, routeName, directionNames, routeColor

arrivals works like this: the live predictions come first; if fewer rows than you asked for are predicted and Fill gaps from the timetable is on, the published timetable for the same stops and window is merged in, marked source: "schedule"; where a prediction and a timetable entry describe the same trip at the same stop, the row keeps the live time and gains scheduledTime and delaySeconds. If the window holds nothing at all — after the last train of the night — the run writes the first departures of the next service day rather than nothing.

Modes of transport

routeTypes valuerouteTypeCovers
light-rail0Green Line branches, Mattapan trolley
subway1Red, Orange and Blue lines
commuter-rail2All CR-… lines
bus3Numbered bus routes and the Silver Line (SL1…)
ferry4Harbor ferry routes (Boat-…)

Route ids

Rapid transit uses colour names: Red, Orange, Blue, Mattapan, Green-B, Green-C, Green-D, Green-E. Buses use their number as the id (1, 39, 66, 111) and the Silver Line uses SL1…SL5. Commuter rail lines are CR- plus the line name: CR-Fitchburg, CR-Providence, CR-Newburyport, CR-Worcester, CR-Fairmount, and so on (13 lines at the time of writing). Ferries start with Boat-. Run mode routes for the current list with names, colours and direction names; give routeTypes to get one mode's lines only.

Stop ids

Station ids start with place- (place-pktrm = Park Street, place-sstat = South Station, place-north = North Station, place-portr = Porter). Subway platforms are four-digit numbers (70075), bus poles are shorter numbers (1936), commuter rail platforms look like FR-0034-02. A station id in stopIds covers every platform under it, and the rows then carry both the platform (stopId) and the station (parentStationId). Run mode stops with a route or a coordinate to get the ids for your area.

Alert effects

DELAY, SHUTTLE, DETOUR, SUSPENSION, CANCELLATION, STATION_CLOSURE, STOP_CLOSURE, STOP_MOVE, TRACK_CHANGE, SCHEDULE_CHANGE, SERVICE_CHANGE, STATION_ISSUE, ACCESS_ISSUE, ELEVATOR_CLOSURE, ESCALATOR_CLOSURE, SNOW_ROUTE, NO_SERVICE, POLICY_CHANGE, NOTICE, SUMMARY.

Causes seen in the live feed include UNKNOWN_CAUSE, CONSTRUCTION, TRAFFIC, MAINTENANCE, WEATHER, SEVERE_WEATHER, ACCIDENT, POLICE_ACTIVITY, MEDICAL_EMERGENCY. Lifecycles are NEW, ONGOING, ONGOING_UPCOMING and UPCOMING. Severity runs 0–10: 1 is an information notice, 3–5 a real disruption, 7 and above a line-wide event. Effects are filtered on the rows, so an effect nobody published today simply yields fewer rows.

Vehicle status values

INCOMING_AT, STOPPED_AT, IN_TRANSIT_TO; occupancy, when the agency reports it, is one of MANY_SEATS_AVAILABLE, FEW_SEATS_AVAILABLE, STANDING_ROOM_ONLY, CRUSHED_STANDING_ROOM_ONLY, FULL, NO_DATA_AVAILABLE.

Examples

Next Red Line trains at Park Street (a stop display)

{ "mode": "arrivals", "stopQuery": "Park Street", "routeIds": ["Red"], "maxItems": 15 }

Commuter rail departures from South Station (one mode at a multi-mode station)

{ "mode": "arrivals", "stopQuery": "South Station", "routeTypes": ["commuter-rail"], "maxItems": 20 }

Buses near a coordinate in downtown Boston

{ "mode": "arrivals", "latitude": 42.3601, "longitude": -71.0589, "radiusMeters": 500, "routeTypes": ["bus"], "maxItems": 20 }

Every disruption in effect right now

{ "mode": "alerts", "maxItems": 30 }

Lift and escalator outages for step-free routing

{ "mode": "alerts", "alertEffects": ["ELEVATOR_CLOSURE", "ESCALATOR_CLOSURE", "ACCESS_ISSUE", "STATION_ISSUE"], "maxItems": 20 }

Today's full commuter rail timetable at Porter

{ "mode": "schedule", "stopQuery": "Porter", "routeIds": ["CR-Fitchburg"], "maxItems": 40 }

The two dictionaries: Orange Line stops, then the commuter rail lines

{ "mode": "stops", "routeIds": ["Orange"], "maxItems": 30 }
{ "mode": "routes", "routeTypes": ["commuter-rail"], "maxItems": 25 }

Output

One real row from the cloud run Cu0CI8DmcadgyYTFB (input: the first example above), 27 September 2026, 18:20 Boston:

{
"rowType": "arrival",
"found": true,
"stopId": "70076",
"stopName": "Park Street",
"parentStationId": "place-pktrm",
"parentStationName": "Park Street",
"municipality": "Boston",
"distanceMeters": null,
"latitude": 42.3563946,
"longitude": -71.0624242,
"routeId": "Red",
"routeName": "Red Line",
"routeShortName": null,
"routeType": 1,
"routeTypeName": "subway",
"routeColor": "DA291C",
"directionId": 1,
"directionName": "North",
"headsign": "Alewife",
"tripId": "77916490",
"tripName": null,
"vehicleId": "R-548BD870",
"vehicleLabel": "1920",
"arrivalTime": "2026-09-27T22:19:27.000Z",
"departureTime": "2026-09-27T22:20:34.000Z",
"time": "2026-09-27T22:20:34.000Z",
"timeLocal": "2026-09-27T18:20:34-04:00",
"scheduledTime": "2026-09-27T22:15:00.000Z",
"delaySeconds": 334,
"serviceDate": "2026-09-27",
"minutesAway": 10,
"predicted": true,
"source": "prediction",
"status": null,
"scheduleRelationship": null,
"stopSequence": 150,
"lastTrip": false,
"url": "https://www.mbta.com/stops/place-pktrm",
"fetchedAt": "2026-09-27T22:10:11.262Z"
}

An alert row from the same suite (run yM0xAAggcS8hPg1Dn), shortened:

{
"rowType": "alert",
"alertId": "1035169",
"header": "Route 44 is experiencing delays of about 20 minutes due to traffic.",
"effect": "DELAY",
"cause": "TRAFFIC",
"severity": 5,
"lifecycle": "NEW",
"serviceEffect": "Route 44 delay",
"active": true,
"activePeriodStart": "2026-09-27T21:59:28.000Z",
"activePeriodEnd": "2026-09-28T00:10:00.000Z",
"updatedAt": "2026-09-27T21:59:28.000Z",
"routeIdsAffected": [
"44"
],
"stopIdsAffected": [],
"routeTypesAffected": [
"bus"
],
"activities": [
"BOARD",
"EXIT",
"RIDE"
],
"url": "https://www.mbta.com/schedules/44/alerts",
"fetchedAt": "2026-09-27T22:10:15.343Z"
}

Fields

FieldTypeMeaning
rowTypestringarrival, schedule, alert, vehicle, stop, route or notFound. Always written, whatever fields lists
foundbooleanfalse only on the single row a run writes when nothing matched. Always written, whatever fields lists
message, mode, stopQuerystringfound: false row only: why nothing matched, the mode and the stop name asked for. message is always written on that row
stopId, stopNamestringThe platform or stop the row happens at, and its name
parentStationId, parentStationNamestringThe station the platform belongs to
municipalitystringTown the stop is in
distanceMetersnumberDistance from your coordinate, measured by the actor (coordinate searches only)
latitude, longitudenumberStop position — for a vehicle row, the vehicle's own position
routeId, routeName, routeShortNamestringThe route: id used by the filters, long name, number
routeType, routeTypeNamenumber, string0–4 and its name (subway, bus, …)
routeColorstringThe line's colour as a hex triplet, for a map or a badge
directionId, directionNamenumber, string0 or 1 and the name that route gives it (South, Inbound, Alewife)
headsignstringWhat the vehicle shows — the destination of this trip
tripId, tripNamestringTrip identity; commuter rail also numbers its trains
vehicleId, vehicleLabelstringThe vehicle serving the trip, when the agency tracks it
arrivalTime, departureTimestringISO 8601 UTC; either may be empty at the first or last stop of a trip
timestringThe time the row is sorted and counted by: departure if there is one, else arrival (UTC)
timeLocalstringThe same moment with Boston's own offset, exactly as the agency publishes it
scheduledTimestringThe timetable time for this trip and stop (UTC), when it exists
delaySecondsnumbertime − scheduledTime; positive = late, negative = early
serviceDatestringThe service day the trip is counted under — a 00:40 train belongs to the previous date
minutesAwaynumberWhole minutes from the start of the run; empty for a timetable row already in the past
predictedbooleantrue = live prediction, false = published timetable
sourcestringprediction or schedule
status, scheduleRelationshipstringThe agency's own words about the trip (ADDED, CANCELLED, …) when it says anything
stopSequencenumberPosition of this stop in the trip
pickUp, dropOff, timepointbooleanTimetable rows: whether the trip boards, alights and keeps time here
lastTripbooleanPrediction rows: the last trip of the night on that route
alertId, header, shortHeader, descriptionstringAlert identity and the rider-facing text
effect, cause, severity, lifecycle, serviceEffect, timeframe, banner—The alert classified: see Alert effects
active, activePeriodStart, activePeriodEndboolean, stringWhether it is in effect now and the window (UTC)
createdAt, updatedAtstringWhen the agency published and last touched it (UTC)
routeIdsAffected, stopIdsAffected, routeTypesAffected, activitiesarrayWhat the alert names as affected
currentStatus, currentStopSequence, bearing, speed, occupancyStatus, revenue—Vehicle rows: what it is doing and where it points
locationType, locationTypeName, platformName, platformCode, address, description—Stop rows: what kind of place it is
wheelchairBoardingnumber0 unknown, 1 accessible, 2 not accessible
matchedRouteIdsarrayStop rows: which of your route filters this stop serves
directionNames, directionDestinations, routeTextColor, routeDescription, fareClass, sortOrder—Route rows
urlstringThe agency page for the station, line or alert — for a human to open
fetchedAtstringWhen the run read the source, ISO 8601 UTC

All timestamps are UTC (…Z) except timeLocal, which keeps Boston's offset on purpose. Dataset views: Next arrivals, Service alerts, Live vehicles and Stops & routes. A run also writes a SUMMARY record to the key-value store with the resolved stop, the filters, how many rows were live and how many came from the timetable, the request count and any warnings.

If nothing matches — a name no stop carries, an alert filter nothing satisfies, an onlyNew watch with nothing new — the dataset gets exactly one row with found: false and a message saying why, never a silent empty success. That row is a dataset row and is billed like one (see Pricing). The dataset schema describes every column with a title, a description and an example, so agents and MCP clients see the output shape before the first run.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~mbta-arrivals/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"arrivals","stopQuery":"Park Street","routeIds":["Red"],"maxItems":15}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/mbta-arrivals').call({ mode: 'arrivals', stopQuery: 'South Station', routeTypes: ['commuter-rail'], maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/mbta-arrivals").call(run_input={"mode": "alerts", "alertSeverityMin": 3, "maxItems": 30})
items = client.dataset(run["defaultDatasetId"]).list_items().items

MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/mbta-arrivals tool with the same JSON input.

A display should poll a narrow window (minutesAhead 30, maxItems 10) rather than the whole day — it is faster and cheaper. Trim the row to what you render with fields, e.g. ["stopName", "routeName", "headsign", "minutesAway", "predicted"]. For a watch, schedule the run and set onlyNew: the keys are remembered per mode, place and route filter, so the same alert is never paid for twice. A watch run that finds nothing new writes one found: false row ("Nothing new since the previous run"), which costs one row. The memory ignores the severity, lifecycle, change-window, direction and time filters, so two watches of one mode that differ only in those share it — give each watch its own route, stop or effect filter if they must stay apart.

Pricing

Pay per event: $0.001 per run start plus $0.002 per dataset row on the free tier. The row price falls with your Apify plan — Bronze −10 % ($0.0018), Silver −20 % ($0.0016), Gold and above −30 % ($0.0014) — while the start event stays $0.001 on every plan. Every run is charged the start event. The single found: false row a run writes when nothing matches — or when an onlyNew watch finds nothing new — is a dataset row and costs the row price like any other, so such a run costs $0.001 + $0.002 on the free tier.

RunRowsFree tierGold and above
Stop display, one direction10$0.001 + $0.020 = $0.021$0.001 + $0.014 = $0.015
Station board (the default maxItems)50$0.001 + $0.100 = $0.101$0.001 + $0.070 = $0.071
Timetable export for a stop300$0.001 + $0.600 = $0.601$0.001 + $0.420 = $0.421

Platform usage (compute, storage, data transfer) is included in these prices; nothing is billed on top.

Limits & FAQ

  • Your maximum cost per run is respected. The run reads it before any request, stops when it is reached and ends with the status "Stopped at your spending limit: N rows delivered". With onlyNew only the rows really written are remembered, so the next run returns the rest instead of losing them.
  • Run timeout. A run close to its timeout starts no new request, writes what it has read and ends SUCCEEDED with "Stopped before the run timeout: N rows saved". A typical run needs a few seconds; the default timeout is 15 minutes.
  • Rate limit. Anonymous callers get 20 requests a minute from the source, and the actor paces itself under that; a run that has to page the whole stop list can therefore take a minute. Paste your own free key from the agency into Your own API key and the limit becomes 1000 a minute. The key is sent as a header, never logged and never written to a row.
  • Predictions exist only where the agency predicts. Realtime covers trips that are being tracked; a stop with no vehicle on the way, or the middle of the night, has none. That is exactly why the timetable fallback exists and why most rows outside rush hour say source: "schedule".
  • Vehicle positions are empty overnight. Roughly 02:00–04:30 Boston time nothing is moving, so mode vehicles legitimately returns the one found: false row. Use arrivals if you need something to show at that hour.
  • The published timetable covers roughly the current quarter. A serviceDate far in the past or future has no trips, and the run says so rather than returning an empty dataset.
  • Alert filters can legitimately return nothing. A quiet hour with alertSeverityMin: 7 is a network running well.
  • Ambiguous names. "Harvard" is a station and also several bus stops; the actor picks the station and writes the id it used into the status message, the log and SUMMARY. Use stopIds when you need to be certain.
  • Service days, not calendar days. A trip at 00:40 belongs to the previous service date and the timetable counts it as 24:40. serviceDate always shows the agency's own day.
  • What is not here. No fare or trip-planning data, no historical performance archive, no vehicle occupancy where the agency does not publish it, and no pages from the agency's website — the actor reads only the public JSON API.
  • Source and attribution. Data comes from the MBTA's public V3 API and is governed by the MassDOT Developers License Agreement; attribute the agency when you republish it, and do not present the output as an official service. Alert text is the agency's wording and is passed through unchanged.

Made by Yadroo. Sibling actors: open-meteo-weather, osm-geocode, nasa-eonet-events, public-holidays, rss-to-json.