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.
Coverage: journey planning covers England, Scotland and
Wales, by bus, coach, rail, Underground, metro, tram and ferry, and the whole
island of Ireland, north and south, by bus, coach, Luas and rail (cross-border
journeys like Belfast to Dublin included), and the Netherlands and Belgium by
train, bus, tram, metro and ferry, planned together (Amsterdam to Brussels
works). Great Britain, Ireland and the Netherlands with Belgium are planned
as separate areas, so a journey from one to another returns
422 outside_coverage, as does an origin or destination in the Isle
of Man. That's rather than an empty result, so you can tell "we can't plan
here" apart from "no journeys found". In Ireland, train legs get live expected
times from Irish Rail, and bus legs in the Republic get them from the
NTA, with status: "cancelled" for a cancelled one. Northern
Ireland's legs are scheduled. Luas legs get live times from TII's forecasts
and a fare in euros (below); there are no other Irish fares. Dutch legs
have live expected times and cancellations from OVapi, and no fares. Belgian
legs are the same, with live times for SNCB, De Lijn and TEC (Brussels' STIB
legs are scheduled). Times are in local time where the journey starts, named
in the response's timezone, and a datetime without an
offset is read as local time there: 09:00 from Amsterdam means
09:00 in Amsterdam.
- Dates: journeys can be planned from a few days ago up to two months ahead.
- Fares: rail fares cover the whole of Great Britain. Bus
fares are estimated where fare data exists, currently operators in Yorkshire
and West Sussex. Elsewhere
fareisnullor marked incomplete rather than guessed. - Updates: journey planning picks up each day's timetables
every morning with no downtime. If the planner is briefly unavailable,
/v1/journey.jsonreturns503 planner_unavailablewith aRetry-Afterheader; every other endpoint keeps working. A date outside the timetables is not an outage: it returns400 date_out_of_range, which retrying won't change. - Leg paths: bus, tram and metro legs follow the route the
operator publishes, and say so with
leg_geometry_source: "shape". Where no route geometry is published the leg is drawn from stop to stop and says"planner".
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. Timetables run from yesterday to about two months ahead; a date outside that returns 400 date_out_of_range giving the first and last dates that can be planned. |
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. |
railcard | Price rail legs with a railcard: a code from /v1/rail/railcards.json, e.g. YNG. | |
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. Bus operators without imported fare
data always return null.
Luas legs carry fare_local, in euros, because
it isn't added to the pounds in fare: the adult TFI Leap Card 90
minute fare, with the paper single (by Luas zones) and the Leap short fare as
alternatives. A second Luas leg within 90 minutes of the first is
0 with included_in_earlier_leg: true. Prices are
Luas's fares valid from 28 April 2025.
"fare_local": { "currency": "EUR", "amount": 2.0,
"ticket": "TFI Leap Card, TFI 90 Minute Fare, adult",
"alternatives": [ { "ticket": "Paper single, adult (3 zones)", "amount": 2.6 },
{ "ticket": "TFI Leap Card Short Fare, adult (selected journeys up to about 3 km)", "amount": 1.5 } ],
"fares_from": "2025-04-28" }
Rail fares are the cheapest standard class single valid on any train, from
the Rail Delivery Group's fares data. That means an Anytime-type fare valid on
any permitted route, or on a route the journey keeps to (a "not via Doncaster"
fare counts when the train doesn't call there). Off-Peak and Advance fares can
be cheaper, but depend on the time or the train, so they're left to
/v1/rail/fares.json. Consecutive
train legs are one ticket: the fare from the first station to the last goes on
the first of them, and the others have "fare": null with
"fare_included": true.
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-05T16:22:00+01: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.
start_time and end_time on every leg carry the
timetabled time, plus an estimated time and delay_s
when the leg is live, and real_time is then true:
- Bus, tram and coach: from the tracked vehicle's delay.
- Trains within National Rail's two-hour live window:
from Darwin. The leg also gains
status(on_time,late,early,delayed,cancelledorpartly_cancelled),platformat the boarding station, anddelay_reasonorcancel_reason.
"start_time": {
"scheduled": "2026-10-07T17:22:00+01:00",
"estimated": "2026-10-07T17:31:00+01:00",
"delay_s": 540
},
"status": "late", "platform": "5",
"delay_reason": "This service has been delayed by a fault with the signalling system"
A train leg's trip_id is the same id the station boards and
/v1/rail/services/{trip_id}.json use,
so you can follow the train from the journey.
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.
Climb and effort
Every walking leg carries a terrain block: how far it climbs
and descends, its steepest stretch each way, a plain-English shape, and an
estimate of the energy it takes.
"terrain": {
"climb_m": 156,
"descent_m": 10,
"steepest_up_pct": 43.6,
"steepest_down_pct": -5.4,
"shape": "steep climb",
"direction": "up",
"calories_kcal": 168
}
Each itinerary sums its walks into walking, so two routes
that walk the same distance can be told apart when one of them is up a hill:
"walking": {
"climb_m": 180,
"descent_m": 17,
"steepest_up_pct": 43.6,
"shape": "steep climb",
"direction": "up",
"calories_kcal": 208,
"complete": true
}
shape is one of flat, gentle climb,
steady climb, steep climb, gentle descent,
steady descent, long descent, rolling
or hilly, and direction is up,
down, mixed or flat.
How it is measured. Heights come from Ordnance Survey's OS Terrain 50, a bare-earth model of Great Britain on a 50m grid. Each walk is sampled every 25m along its actual path and smoothed over 75m, and a rise counts as climb only once it clears 1.5m, so the model's small errors don't turn a riverside path into a hill. Calories use Minetti's measured cost of walking on a gradient, for a 70kg adult, including resting energy. That model knows a gentle downhill is easier than the flat and a steep one is not.
These are estimates for comparing routes, not measurements. A 50m grid
softens sharp changes, so a short steep ramp or a flight of steps may read
gentler than it is, and summits read a little low. Where any part of a walk
has no height data (ferries, Northern Ireland) or the leg has no path, the leg
has no terrain rather than a partial figure. complete
is false when some of an itinerary's walks couldn't be measured,
so its totals cover only the walks that could. Cycling and transit legs carry
no terrain.
Reachable areas and travel times
Two endpoints answer "where can I get to?" rather than "how do I get there?". Both cover bus, coach, tram, metro, Underground, ferry and National Rail anywhere in Great Britain, for departures from now until the end of tomorrow.
Reachable area: /v1/journey/reachable.json
The area you can reach from a point within each cutoff, as GeoJSON ready to draw on a map, one feature per cutoff, largest first:
curl "https://api.transcapi.com/v1/journey/reachable.json?lat=53.3781&lon=-1.4623&cutoffs=15,30,45&datetime=2026-10-01T08:00:00" \
-H "X-Api-Key: YOUR_API_KEY"
{
"type": "FeatureCollection",
"departure_time": "2026-10-01T08:00:00+01:00",
"features": [
{ "type": "Feature",
"properties": { "minutes": 45, "stops_reached": 3669, "area_km2": 198.6 },
"geometry": { "type": "MultiPolygon", "coordinates": [ ... ] } },
...
]
}
Up to four cutoffs, each 5 to 90 minutes. max_walk (100–2000,
default 800) caps the walk to and from a stop. Expect islands: a railway
reaches a town 30 km away sooner than a bus reaches the suburb between.
Travel times to many places: /v1/journey/travel_times.json
Minutes by public transport from one point to up to 500 destinations in one request, for "homes within 40 minutes of the office" or sorting a list of venues by how easy they are to reach. POST a JSON body:
curl -X POST "https://api.transcapi.com/v1/journey/travel_times.json" \
-H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"from": {"lat": 53.3781, "lon": -1.4623},
"to": [{"id": "meadowhall", "lat": 53.4172, "lon": -1.4125},
{"id": "leeds", "lat": 53.7950, "lon": -1.5474}],
"datetime": "2026-10-01T08:00:00", "max_minutes": 90}'
"results": [
{ "id": "meadowhall", "latitude": 53.4172, "longitude": -1.4125, "minutes": 9.6 },
{ "id": "leeds", "latitude": 53.795, "longitude": -1.5474, "minutes": 66.3 }
]
minutes is null where a destination can't be
reached within max_minutes (at most 90). For up to 100
destinations there's a GET form too:
?from_lat=&from_lon=&to=lat,lon|lat,lon.
How they're worked out
Both scan every scheduled departure in order from the start time: the standard connection-scan method, over the day's full timetable. Times are from the timetable, not live running. Walking, to the first stop, between stops when changing (up to 400 m) and from the last stop, is estimated from straight-line distance with a 30% allowance for streets, at 1.3 m/s, the same walking speed journey planning uses. So an area's edges are good estimates rather than street-exact, and a travel time can differ by a few minutes from the journey planner's, which walks real streets. For the exact route, plan the journey.
Chance of making a connection
Every itinerary with a change carries connections, one per
change, and connection_chance_pct, the chance of making all of
them:
"connection_chance_pct": 25,
"connections": [
{ "at": "West Street/Carver Street",
"from_line": "120", "from_mode": "BUS", "to_line": "52", "to_mode": "BUS",
"spare_s": 120,
"chance_pct": 25, "basis": "trip_history", "days": 20,
"live_margin_s": null }
]
spare_s is the timetabled time between arriving and the next
departure, less any walk between them. chance_pct says how often
that change has actually worked:
trip_history: TransCAPI records when every tracked bus passes each timing point, and a trip is the same journey every day it runs. So this is the share of the last 28 days, out ofdayson which that exact bus was tracked, that you'd have made the change, taking its delay at the change stop (or the last timing point before) against the next vehicle's delay leaving. It needs at least 5 days.route_history: otherwise, from how late that route usually runs at that time of day, assuming the next vehicle leaves on time.null: arriving by train, tram or Underground, or on a bus TransCAPI doesn't track, there's no history to go on.
Where the bus you'd arrive on is running now and tracked,
live_margin_s is the time to spare after its current delay: today's
answer, beside the history's.
Two changes at 80% each make 64% for the journey. A 2-minute change onto a frequent service can look alarming and matter little; a 2-minute change onto the last train of the night is a different thing. Show the number beside the journey and let people judge. History builds from 4 August 2026 in northern England and West Sussex and from 30 September 2026 elsewhere, so figures outside those areas firm up over the coming weeks.
Accessibility: slopes and steps
Every itinerary carries accessibility:
"accessibility": {
"steepest_walk_gradient_pct": 6.4,
"gradient_checked": true
}
steepest_walk_gradient_pct is the steepest slope on any walk in
the journey, uphill or down, measured from OS Terrain 50 over 25 m
stretches. gradient_checked is false if any walk
couldn't be measured.
max_gradient (a percentage, 1 to 50) excludes itineraries with
a steeper walk. 8.3 is the 1-in-12 standard for ramps; 5 is gentle. Like the
walking limits, it's enforced after the search, and the response says how many
journeys it removed:
"gradient_limits": { "max_gradient_pct": 8.3, "excluded_itineraries": 4 }
An empty list with excluded itineraries means journeys exist but all of them involve a steeper walk: in a hilly city a gentle limit can rule out everything, which is worth telling people plainly.
What isn't here yet. The national timetables mark almost
every bus stop, station and journey's wheelchair accessibility as unknown, so
TransCAPI can't yet say which stations are step-free or which vehicles are
accessible. Every UK local bus has had to be wheelchair accessible since 2017.
Wheelchair-accessible routing that avoids steps isn't available yet:
wheelchair=true returns 400 not_available. In the
meantime max_gradient rules out steep walks.
Carbon
Every leg carries carbon, the greenhouse gas it's responsible
for per passenger, and every itinerary a total, with what driving the same
distance alone would emit beside it:
"carbon": {
"co2e_g": 1428,
"driving_alone_co2e_g": 7869,
"complete": true,
"source": "UK government GHG conversion factors 2026 (DESNZ)"
}
A leg's figure is its distance times the UK government's 2026 conversion factor for its mode, in kg CO2e per passenger-km. These are the factors UK companies use for their own travel reporting, so your totals will match theirs. The leg names the factor it used:
| Mode | Factor | g CO2e per passenger-km |
|---|---|---|
| Bus outside London | Local bus (not London) | 125.5 |
| Bus in London | Local London bus | 63.6 |
| Coach | Coach | 39.5 |
| Rail | National rail | 30.9 |
| Tram, light rail, metro | Light rail and tram | 21.2 |
| Underground | London Underground | 15.5 |
| Ferry | Ferry, foot passenger | 18.7 |
| Car (comparison) | Average car, unknown fuel, one occupant | 165.9 per vehicle-km |
Walking and cycling count as zero. These are national averages per
passenger, not the particular vehicle: a busy bus does better than the average
and an empty one worse. The driving comparison uses the journey's own distance,
which can be a little longer or shorter than the road route.
complete is false if a leg's mode has no factor, in
which case the total leaves that leg out.
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. Walking and cycling alternatives are offered
for routes up to 4 hours; beyond that the list is empty (nobody wants a
9-hour walk from Leeds to York).
Each transit search covers departures in the hour from your requested time.
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": "Great Britain",
"bbox": [49.8, -8.7, 61.0, 2.0]
},
"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.