Skip to content

GTFS-RT feeds

Use GTFS-RT if your system already consumes GTFS Realtime. For rider apps and live streams, Frontline is recommended.

Both feeds are at GTFS_BASE_URL and need Authorization: Bearer ACCESS_TOKEN with the frontline:consumer scope. See the feed reference for the route inventory.

Request Returns
GET /gtfs-rt/trip-updates GTFS Realtime FeedMessage with trip updates
GET /gtfs-rt/vehicle-positions GTFS Realtime FeedMessage with vehicle positions

Responses are binary Protocol Buffers (application/x-protobuf), GTFS Realtime version 2.0, with FULL_DATASET incrementality: replace your view with each snapshot instead of applying it as a delta.

These are not static GTFS, JSON, or alerts feeds. Decode them with your language’s GTFS Realtime library and match them against the static GTFS identifiers from your MyBusz contact; this site has no static dataset download.

Terminal window
curl --fail --silent --show-error \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--dump-header trip-updates.headers \
--output trip-updates.pb.download \
--write-out 'HTTP %{response_code}\n' \
"${GTFS_BASE_URL}/gtfs-rt/trip-updates"

Accept the file only if the transfer completed with HTTP 200, the content type is application/x-protobuf, and it decodes as a FeedMessage. Downloading to a separate file means a failed attempt can’t overwrite good data. Use --fail, not --fail-with-body, so an error body is never saved as a feed, and don’t treat a redirect or partial transfer as a feed. Check the feed header and entity timestamps before showing data as current.

200 and 304 responses include:

Header Meaning
ETag Weak validator (W/"…") based on feed content, not a byte checksum
Last-Modified When the feed artifact was generated, not each vehicle’s observation time
Cache-Control: private, no-store Don’t store the response in any HTTP or shared cache

Send the last ETag back exactly, including quotes and any W/ prefix, as If-None-Match:

Terminal window
curl --fail --silent --show-error \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "If-None-Match: ${FEED_ETAG}" \
--dump-header trip-updates.headers \
--output trip-updates.pb.download \
--write-out 'HTTP %{response_code}\n' \
"${GTFS_BASE_URL}/gtfs-rt/trip-updates"
  • 200: decode the new body, then update your data and stored ETag.
  • 304 Not Modified: no body; keep your current data. The feed content is unchanged, which doesn’t mean no vehicle has moved.
  • With no usable earlier data, make an unconditional request.

Rely on If-None-Match, not If-Modified-Since. An unchanged feed can keep its ETag even when timestamps or encoded bytes differ.

A missing or stale feed returns 503 Service Unavailable, not an empty feed, even when your ETag matches. A feed is stale after 300 seconds by default (configurable per feed). That is the artifact’s age, not a guaranteed delivery latency.

Entities are dropped when observations or trip matches are unusable, and vehicle positions can lack trip details, so a 200 doesn’t prove full fleet coverage. If you keep showing data during an outage, label it as stale.

You get 10 requests per minute per feed for each identity, whatever your credential type, and the two feeds have separate budgets. 304 responses count. Poll each feed about every 10 seconds, one request at a time, and share the budget across all workers using the same identity. A 304 is normal; don’t poll faster because of it.

See rate limits and retry and backoff.