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 currently covers South and West Yorkshire only (approximately 53.30–53.96°N, 2.18°W–0.85°W). The stop, place, bus and rail departure endpoints cover all of Great Britain. A request with an origin or destination outside the area returns 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

NameDefaultDescription
from_lat, from_lonrequiredOrigin coordinate.
to_lat, to_lonrequiredDestination coordinate.
datetimenowISO 8601 departure time, or arrival time when arrive_by=true.
arrive_byfalseTreat datetime as the latest acceptable arrival.
transit_modestram,railOptional transit sub-modes to include. Bus and coach are always searched. Pass an empty value (transit_modes=) for bus and coach only.
modeswalk,bicycleNon-transit alternatives returned alongside the transit result. Pass an empty value to skip them.
include_farestrueAttach fare estimates.
include_livetrueAttach live vehicle positions.
include_intermediate_stopsfalseList every stop called at on each transit leg.
max_walk_distanceMetres. Caps the longest single walking leg.
max_total_walk_distanceMetres. Caps the total walked across the journey.
include_stepsfalseTurn-by-turn directions on each walking leg.
limit5Maximum 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.