Skip to content

Frontline rider API

Frontline is the recommended API for rider apps, passenger displays, and maps. It serves stop search, arrivals, route details, and live bus streams at FRONTLINE_BASE_URL over native gRPC or gRPC-Web.

Every method needs the frontline:consumer scope: send authorization: Bearer ACCESS_TOKEN as metadata. Frontline is not a JSON REST API and doesn’t support Connect or Server-Sent Events. Browsers need a gRPC-Web client and an approved browser origin; native gRPC and gRPC-Web clients aren’t interchangeable.

The service is mybusz.frontline.FrontlineService. Wire paths are /mybusz.frontline.FrontlineService/METHOD; grpcurl omits the leading slash.

Generate a client from the consumer schema (.proto files and their imports) supplied by your MyBusz contact. This site doesn’t host generated clients or the backend .proto files. Don’t rely on server reflection in production.

See the RPC reference for signatures and the message and enum reference for fields. Declared fields aren’t always populated; see current limitations.

Method Kind What it does
GetStopArrivals Unary Paginated arrivals for bus_stop_code. A blank code returns INVALID_ARGUMENT.
StreamStopArrivals Server stream Same selection: an initial snapshot, then full refreshes on updates or at a 30-second fallback interval.
GetBusPosition Unary Latest position for plate_no. Blank or unknown vehicles return an empty position, not NOT_FOUND.
StreamServiceBuses Server stream All buses on a base service_no: an initial list, then refreshed lists after a 3-second polling delay. Ignores direction.
GetServiceDetails Unary Metadata, encoded route polyline, ordered stops, origin, and destination for service_no and direction. An unknown pair can return NOT_FOUND.
GetServicesAtStop Unary Service-and-direction rows for bus_stop_code, with known route-node visits and next-arrival hints where available.
SearchBusStops Unary Stops matching query by name or code, up to limit. Queries shorter than two characters return nothing, not an error.
FindNearbyStops Unary Stops within radius_meters of location, with distances and service numbers.
GetStopMessages Unary Messages and scrolling_message for bus_stop_code. The list can be empty.
StreamAnnouncements Server stream Currently never emits, not even an initial snapshot or heartbeat. Don’t use it for alerts.

Find stops with SearchBusStops or FindNearbyStops, then pass the stop’s operational code to stop methods, not its numeric bus_stop_id, name, or display label. A stop alias you send can resolve to a different code in the response.

This native gRPC example uses grpcurl with the consumer schema checked out at CONSUMER_PROTO_ROOT. Adjust the paths to match your copy.

Terminal window
grpcurl \
-import-path "${CONSUMER_PROTO_ROOT}" \
-proto frontline/frontline_service.proto \
-H "authorization: Bearer ${ACCESS_TOKEN}" \
-d '{"busStopCode":"90001234","maxArrivals":20,"page":1}' \
"${FRONTLINE_BASE_URL#https://}:443" \
mybusz.frontline.FrontlineService/GetStopArrivals
  • The command assumes an https:// origin with no port or path and connects on port 443. If your origin has a port, pass its host:port instead.
  • grpcurl can’t reach a gRPC-Web-only origin.
  • Stop 90001234 is synthetic; use a code you discovered.
  • grpcurl converts the JSON to protobuf. The server is still gRPC, not JSON.
  • To subscribe, call StreamStopArrivals with the same fields, and cancel it when you’re done.
  • Arrival pages: max_arrivals defaults to 20 when 0 or omitted and is clamped to 1–50. page<=0 means page 1, and a page past the end is empty. Check page_info. Each row is a service, direction, and stop call, not necessarily one physical bus. A stream keeps its requested page while the contents and totals change.
  • Bus locations: include_bus_locations defaults to false. When true, locations for buses on the returned page are added where available; arrivals still succeed without them.
  • Search: limit defaults to 10 when 0 or omitted and is clamped to 1–50. Results include relevance scores and service numbers.
  • Nearby stops: send WGS 84 latitude and longitude in decimal degrees. radius_meters defaults to 500 when 0 and is clamped to 100–5,000. limit works as in search. Distances are in metres.
  • Directions: 0 is outbound and 1 is inbound. Request services by base number, such as synthetic X42. A returned BusPosition.service_no can include a direction suffix, such as X42~1, and its direction is per vehicle. StreamServiceBuses doesn’t filter by direction, and its response direction isn’t populated, so filter buses yourself.
  • Stream identifiers: service numbers and resolved stop codes for streams must be non-blank and use only letters, digits, _, -, or :. Don’t send wildcards, spaces, or a ~ service key.
  • Position units: speed_kmh is in kilometres per hour and heading in degrees. GTFS Realtime uses metres per second for speed.
  • Time: timestamps are UTC instants. The stream timestamp is when the response was generated. Arrival updated_at is the arrival data’s time, or the response time when unavailable. Per-bus GPS timestamps are observation times. A new snapshot doesn’t make every observation in it fresh. See UTC arrival countdowns.

Reference implementation: UTC arrival countdowns

Section titled “Reference implementation: UTC arrival countdowns”

next_arrival.time and later_arrival.time are absolute UTC timestamps, not durations. Your app derives labels such as “3 min” or “Arriving” from them.

An arrival at 2026-10-03T10:03:00Z stays the same while its countdown changes (synthetic times):

Approximate current UTC time Arrival UTC time Remaining time
10:00:00Z 10:03:00Z 3 minutes
10:00:30Z 10:03:00Z 2.5 minutes
10:01:00Z 10:03:00Z 2 minutes

A repeated snapshot must not restart the countdown at three minutes. If the server revises the arrival, use the new instant.

Capture performance.now() as soon as a stream message arrives, before any asynchronous mapping, and pair it with the message’s server timestamp:

approximate now = server UTC at generation + monotonic elapsed since receipt
remaining minutes = max(0, (arrival UTC - approximate now) / 60,000)

The reference implementation below is framework-neutral, not a complete client:

function makeTimeReference(serverEpochMs, receivedMonotonicMs) {
if (!Number.isFinite(serverEpochMs) || !Number.isFinite(receivedMonotonicMs)) {
return null;
}
return { serverEpochMs, receivedMonotonicMs };
}
function minutesUntil(
arrivalEpochMs,
reference,
monotonicNowMs = performance.now(),
wallNowEpochMs = Date.now(),
) {
if (!Number.isFinite(arrivalEpochMs)) return null;
const nowEpochMs = reference
? reference.serverEpochMs + (monotonicNowMs - reference.receivedMonotonicMs)
: wallNowEpochMs;
if (!Number.isFinite(nowEpochMs)) return null;
return Math.max(0, (arrivalEpochMs - nowEpochMs) / 60_000);
}

Epoch milliseconds hold UTC instants and monotonic milliseconds hold elapsed time. They share a unit, not an origin: performance.now() alone isn’t a UTC time. With a reference, changes to the client’s clock don’t affect the countdown. Without one, the helper falls back to the uncalibrated local clock.

This example uses Protobuf-ES @bufbuild/protobuf 1.10.0, whose decoded Timestamp has .toDate(). Given a decoded stream update and one of its bus rows:

// Capture this immediately when the stream message is received.
const receivedMonotonicMs = performance.now();
const reference = makeTimeReference(
update.timestamp?.toDate().getTime(),
receivedMonotonicMs,
);
const arrival = bus.nextArrival;
const arrivalEpochMs = arrival?.time?.toDate().getTime();
const remaining = minutesUntil(arrivalEpochMs, reference);
// Preserve arrival.isLive separately; render missing time as unavailable.

These are the generated client’s camelCase fields for protobuf next_arrival and is_live, not plain JSON objects. Other SDKs, including newer Protobuf-ES versions, have their own timestamp conversion. Let the decoder convert: timestamps carry seconds and nanoseconds, and Date.getTime() drops anything below a millisecond.

Keep countdown, source, and freshness separate

Section titled “Keep countdown, source, and freshness separate”
  • Clock reference: use the stream’s timestamp, not data.updated_at, an arrival time, or last_gps_update. updated_at can be older data time or the response time, and neither proves every row is fresh. Unary GetStopArrivals has no stream timestamp, so use a trusted client clock or another established time reference rather than inventing one.
  • Ticking: recompute from the stored instant on every UI tick; no new request is needed. A one-second tick is enough. In a reactive UI, read an explicit tick signal, because performance.now() alone won’t invalidate a memoized value. Don’t decrement a stored “minutes remaining”, since delayed or background ticks skip time.
  • Live or scheduled: keep is_live. It separates a realtime estimate from a schedule; it doesn’t say whether data is fresh. For example, you might round minutes down, show “Arriving” for live arrivals under two minutes, and show a clock time for longer waits. Those are UI choices, not API guarantees. Keep scheduled arrivals visibly labelled.
  • Missing or past times: the helper returns null for a missing or invalid time and clamps past times to zero. Show missing times as unavailable. Zero doesn’t mean the bus is at the stop, so don’t show “Arriving” indefinitely. Track connectivity and data freshness separately.
  • Delivery delay: the reference doesn’t measure network or buffering delay. It treats generation time as the receipt time, so it can overstate the remaining time by that delay. It reduces clock skew; it isn’t precise synchronization or an accuracy guarantee.
  • Reconnects and sleep: a monotonic reference belongs to one page or context. Don’t persist it across reloads or share it between contexts. Update it on every stream message, and get a new one after the device or browser resumes from sleep. Invalidating it is your code’s job; the helpers can’t detect this. You can keep the last reference on disconnect or clear it to fall back to the uncalibrated local clock. Neither makes old arrivals fresh.
  • Time zones: do arithmetic on UTC instants and convert only the final label. 10:03Z and 18:03+08:00 are the same instant, so never add eight hours to an epoch value. Browser-local formatting follows the device’s time zone; set one explicitly if your display needs it regardless of device settings.

next_arrival and later_arrival can be absent. Each present arrival’s is_live separates a realtime estimate from a schedule. A later arrival can be a second live bus or a scheduled fallback, so don’t infer live status from its position. Rows without an estimate can still carry useful service details. Rows with arrival times come first, ordered by time.

A bus can visit the same stop more than once, so service_no alone isn’t a unique key. Add direction and, when present, stop_order and visit_index. stop_order=0 is valid; an absent stop_order means unknown. visit_index starts at 1 when known. GetServicesAtStop groups rows by service and direction and lists known visits; its next-arrival hint can be live or scheduled.

delay_minutes is signed: negative means early. Zero can also mean no schedule comparison was possible, so check the arrival, schedule, and source fields rather than trusting zero or ON_TIME alone. Staleness categories describe GPS age: under 2 minutes, 2 to under 5, 5 to under 30, and 30 minutes or more. Unspecified means no GPS age is available.

Arrival and service-bus messages are full snapshots. Replace your state for that stop page or service with each one instead of appending. An empty service-bus snapshot clears that service’s buses. delta_update is defined but unused.

There’s no resume token, replay, or guaranteed latency. After a disconnect, open a new stream and use its initial snapshot. Read messages promptly: the server can end a stream when a write takes over 15 seconds. Cancel a stream when its view closes or its selection changes. Renew your token before opening replacement streams, and don’t assume an open stream picks up a new token.

Opening a stream counts toward your Frontline rate limit; messages on an open stream don’t. Each identity can hold up to 1,000 concurrent streams.

  • BusPosition.next_stop_code, next_stop_name, and eta_to_next_stop_minutes are defined but not populated.
  • NearbyStopResult.next_arrival and the machine-learning ETA enum value are defined but not currently produced.
  • StopArrivalsUpdate.delta_update and ServiceBusesUpdate.direction aren’t implemented.
  • Stop messages have content-derived IDs and an expires_at one hour after retrieval, not the notice’s own publication or expiry time. The combined scrolling string uses • separators.
  • StreamAnnouncements stays open until cancelled but never emits.

Empty fields aren’t real measurements. Check field presence before showing them.