Transit Routing
Set mode to transit and Plaza plans a trip the way a rider takes it: walk to a stop, ride, maybe change, walk to the door. Times come from the agency’s published GTFS timetable, so a 07:58 departure means you board the train that leaves at 07:58, not “about 20 minutes by transit”.
Transit mode works on Directions and Isochrones. Both are premium endpoints, so each request counts as 4x.
Coverage
Section titled “Coverage”Transit routing only knows the GTFS feeds Plaza has loaded. Each feed is a Plaza-curated dataset named gtfs-<agency> (for example gtfs-mbta), so you can see which ones exist:
curl "https://plaza.fyi/api/v1/datasets?scope=plaza&include_pending=true" \ -H "x-api-key: $PLAZA_API_KEY"A feed that is listed but still pending has not been loaded yet, and requests in its area find no stops. Feeds are loaded and refreshed by hand for now, so a schedule change shows up after Plaza re-syncs the feed.
A journey can change between two agencies’ feeds where their stops are a short walk apart: up to 500 m in a straight line and 1 km on foot, measured when a feed is loaded. Plaza searches the feeds with stops near your origin or destination, plus any feed that connects to feeds at both ends over those walks. So a bus from one agency to a regional rail station run by another works, and so does a ride through a third network in the middle. Feeds in different time zones are never combined.
depart_at has to fall inside a feed’s published timetable (most agencies publish a few months ahead). A date outside every nearby feed’s period is a 422 that names the periods.
Feeds that describe some service with frequencies.txt (a departure every N minutes instead of a timetable) are loaded too. Each frequencies row becomes a copy of its template trip every headway_secs, starting at start_time and stopping before end_time. See Headway-based service for how those legs look. Per-stop time zones (stop_timezone) are ignored: every time in a feed is read in the agency’s time zone.
The catalog lists MBTA (gtfs-mbta) and BART (gtfs-bart), but neither is loaded yet. Their license and attribution strings in the dataset metadata are placeholders until checked against each agency’s developer terms, so check those terms yourself before you build on that data.
A route
Section titled “A route”curl -X POST https://plaza.fyi/api/v1/route \ -H "x-api-key: $PLAZA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "origin": {"type": "Point", "coordinates": [-71.0620, 42.3567]}, "destination": {"type": "Point", "coordinates": [-71.1210, 42.3745]}, "mode": "transit", "depart_at": "2026-03-16T08:00:00-04:00" }'The response is one GeoJSON Feature. geometry is a MultiLineString with one line per leg, and properties.legs lists the legs in order. Here: walk to Park Street, ride the Red Line to Harvard, walk to the destination.
{ "type": "Feature", "geometry": {"type": "MultiLineString", "coordinates": [[...], [...], [...]]}, "properties": { "mode": "transit", "feed_id": "mbta", "feed_ids": ["mbta"], "depart_at": "2026-03-16T08:03:12-04:00", "arrive_at": "2026-03-16T08:21:30-04:00", "duration_s": 1098, "distance_m": 5940.7, "walk_distance_m": 520.7, "transfers": 0, "legs": [ { "mode": "walk", "from": {"kind": "origin", "lat": 42.3567, "lng": -71.062}, "to": {"kind": "stop", "stop_id": "place-pktrm", "feed_id": "mbta", "name": "Park Street", "code": null, "lat": 42.3564, "lng": -71.0624}, "departure": "2026-03-16T08:03:12-04:00", "arrival": "2026-03-16T08:04:00-04:00", "duration_s": 48, "distance_m": 62.4, "geometry": {"type": "LineString", "coordinates": [...]} }, { "mode": "subway", "feed_id": "mbta", "route": {"id": "Red", "short_name": null, "long_name": "Red Line", "color": "#DA291C", "text_color": "#FFFFFF", "type": 1, "agency_id": "1"}, "trip_id": "...", "headsign": "Alewife", "headway_s": null, "exact_times": null, "from": {"kind": "stop", "stop_id": "place-pktrm", "name": "Park Street", "departure": "2026-03-16T08:04:00-04:00", "...": "..."}, "to": {"kind": "stop", "stop_id": "place-harsq", "name": "Harvard", "arrival": "2026-03-16T08:16:00-04:00", "...": "..."}, "departure": "2026-03-16T08:04:00-04:00", "arrival": "2026-03-16T08:16:00-04:00", "wait_s": 0, "duration_s": 720, "distance_m": 5420.0, "stops": 4, "intermediate_stops": [ {"kind": "stop", "name": "Charles/MGH", "arrival": "2026-03-16T08:06:00-04:00", "...": "..."}, {"kind": "stop", "name": "Kendall/MIT", "arrival": "2026-03-16T08:09:00-04:00", "...": "..."}, {"kind": "stop", "name": "Central", "arrival": "2026-03-16T08:13:00-04:00", "...": "..."} ], "geometry": {"type": "LineString", "coordinates": [...]} }, { "mode": "walk", "from": {"kind": "stop", "stop_id": "place-harsq", "feed_id": "mbta", "name": "Harvard", "code": null, "lat": 42.3734, "lng": -71.1189}, "to": {"kind": "destination", "lat": 42.3745, "lng": -71.121}, "departure": "2026-03-16T08:16:00-04:00", "arrival": "2026-03-16T08:21:30-04:00", "duration_s": 330, "distance_m": 458.3, "geometry": {"type": "LineString", "coordinates": [...]} } ] }}The times are illustrative (Plaza’s own tests use a synthetic feed), but the shape and the arithmetic are real. duration_s runs from depart_at to arrive_at, and each leg starts when the one before it ends, plus wait_s before a vehicle.
A few things about the fields:
depart_atis when you need to leave the origin to catch the first vehicle, not the time you asked for. The walk to the first stop is timed to arrive just as it departs.wait_sis the time spent at a stop before the vehicle leaves. Transfers often have a wait; the first ride’s is always 0, because the first walk is timed to end as it departs.- A vehicle leg’s
modecomes from the GTFSroute_type:bus,trolleybus,coach,tram,subway,rail,monorail,ferry,cable_tram,aerial_liftorfunicular. Aroute_typePlaza doesn’t recognize shows up astransit. Walking legs arewalk. - Vehicle geometry follows the feed’s
shapes.txtwhen it has one. Otherwise it is straight lines through the stops served. - Walking legs follow the street graph. A transfer between two platforms of one station can be a straight line, because station interiors are often missing from OpenStreetMap.
- Times are ISO 8601 in the feed’s time zone.
feed_idslists every feed the journey rides, in order.feed_idis the first vehicle’s feed. Each vehicle leg and each stop (from,to,intermediate_stops) has its ownfeed_id, so a change from one agency to another shows up as a walk between stops with differentfeed_ids.
If walking the whole way is faster and no longer than max_walk_m, you get a single walk leg, feed_id is null and feed_ids is empty.
Headway-based service
Section titled “Headway-based service”Trips from frequencies.txt have two extra values on their vehicle legs. On scheduled trips both are null.
headway_sis the number of seconds between vehicles.exact_timesistruewhen the feed publishes exact departures on that headway. When it’sfalse, vehicles only run about everyheadway_sseconds, and the departure and arrival times in the leg are an estimate on that grid. Leave yourself some slack.
trip_id on these legs is the feed’s template trip id, so several vehicles in one day share it.
Options
Section titled “Options”| Field | Type | Default | Meaning |
|---|---|---|---|
depart_at | string | now | When you leave. With an offset (Z, -04:00) it is an exact instant. Without one (2026-03-16T08:00:00) it is wall-clock time in the feed’s time zone, which is usually what you want for “8 am in Boston”. |
arrive_by | string | none | When you need to be there, read like depart_at (now isn’t accepted). Routes only, and not together with depart_at. See Arriving by a time. |
wheelchair | boolean | false | Ride only wheelchair-accessible trips and use only accessible stops. See Wheelchair access. |
max_walk_m | number | 1000 | Longest walk for each walking leg: to the first stop, between stops, and from the last stop. At most 3000. |
max_transfers | integer | 3 | Most changes between vehicles. 0 means one vehicle only. At most 4. |
use_bus | number | 1 | 0 to 1. 0 never rides a bus (bus, trolleybus, coach). |
use_rail | number | 1 | 0 to 1. 0 never rides rail (tram, subway, rail, monorail, cable car, funicular). |
use_transfers | number | 1 | 0 to 1. 0 allows no transfer. |
pedestrian | object | defaults | Walker options for the walking legs, the same object mode: "foot" takes (see Cycling and Walking Options). See Walking options. |
Ferries are always allowed.
Walking options
Section titled “Walking options”The pedestrian object shapes the walks. walking_speed sets how long every walk takes, transfers between stops included, so a slow walker may miss a connection a fast one makes. wheelchair, max_hiking_difficulty and access rules decide which ways the walks to and from stops can use. The preferences (use_hills, use_lit, walkway_factor, sidewalk_factor, step_penalty) choose which path is drawn. Walks between stops were measured when the feed was loaded, with the default rules. A slower walker takes longer on them, and a transfer with a minimum time set by the feed never gets shorter.
{ "origin": {"type": "Point", "coordinates": [-71.0589, 42.3601]}, "destination": {"type": "Point", "coordinates": [-71.1190, 42.3736]}, "mode": "transit", "depart_at": "2026-03-16T08:00:00", "pedestrian": {"walking_speed": 3.5, "use_lit": 1}}On transit isochrones walking_speed also sizes the walking circles.
How the preferences work
Section titled “How the preferences work”Plaza first finds the fastest journey for each number of vehicles: the fastest with one ride, the fastest with two rides if it arrives earlier, and so on. Then it scores those candidates and returns the cheapest one:
- arrival time minus
depart_at, in seconds, - plus bus time multiplied by
2 * (1 - use_bus), and rail time multiplied by2 * (1 - use_rail), - plus
600 * (1 - use_transfers)seconds for each transfer.
So use_transfers: 0.5 costs 5 minutes per change. A direct bus that arrives 4 minutes later than a train-plus-bus trip wins; one that arrives 10 minutes later doesn’t.
The preferences pick between the journeys the search found. They don’t make it look for slower journeys. If the only way to get there involves a bus, use_bus: 0.2 still takes the bus, and use_bus: 0 returns 404.
Arriving by a time
Section titled “Arriving by a time”Send arrive_by instead of depart_at and Plaza returns the latest departure that still gets you there in time:
curl -X POST https://plaza.fyi/api/v1/route \ -H "x-api-key: $PLAZA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "origin": {"type": "Point", "coordinates": [-71.0620, 42.3567]}, "destination": {"type": "Point", "coordinates": [-71.1210, 42.3745]}, "mode": "transit", "arrive_by": "2026-03-16T09:00:00-04:00" }'The response has the same shape as a depart_at route. Its depart_at is when to leave the origin and arrive_at is when you actually get there, which is at or before arrive_by. If the 08:41 train gets you in at 08:58, that’s the answer, even if a later one would arrive at 09:01.
The search looks back 4 hours from the deadline. A deadline just after midnight can use the previous evening’s late trips.
arrive_by works on routes only. Sending it with depart_at, on an isochrone, or with another mode is a 422 validation_error.
Wheelchair access
Section titled “Wheelchair access”With wheelchair: true, Plaza rides only trips the feed marks wheelchair_accessible 1, and boards and alights only at stops marked wheelchair_boarding 1. A platform with no value takes its parent station’s. Unknown counts as not accessible, so a feed that leaves these fields blank has no accessible service at all.
curl -X POST https://plaza.fyi/api/v1/route \ -H "x-api-key: $PLAZA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "origin": {"type": "Point", "coordinates": [-71.0620, 42.3567]}, "destination": {"type": "Point", "coordinates": [-71.1210, 42.3745]}, "mode": "transit", "depart_at": "2026-03-16T08:00:00-04:00", "wheelchair": true }'It works on transit isochrones too. It also sets pedestrian.wheelchair, so the walks to and from stops avoid steps and steep or rough ways. Transfers are different: the walks between stops were measured with the default walking rules when the feed was loaded, so a transfer can include steps. Check the transfer legs before relying on a journey. If no accessible stop is within max_walk_m of an end, the request is a 404.
wheelchair is a transit option. For a wheelchair walking route, use mode: "foot" with pedestrian: {"wheelchair": true} (see Cycling and Walking Options); wheelchair: true with any mode other than transit is a 422 that says so.
Options transit doesn’t take
Section titled “Options transit doesn’t take”waypoints, alternatives, ev, truck (and its fields), exclude, avoid_datasets, datasets, traffic_model, steps, annotations, overview, geometries, lang, units, verbal, voice_instructions, banner_instructions, roundabout_exits, voice_units and waypoint_names are rejected with a 422 when mode is transit. On isochrones, so are denoise and generalize.
Transit isochrones
Section titled “Transit isochrones”curl -X POST https://plaza.fyi/api/v1/isochrone \ -H "x-api-key: $PLAZA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "geometry": {"type": "Point", "coordinates": [-71.0589, 42.3601]}, "time": [900, 1800], "mode": "transit", "depart_at": "2026-03-16T08:00:00" }'Each contour is the union of walking circles: one around the starting point, and one around every stop you can reach in time, sized by the time you have left when you get there. Both are capped at max_walk_m. The circles are straight-line distance at 5 km/h, not walks on the street graph, so a contour can cross a river where there’s no bridge.
Each Feature’s properties have time_seconds, mode (transit), stops_reached and area_m2. One time returns a Feature; a list returns a FeatureCollection, like other isochrones.
Isochrones ride every feed with a stop near the starting point, plus feeds one walk away from those (the same between-feed walks routes use). wheelchair works here; arrive_by doesn’t.
PlazaQL
Section titled “PlazaQL”route() and isochrone() take the same options as keyword arguments:
route( origin: point(42.3601, -71.0589), destination: point(42.3736, -71.1190), mode: "transit", depart_at: "2026-03-16T08:00:00", max_walk_m: 800, use_bus: 0.5);
isochrone( center: point(42.3601, -71.0589), time: [900, 1800], mode: "transit", depart_at: "2026-03-16T08:00:00-04:00");
route( origin: point(42.3567, -71.0620), destination: point(42.3745, -71.1210), mode: "transit", arrive_by: "2026-03-16T09:00:00-04:00", wheelchair: true);The results, errors and billing match REST. The transit options need mode: "transit"; on another mode, or another computation, they are a type error. The exception is wheelchair with mode: "foot", which is the walking option from Cycling and Walking Options. The walking keywords (walking_speed:, use_lit: and the rest) are the pedestrian object, as with mode: "foot".
Finding stops
Section titled “Finding stops”Each feed’s stops are a dataset you can search like any other. Properties are stop_id, name, code, kind (stop, station or entrance), parent_station, wheelchair_boarding, routes (the names of the routes serving the stop) and modes.
search(dataset("gtfs-mbta"), name: "Harvard");How it works
Section titled “How it works”Plaza runs RAPTOR (Round-Based Public Transit Routing) over the day’s timetable (forward from depart_at, or backward from arrive_by), which it loads into memory the first time a day is asked for and keeps while it’s in use. Each round adds one more vehicle. Walking legs to, between and from stops are measured on the OpenStreetMap street graph at 5 km/h. Walks between nearby stops (up to 500 m apart in a straight line, 1000 m on foot) are measured when the feed is loaded, including walks to stops of other feeds. transfers.txt minimum transfer times replace those walks, and transfer_type 3 removes them. A transfer is one of these walks, never two chained together.
Calendars are honoured: calendar.txt service days and calendar_dates.txt exceptions (holidays, special service). Trips that run past midnight count, so leaving at 00:15 can catch a train scheduled at 24:20:00 on the previous service day.
Limits
Section titled “Limits”- The search covers 4 hours of travel after departure, or before an
arrive_bydeadline. - No real-time data: delays and cancellations in GTFS-Realtime are not used.
- No fares.
- Feeds in different time zones aren’t combined in one journey.
- The first request for a day can get a 503
transit_unavailablewhile that day’s timetable loads; retry after a few seconds. A day too large to hold in memory gets the same 503 every time. - The whole request, including loading a timetable, gets about 15 seconds; past that it’s a 408
request_timeout.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 400 | bad_request | Missing or malformed origin, destination or geometry |
| 404 | not_found | No stop within max_walk_m of the origin or destination (and walking all the way is too far), no accessible stop there with wheelchair: true, or no journey: every option ruled out by use_bus, use_rail or max_transfers, or no arrival within 4 hours |
| 408 | request_timeout | The search ran past its time budget or your plan’s request timeout |
| 422 | validation_error | An option out of range, a bad depart_at or arrive_by, both of them at once, arrive_by on an isochrone, a time outside every nearby feed’s published period, an option transit doesn’t take, or arrive_by / wheelchair: true with another mode |
| 503 | transit_unavailable | The day’s timetable is still loading or too many days are loading at once (retry shortly), it failed to load, or it is too large to hold in memory |
Through PlazaQL, option errors are a 400 query_error, as for other computations. The other statuses match REST.