Overview
Products
Reference
Journey planning
Plan a door-to-door journey between two coordinates. Results include full leg geometry, per-leg fare estimates, and live vehicle positions for buses already on the road.
422 outside_coverage rather than an empty result, so you can tell
"we can't plan here" apart from "no journeys found".
GET /v1/journey.json?from_lat={lat}&from_lon={lon}&to_lat={lat}&to_lon={lon}
Parameters
| Name | Default | Description |
|---|---|---|
from_lat, from_lon | required | Origin coordinate. |
to_lat, to_lon | required | Destination coordinate. |
datetime | now | ISO 8601 departure time, or arrival time when arrive_by=true. |
arrive_by | false | Treat datetime as the latest acceptable arrival. |
transit_modes | tram,rail | Optional transit sub-modes to include. Bus and coach are always searched. Pass an empty value (transit_modes=) for bus and coach only. |
modes | walk,bicycle | Non-transit alternatives returned alongside the transit result. Pass an empty value to skip them. |
include_fares | true | Attach fare estimates. |
include_live | true | Attach live vehicle positions. |
include_intermediate_stops | false | List every stop called at on each transit leg. |
max_walk_distance | — | Metres. Caps the longest single walking leg. |
max_total_walk_distance | — | Metres. Caps the total walked across the journey. |
include_steps | false | Turn-by-turn directions on each walking leg. |
limit | 5 | Maximum itineraries, 1–20. |
Fares
Each transit leg carries a fare (GBP, or null if we
can't price it), and each itinerary a total:
"fare": { "amount_gbp": 3.45, "complete": true }
complete is false when at least one transit leg went
unpriced — the total is then a lower bound, not a full estimate. The whole object
is null when no leg could be priced at all, so an unpriceable
journey never masquerades as a cheap one.
These are estimates, not tickets. Bus fares come from
published NeTEx Adult Single data and don't account for multi-operator tickets,
daily or weekly caps, or any concession. Rail fares are the cheapest standard
walk-up single for the station pair, and are served from a cache — a pair that
has never been looked up returns null rather than a live price.
Operators without imported fare data always return null.
Live vehicles
A transit leg whose vehicle is already running gains a live
block, sourced from the BODS SIRI-VM feed:
"live": {
"vehicle_id": "SYRK:SYRK-19514",
"fleet_id": "SYRK-19514",
"delay_seconds": 0,
"latitude": 53.3461,
"longitude": -1.3479,
"bearing": 36.0,
"recorded_at": "2026-08-05T17:22:00+02:00",
"stale": false
}
Only imminent legs match anything. A vehicle appears on its trip shortly
before it departs and is gone once the trip finishes, so a journey later today
simply has no live data to attach — there is nothing to report about a bus that
hasn't set off. stale is true when the last fix is over
two minutes old.
Separately, start_time and end_time on every leg
carry the timetabled time plus a real-time estimated time and
delay_s where the operator supplies predictions:
"start_time": {
"scheduled": "2026-08-05T16:41:00+01:00",
"estimated": null,
"delay_s": null
}
Walking limits
Two independent caps, both in metres. max_walk_distance limits
the longest single walking leg — the useful one for a mobility limit, since a
journey made of three separate 400m walks is very different from one with a
single 1.2km walk. max_total_walk_distance limits the sum instead.
Use either or both.
Every itinerary reports what it actually asks of you, so you can rank or reject without adding the legs up yourself:
"walk_distance_m": 536.0,
"longest_walk_m": 307.0
How the limit is enforced. OpenTripPlanner has no maximum walk distance of its own — its walk preferences only shape cost. So a limit does two things here: it raises walking's cost so the search prefers nearer stops, and it then discards anything that still exceeds the cap. The result is a real guarantee rather than a preference, at the cost of sometimes returning fewer itineraries than you asked for.
When a limit is in play the response says so, including how many options it removed:
"walk_limits": {
"max_walk_distance": 500,
"max_total_walk_distance": null,
"excluded_itineraries": 9
}
An empty itineraries list alongside a non-zero
excluded_itineraries means journeys exist but all of them walk too
far — which is a different problem from nothing running, and worth telling your
users apart.
Neither limit applies to the direct walking alternative under
alternatives.walk. Filtering a walking route by how far it walks
would leave someone who asked for one with nothing.
Turn-by-turn directions
With include_steps=true, every walking leg carries a
steps array:
"steps": [
{
"relative_direction": "RIGHT",
"absolute_direction": "NORTHEAST",
"street_name": "Fargate",
"distance_m": 69.0,
"latitude": 53.3806,
"longitude": -1.4699,
"exit": null,
"stay_on": false,
"area": false
}
]
relative_direction is the instruction — DEPART,
CONTINUE, LEFT, SLIGHTLY_RIGHT,
HARD_LEFT, CIRCLE_CLOCKWISE, STAIRS and
others. absolute_direction is the compass bearing, which is what
makes the first instruction usable before someone is moving and has no heading
to work from. exit is set on roundabouts, stay_on
when a road continues under a new name and no turn is needed, and
area when crossing an open space such as a square with no street
to name.
Steps are off by default because they roughly double the size of a response.
Transit legs have no steps — use include_intermediate_stops for
the calling points instead.
Alternatives and empty windows
Walking and cycling routes are computed alongside the transit search and
returned under alternatives, keyed by mode. Transit is always the
primary itineraries array; a walk-only result never appears there
even when walking beats the bus, since it belongs under
alternatives.walk.
When nothing runs in the requested window — late at night, after the last
bus — the response sets next_available: true and returns the next
departures found instead, which may be the following morning, rather than an
empty list.
Example
curl "https://api.transcapi.com/v1/journey.json?\
from_lat=53.3811&from_lon=-1.4701&to_lat=53.4106&to_lon=-1.4290" \
-H "X-Api-Key: YOUR_API_KEY"
{
"request_time": "2026-08-05T15:25:40Z",
"coverage": {
"area": "South and West Yorkshire",
"bbox": [53.2989, -2.1755, 53.9649, -0.8529]
},
"next_available": false,
"itineraries": [
{
"start_time": "2026-08-05T16:36:36+01:00",
"end_time": "2026-08-05T17:09:14+01:00",
"duration_s": 1958.0,
"fare": { "amount_gbp": 2.80, "complete": true },
"legs": [
{
"mode": "WALK",
"transit": false,
"distance_m": 306.86,
"duration_s": 264.0,
"from": { "name": "Origin", "latitude": 53.3811, "longitude": -1.4701, "atcocode": null },
"to": { "name": "Arundel Gate/AG6", "latitude": 53.38065, "longitude": -1.46629, "atcocode": "370023169" },
"leg_geometry": "ki`dI..."
},
{
"mode": "BUS",
"transit": true,
"headsign": "Maltby",
"fare": 2.80,
"stop_count": 13,
"trip_id": "VJfde1c8a6f4a5b81dfb398429a6ef7793",
"route": {
"line": "X1",
"operator": "First South Yorkshire",
"operator_id": "OP153",
"mode": "BUS"
},
"start_time": { "scheduled": "2026-08-05T16:41:00+01:00", "estimated": null, "delay_s": null },
"from": { "name": "Arundel Gate/AG6", "atcocode": "370023169" },
"to": { "name": "Meadowhall Road/Weedon Street", "atcocode": "370020991" }
}
]
}
],
"alternatives": {
"walk": { "itineraries": [] },
"bicycle": { "itineraries": [] }
}
}
Fares and live positions are enrichments, not the answer: if that data is
temporarily unavailable the journey is still returned, with those fields
null.
Check the changelog for updates as coverage expands.