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.
Contract
Section titled “Contract”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.
Methods
Section titled “Methods”| 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 a stop, then read its arrivals
Section titled “Find a stop, then read its arrivals”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.
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 itshost:portinstead. - grpcurl can’t reach a gRPC-Web-only origin.
- Stop
90001234is synthetic; use a code you discovered. - grpcurl converts the JSON to protobuf. The server is still gRPC, not JSON.
- To subscribe, call
StreamStopArrivalswith the same fields, and cancel it when you’re done.
Defaults, units, and identifiers
Section titled “Defaults, units, and identifiers”- Arrival pages:
max_arrivalsdefaults to 20 when 0 or omitted and is clamped to 1–50.page<=0means page 1, and a page past the end is empty. Checkpage_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_locationsdefaults to false. When true, locations for buses on the returned page are added where available; arrivals still succeed without them. - Search:
limitdefaults 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_metersdefaults to 500 when 0 and is clamped to 100–5,000.limitworks 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 returnedBusPosition.service_nocan include a direction suffix, such asX42~1, and itsdirectionis per vehicle.StreamServiceBusesdoesn’t filter bydirection, and its responsedirectionisn’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_kmhis in kilometres per hour andheadingin degrees. GTFS Realtime uses metres per second for speed. - Time: timestamps are UTC instants. The stream
timestampis when the response was generated. Arrivalupdated_atis 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.
Clock pattern
Section titled “Clock pattern”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 receiptremaining 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.
Use it with a decoded message
Section titled “Use it with a decoded message”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, notdata.updated_at, an arrival time, orlast_gps_update.updated_atcan be older data time or the response time, and neither proves every row is fresh. UnaryGetStopArrivalshas no streamtimestamp, 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
nullfor 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:03Zand18:03+08:00are 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.
Reading arrival rows
Section titled “Reading arrival rows”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.
Streams
Section titled “Streams”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.
Current limitations
Section titled “Current limitations”BusPosition.next_stop_code,next_stop_name, andeta_to_next_stop_minutesare defined but not populated.NearbyStopResult.next_arrivaland the machine-learning ETA enum value are defined but not currently produced.StopArrivalsUpdate.delta_updateandServiceBusesUpdate.directionaren’t implemented.- Stop messages have content-derived IDs and an
expires_atone hour after retrieval, not the notice’s own publication or expiry time. The combined scrolling string uses•separators. StreamAnnouncementsstays open until cancelled but never emits.
Empty fields aren’t real measurements. Check field presence before showing them.