ptv is a command-line companion for Victorian public transport. It combines
the PTV Timetable API v3 (real-time departures, disruptions, line and
station information) with the PTV GTFS static feed (multi-modal journey
planning) to bring Google-Maps / Transit-app style functionality to the
terminal — exclusively for VIC PTV.
- 🔎 Search stops, routes (lines) and myki outlets.
- 🚆 Inspect train / tram / bus / V/Line lines, their directions and stops.
- 🏟️ Show station facilities, platforms and accessibility info.
- ⏱️ See how soon the next services depart a stop, with live countdowns,
delays, platforms and disruptions (
ptv next). - 🗺️ Plan multi-modal A→B journeys (train + tram + bus + walking transfers)
with earliest-arrival (
--depart) and latest-departure (--arrive-by) modes (ptv plan). - 📍 Enter place names and addresses (e.g. "Federation Square", "121
Exhibition St, Melbourne") — geocoded via OpenStreetMap Nominatim — instead
of
lat,lng. ⚠️ Real-time disruption flagging insideptv plan: any line your journey uses is checked for active disruptions and flagged inline.- 🚉 Best-effort live vehicle lookup (
ptv vehicle) from PTV vehicle descriptors/positions where the API exposes them, with optional Transport Victoria GTFS Realtime train, tram, bus and V/Line enrichment. - 🚋 Mode-scoped commands
ptv tram,ptv bus,ptv vlinefor a full per-route view pluslinesandnextsubcommands. - 🔐 Cross-platform secure credential storage in the OS keyring.
--jsonoutput on every command for scripting and AI agents.
Prebuilt binaries for Linux, macOS and Windows (amd64 + arm64) are attached to
each GitHub Release.
Download the archive for your platform, extract ptv, and put it on your
PATH:
# example (macOS arm64)
tar -xzf ptv_*_Darwin_arm64.tar.gz
sudo mv ptv /usr/local/bin/
ptv versionmacOS release binaries are not yet Developer ID signed/notarized. If Gatekeeper
blocks a downloaded ptv binary, remove the quarantine attribute after you have
verified the release archive/checksum:
xattr -d com.apple.quarantine /usr/local/bin/ptvgo build -o ptv .
# optionally: mv ptv /usr/local/bin/Requires Go 1.25+. The journey planner uses a local SQLite database
(modernc.org/sqlite, pure Go — no cgo).
You need a PTV Timetable API key and user/dev id. Request them from PTV (see the PTV API access page).
Credentials are resolved in this order:
- Environment variables
PTV_API_KEYandPTV_API_USERID. - The OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret
Service) — populated via
ptv auth login.
For local development only, pass --env-file <path> to load a dotenv file
explicitly. The CLI does not auto-read .env from the working directory.
Optional Transport Victoria Open Data features use separate credentials from
the PTV Timetable API. Create an account at
https://opendata.transport.vic.gov.au/, subscribe to the public transport data,
then set the Open Data subscription key as PTV_OPENDATA_KEY_ID in the
environment or in an explicit --env-file; PTV_OPENDATA_KEYID is accepted as
an alias. If the portal also gives you a data platform API token, set it as
PTV_OPENDATA_API_ID. Without these Open Data credentials, the core PTV
Timetable API features still work; commands that can use GTFS Realtime data will
print a warning and skip that enrichment.
Store them securely in the OS keyring:
ptv auth login # prompts, verifies against the API, stores in the keyring
ptv auth status # shows where credentials are being read from
ptv auth check # makes a signed test call
ptv auth logout # removes credentials from the keyringOpen Data credentials can also be stored in the OS keyring, independently from the PTV Timetable API credentials:
ptv auth opendata login # prompts for Open Data key/token, verifies GTFS-R
ptv auth opendata status # shows whether Open Data credentials are configured
ptv auth opendata check # makes a GTFS Realtime test request
ptv auth opendata logout # removes stored Open Data credentialsThe PTV Timetable API v3 remains the main source for stop/route lookup, departures, runs, disruptions and local journey context. Transport Victoria Open Data GTFS Realtime is separate and is often better for live vehicle identity and position because its vehicle-position feeds cover train, tram, bus and V/Line directly and frequently expose vehicle IDs/labels that v3 omits.
Current CLI wiring:
ptv vehicleuses GTFS-R vehicle-position feeds for all four mode groups when Open Data credentials are configured, and can find vehicles even when v3 has novehicle_descriptor.ptv gtfs realtimelists and inspects the official GTFS-R trip update, service alert and vehicle-position feeds for debugging or scripting.ptv next,ptv planandptv disruptionsstill use the Timetable API v3 and local static GTFS today; GTFS-R trip updates and service alerts are exposed for inspection but are not yet merged into those commands.
The signature scheme is HMAC-SHA1 over
"{path}?{query-incl-devid}", keyed by the API key; the uppercase-hex result is appended as&signature=.
Trip planning needs the PTV GTFS static feed ingested into a local database:
ptv gtfs update # downloads (~210 MB) and ingests into SQLite
ptv gtfs status # shows ingest time, data age/staleness and row counts
ptv gtfs check # asks the endpoint whether a newer feed is available
ptv gtfs realtime # list Transport Victoria GTFS Realtime feedsThe feed is a zip-of-zips (one feed per mode). Stops and routes are namespaced
{feedMode}:{id} to avoid cross-feed collisions, and proximity walk-transfers
(≤250 m) are generated to connect stops across modes for multi-modal routing.
PTV publishes the GTFS feed roughly weekly, and each export only contains a rolling ~30 days of timetable data. If your local copy falls outside that window, the planner will simply find no services for the requested date — so it pays to stay current.
ptv helps in two ways:
- Staleness warning — if the local data is older than 7 days (override
with
PTV_GTFS_STALE_DAYS),ptv planprints a warning to stderr. - Upstream update detection —
ptvrecords the feed'sETag/Last-Modifiedon ingest and compares them against the endpoint with a cheapHEADrequest, throttled to once per 24h.ptv planflags when a newer feed is available;ptv gtfs checkforces an immediate check.
Both checks are non-blocking (they only warn) and run during ptv plan
unless you pass --no-update-check. Warnings go to stderr, so --json
output on stdout stays clean for scripts and agents.
Transport Victoria Open Data also publishes GTFS Realtime protobuf feeds for
trip updates, vehicle positions and service alerts. Live testing from this repo
currently returns 401 for unauthenticated feed requests, so create an account
at https://opendata.transport.vic.gov.au/ and configure PTV_OPENDATA_KEY_ID
before fetching. Some accounts also require PTV_OPENDATA_API_ID. Use
ptv gtfs realtime to list the known feed catalog, or fetch one/all feeds when
Open Data credentials are configured:
ptv gtfs realtime
ptv --env-file .env gtfs realtime bus-vehicle-positions --json
ptv --env-file .env gtfs realtime --allThe command reports feed timestamps and entity counts. ptv vehicle already
uses the vehicle-position feeds; trip updates and service alerts are currently
available through this inspection command for debugging/scripting and are not yet
merged into ptv next, ptv plan or ptv disruptions.
ptv auth Manage and verify API credentials
ptv search Search stops, routes and outlets
ptv lines List transport lines/routes (lines show <route> for detail)
ptv stops Find stops near a location (stops near) or on a route (stops on)
ptv station Show facilities and platforms for a station/stop
ptv next How soon the next services depart a stop (real-time)
ptv vehicle Best available live information for a vehicle id or run_ref
ptv tram Tram route info, stops, departures and disruptions
ptv bus Bus route info, stops, departures and disruptions
ptv vline V/Line route info, stops, departures and disruptions
ptv plan Plan a multi-modal journey between two places
ptv disruptions View current and planned service disruptions
ptv fare Estimate a myki fare by zone
ptv outlets List myki ticket outlets
ptv gtfs Manage the local GTFS dataset used for journey planning
ptv version Print version and build information
Global flags: --json, --limit.
ptv tram, ptv bus and ptv vline give an ergonomic, per-mode entry point:
ptv tram 109 # route header, directions, ordered stops + disruptions
ptv tram lines # list every tram route
ptv tram next "Melbourne University" # live departures from a stop (tram only)
ptv bus 246
ptv vline 1745 # Geelong - MelbourneEach accepts --json for structured output.
ptv vehicle (alias ptv vehicles) combines the PTV Timetable API with
Transport Victoria GTFS Realtime vehicle-position feeds. In practice GTFS-R is
often the superior source for physical vehicle identity and live position,
especially for bus and V/Line and for all-mode direct ID lookup. The Timetable
API does not provide a direct "find vehicle by id" endpoint, so the command first
uses v3 stop/run context when you provide hints, then uses GTFS-R
vehicle-position feeds for train/tram/bus/VLine matches when Open Data
credentials are configured.
# Search a Metro train car/consist component seen at a stop/line.
ptv vehicle 243M --stop Mordialloc --route Frankston
# Search a tram number from a stop context.
ptv vehicle 6059 --stop "Melbourne Central Station"
# Treat the argument as a PTV run_ref if no physical vehicle id is found.
ptv vehicle 952377 --json
# Enable optional GTFS Realtime enrichment from an explicit env file.
ptv --env-file .env vehicle '17-903--1-Sun12-903738' --stop 11293
# Search a physical bus id from the GTFS Realtime vehicle-position feed.
ptv --env-file .env vehicles BS11ZU --stop Chadstone
# Search a train consist component from the GTFS Realtime vehicle-position feed.
ptv --env-file .env vehicle 381M
# Slow fallback: scan a bounded number of active route runs.
ptv vehicle 243M --scan-routes 20What PTV currently exposes in live output:
- Metro trains:
vehicle_descriptor.operatoris usuallyMetro Trains Melbourne;vehicle_descriptor.idis a consist string such as113M-114M-1357T-1422T-243M-244M;vehicle_descriptor.descriptionincludes values observed in an all-route/all-stop scan:3 Car Silver Hitachi,3 Car Xtrapolis,6 Car Comeng,6 Car Siemens,6 Car Xtrapolis. - Trams: some stop departure contexts expose
vehicle_descriptor.operatorasYarra Tramsand a numericvehicle_descriptor.idsuch as6059; the description is often blank and route-filtered scans may not expose it. - GTFS Realtime: with
PTV_OPENDATA_KEY_ID,ptv vehiclechecks the official train, tram, bus and V/Line vehicle-position feeds for matchingrun_ref/trip ids, exact vehicle ids/labels, and consist components. It can return GTFS-R position, occupancy and status directly when PTV descriptors are absent. - V/Line: no Timetable API
vehicle_descriptordata was observed in broad live sampling, but GTFS-R vehicle positions expose live V/Line vehicle ids.
The Transport Victoria Open Data endpoint is useful beyond vehicle enrichment: it
also publishes GTFS Realtime trip updates, service alerts and vehicle-position
feeds for Metro Train, Yarra Trams, Metro & Regional Bus, and V/Line. ptv vehicle currently uses the vehicle-position feeds for all four mode groups;
ptv gtfs realtime exposes trip updates and alerts for inspection until they are
merged into higher-level commands.
Shortfalls:
- New, testing, commissioning, non-revenue or not-currently-running vehicles may exist in external fleet references but not appear in PTV live data.
last_spottedmeans the vehicle appeared in earlier PTV departure data for the hinted stop/route and does not appear in upcoming departures there now.- Use external fleet references to confirm that a vehicle exists or belongs to a
set/class; use
ptv vehicleonly for what PTV currently exposes live.
# Real-time departures
ptv next "Flinders Street" --mode train --limit 6
# Plan a trip leaving now
ptv plan "Flinders Street" "Box Hill"
# Use a place name or street address as origin/destination (geocoded)
ptv plan "Federation Square" "Melbourne Zoo"
ptv plan "121 Exhibition St, Melbourne" "Box Hill"
# Leave at a specific time
ptv plan "Flinders Street" "Southern Cross" --depart 17:30
# Arrive no later than a time, using coordinates as the origin.
# A leading '-' (Melbourne latitudes) must follow a '--' separator.
ptv plan --arrive-by 09:00 -- "-37.8183,144.9671" "Camberwell"
# Plan flags
# --depart HH:MM | "YYYY-MM-DD HH:MM" leave at (default: now)
# --arrive-by HH:MM | "..." arrive no later than
# --date YYYY-MM-DD service date for HH:MM times
# --radius <metres> search radius for lat,lng / geocoded points
# --no-geocode match local stop names only (no address lookup)
# --no-disruptions skip the real-time disruption overlay
# --no-update-check skip the GTFS staleness / upstream-update check<from>/<to> accept a stop name (prefix/substring matched across all modes,
so a station's train, tram and bus stops are all considered), a lat,lng
coordinate, or a free-text place / address that is geocoded via
OpenStreetMap Nominatim (biased to Victoria). Local stop-name matches take
precedence; geocoding is the fallback. Use --no-geocode to disable it.
ptv stops near also accepts either lat,lng coordinates or a place/address,
for example ptv stops near "36 McClelland Drive, Mill Park".
After planning, ptv plan checks every line your journey uses against the live
PTV disruptions feed. Affected legs are flagged with ⚠ and a Disruptions
section lists each active disruption (status, title and URL). With --json,
each affected leg carries disrupted / disruption_ids and the journey gains a
disruptions array. The overlay is best-effort — if it can't reach the API it
is skipped with a one-line note (the planner itself is fully local).
internal/
config/ credential resolution (env → keyring; explicit --env-file), paths
credstore/ cross-platform OS keyring wrapper (go-keyring)
geocode/ OpenStreetMap Nominatim client (VIC-biased, cached, throttled)
ptvapi/ HMAC-SHA1 signer, HTTP client, typed v3 responses, endpoints
gtfs/ downloader, zip-of-zips ingest, SQLite schema/queries, timetable
router/ Connection Scan Algorithm (earliest-arrival + latest-departure)
model/ shared domain types (Stop, Connection, Journey, Leg, ...)
render/ table output helper
cmd/ cobra command definitions
The router uses the Connection Scan Algorithm (CSA). Latest-departure
(--arrive-by) is implemented by running a forward scan over a time-reversed
connection set and flipping the resulting legs back to forward time.
Times are handled in Australia/Melbourne; the Timetable API uses UTC and is
converted for display.
Every command accepts --json, emitting stable, structured output suitable for
scripts and AI agents. Examples:
ptv plan "Federation Square" "Box Hill" --json # legs[], disruptions[], per-leg disrupted/disruption_ids
ptv tram 109 --json # route, directions, stops, disruptions
ptv next "Flinders Street" --mode train --json
ptv gtfs status --json # counts + freshness{} report
ptv gtfs check --json # upstream update check
ptv gtfs update --json # ingest counts after update
ptv version --jsonTagging a commit vX.Y.Z triggers the Release GitHub Action, which first
runs go build ./..., go vet ./..., and go test ./..., then runs
GoReleaser to cross-compile ptv for
linux/macOS/windows × amd64/arm64 (pure-Go, CGO_ENABLED=0) and attaches the
archives + checksums.txt to a GitHub Release. The binary's version,
commit and build date are stamped via ldflags and surfaced by
ptv version.
git tag v0.1.0
git push origin v0.1.0 # → builds and publishes the releaseA lightweight CI workflow runs go build/go vet/go test on pushes to
main and pull requests.
go build ./...
go vet ./...
go test ./...Tests cover the HMAC signer, the CSA planner (earliest-arrival, latest-departure and the no-journey case) on a fixture network, and GTFS id parsing.
- Never commit
.envor credentials. Preferptv auth login(keyring), or use--env-file <path>only when you intentionally need dotenv-based local setup. - The GTFS download is large; ingest once and refresh on demand.
- There is no journey-planning endpoint in the PTV API —
ptv planis built entirely from the GTFS feed, optionally surfacing real-time data viaptv next.