Switch to dark mode

Finding Journeys

Before you can book tickets, you need to find available train routes between locations. This section explains how to search for journeys and understand the results.

What is a journey?

A journey represents a complete train trip from one location to another. Unlike simple point-to-point routes, journeys can include:

  • Multiple train segments (changing trains)
  • Stopovers (overnight stays or longer connections)
  • Different routing options (fastest, most comfortable, etc.)

The API finds optimal routes automatically, so you don't need to piece together individual train segments yourself. Just specify origin, destination, and date; the API handles the routing.

Every segment of a journey carries the distance one passenger travels on it and that passenger's share of its emissions, in CO2e. See Distance and CO2e.

Locations

Every journey needs an origin and destination location. Locations can be cities (like "Paris") or specific train stations (like "Paris Gare de Lyon"). Each location has a stable uid, which is what you pass as origin and destination to journeys and the other journey queries.

Syncing all locations

We recommend storing the location data in your own system rather than calling the API for every lookup. This gives you faster lookups, offline access and full control over how locations are searched and presented.

Use the locations query to retrieve every location that can be searched and booked. The result is a cursor-paginated LocationConnection: pages hold 25 locations by default and 100 at most.

Example: Fetch a page of locations

graphql
query Locations($after: String) {
  locations(first: 100, after: $after) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        uid
        name
        countryCode
        isMeta
        coordinates {
          type
          coordinates
        }
      }
    }
  }
}

To retrieve the full set, walk the pages:

  1. Run the query without after to get the first page.
  2. While pageInfo.hasNextPage is true, run it again with after set to the previous page's pageInfo.endCursor.
  3. Upsert each location into your own storage, keyed on uid.
  4. Once the walk has run to completion, remove (or mark inactive) any stored location that wasn't seen in it. Locations can be withdrawn, and this is how you stop offering them.

Repeat the walk regularly

A location is listed from the day a journey can be sold from it, so new locations appear over time, and one that joins mid-walk may be missed until the next walk. A scheduled re-sync (for example daily or weekly) keeps your copy up to date.

Meta locations

Each Location has an isMeta flag. A meta location stands for a place rather than one specific station: "Paris" rather than "Paris Gare de Lyon", "London" rather than "London Paddington".

When you search from or to a meta location, the API routes via whichever station is best for the given route. A traveler going from Paris to Lyon departs from "Paris Gare de Lyon", while one going to London departs from "Paris Nord". Searching on "Paris" finds the right one in both cases, whereas searching on a specific station may rule out the better connection.

In most circumstances, prefer a meta location over a specific station. isMeta is typically a good indicator of a preferred location:

  • Rank locations with isMeta: true first in autocomplete and search results
  • Default to the meta location when there's uncertainty about which station the user means
  • Use a specific station only when the user explicitly asks for it

Read the actual station off the journey

A journey searched from a meta location departs from whichever station its route uses. Don't assume a station; read the departure and arrival off the journey's segments.

Searching locations via API

If you need to search locations dynamically (for example, in an autocomplete field), pass a filter to the locations query. For most use cases, searching your own synced copy of the location data is more efficient.

Which fields the filter carries decides the order the locations come back in:

  • With query, the order is relevance. The text matches a name, part of one, or an alias, optionally with an English country name ("paris france"). Case and diacritics are ignored, so "Koln" finds "Köln", but every word has to match. Blank text counts as no text.
  • With near (a GeoJSON Point, { type: "Point", coordinates: [lng, lat] }) and no query, the order is distance from that point. Given alongside a query, it only breaks ties between equally good matches. Either way, radius leaves out locations farther away than that many meters.
  • With neither, the order is the stable one to walk when retrieving every location (see Syncing All Locations).

countryCode and isMeta only narrow the list; they don't change the order.

Example: Search for locations

graphql
query FindLocations {
  locations(filter: { query: "paris" }, first: 10) {
    edges {
      node {
        uid
        name
        countryCode
        isMeta
        coordinates {
          type
          coordinates
        }
      }
    }
  }
}

A ranked list is ordered when a page is read, so a location added or withdrawn between two pages can shift the ones after it by a place. Use it to find a location, not to retrieve all of them. Cursors are opaque, and a cursor only continues the list it came from: present it with the same filter.

When displaying search results, prioritize meta locations. This helps users find the right city-level location even if they're not familiar with specific station names.

Searching for journeys

To find available train routes, use the journeys query. Provide the origin and destination locations, plus the departure date. The API returns all possible journeys departing on that day.

The API handles complex routing automatically. You don't need to figure out which trains connect or where to change. Just specify where you want to go, and the API finds the best routes.

Prices are shown in EUR by default. Use the optional currency argument to display prices in other currencies.

Let the API handle routing

Always search for the complete journey from origin to destination. Don't try to split routes into segments yourself; the API's routing engine finds optimal connections automatically. Manual routing usually results in worse journeys and unnecessary API calls.

Example: London to Rome

graphql
query Journeys {
  journeys(
    origin: { uid: "Sb0ISveC" }
    destination: { uid: "p6fERure" }
    date: "2026-12-02"
  ) {
    id
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}

Filter by journey type

Journeys come in different types, each optimized for different travel preferences. Use the optional filter to focus on the type that matches your needs:

  • SMART (default): Comfort-optimized journeys that balance travel time with comfort. Long routes may include overnight stopovers in cities, giving you time to rest and explore. Best for most travelers who want a pleasant journey.

  • BLUEPRINT: Pre-defined itineraries created by travel experts. These include planned stopovers at specific locations and departure times. Suited for tour operators or when you want a curated travel experience. See Blueprints for more.

  • NON_STOP: Fastest possible routes without planned overnight stops. These prioritize speed but may involve long travel days or tight connections. Best when time is critical.

Filtering by type helps you get relevant results without fetching journeys your users won't want. If you're not sure, start with SMART (it's the default for good reason).

Adding stopovers

Want to include a specific city in your journey? Use the via argument to route through one or more locations. You can also specify how long to stay at each stopover.

  • No duration: Just a train change at the location
  • Hours/minutes: A longer connection time, useful for comfortable transfers
  • Days: An overnight stopover, where passengers stay in the city before continuing

Useful for multi-city trips where travelers want to spend time in intermediate cities.

Example: London to Rome, spending 2 nights in Zürich

graphql
query Journeys {
  journeys(
    origin: { uid: "Sb0ISveC" }
    destination: { uid: "p6fERure" }
    date: "2026-12-02"
    via: [{ uid: "ObB7ATsZ", duration: "P2D" }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}

Searching for a round trip

There is no round-trip search. Search each direction on its own (one journeys call out, one back with origin and destination swapped) and let the traveler pick a journey from each.

The two directions only come together when you price them, so that operators selling genuine return fares can quote one. Keep both journey IDs and pass them to combinedJourneyOffer.

Understanding journey status

Journey searches can take time, and sometimes parts of a journey may not be available. The API provides status information so you can show users what's happening.

A journey consists of multiple train segments (parts of the route). Each segment is checked independently, so some segments might succeed while others fail. The journey's overall status field tells you:

  • LOADING: The API is still searching for routes
  • SUCCESS: All segments found successfully
  • ERROR: One or more segments failed (but others may have succeeded)

Each SegmentCollection also has its own status, so you can see exactly which parts of the journey are available and which aren't.

An ERROR status doesn't mean your request was wrong. It means some segments couldn't be found, perhaps because of a temporary outage or because that route isn't available. Check the individual segment statuses to see what succeeded.

json
{
  "__typename": "JourneyOffer",
  "status": "ERROR",
  "itinerary": [
    {
      "__typename": "SegmentCollection",
      "status": "SUCCESS",
    },
    {
      "__typename": "SegmentCollection",
      "status": "NO_TIMETABLE"
    }
  ]
}

Planning period

Want to show users which days a journey is available? Instead of searching the same route for every possible date, use the planningPeriod field. It tells you which days in a date range have available journeys.

Useful for building calendar views or date pickers that only show days when travel is possible.

Example: Get available days for a journey

query Journeys {
  journeys(
    origin: { uid: "6C9s-Z7A" }
    destination: { uid: "sJbfD64u" }
    date: "2026-12-02"
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
        planningPeriod {
          startDate
          endDate
          bitfield
        }
      }
    }
  }
}

The PlanningPeriod holds a bitfield which can be used to determine when a given journey is available. The bitfield format is a compact representation of the availability of a SegmentCollection for each day within the given planning period. Each bit in the bitfield corresponds to a specific day, starting from the startDate and ending on the endDate. The bitfield is encoded as a hexadecimal string, where each hexadecimal character represents 4 days (bits). The bitfield is padded with zeros on the right to a multiple of 4 bits, so a 10-day planning period is padded to 12 bits: 3 hexadecimal characters.

function parseBitfield(bitfield, startDate, endDate) {
  const start = new Date(startDate)
  const end = new Date(endDate)
  const totalDays = Math.floor((end - start) / (1000 * 60 * 60 * 24)) + 1

  // Convert the bitfield from hexadecimal to binary
  const bytes = bitfield.split('').flatMap((char) => {
    // Convert the hexadecimal character to an integer, e.g. '4' -> 4
    const int = parseInt(char, 16)

    // Convert the integer to a binary string, e.g. 4 -> '100'
    const str = int.toString(2)

    // Compensate for leading 0's dropped by toString, e.g. '100' -> '0100'
    const padded = str.padStart(4, '0')

    // Split into individual bits
    return padded.split('')
  })

  // Join the bytes to get the binary string and trim padding
  const days = bytes.slice(0, totalDays)

  return days.reduce((acc, bit, index) => {
    const date = new Date(start)
    date.setDate(start.getDate() + index)
    const key = date.toISOString().slice(0, 10)
    acc[key] = bit === '1'
    return acc
  }, {})
}

// Example usage:
const startDate = '2023-12-10'
const endDate = '2025-12-13'
const bitfield =
  'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFF7FFDFFFFFFFFEFFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'

console.log(parseBitfield(bitfield, startDate, endDate))
from datetime import datetime, timedelta
from pprint import pprint

def parse_bitfield(bitfield, planning_period_start, planning_period_end):
    def days_count(start, end):
        return (end - start).days + 1

    days_count = days_count(planning_period_start, planning_period_end)

    # The amount of bits in the whole string, padding included
    bit_length = len(bitfield) * 4

    # The amount of padding added to make it align on a byte boundary
    byte_padding = bit_length - days_count

    # Convert the hex string to an integer
    binary = int(bitfield, 16) >> byte_padding

    result = []
    for i in range(days_count):
        day = planning_period_start + timedelta(days=i)

        shift_with = days_count - i - 1
        shifted = 1 << shift_with
        value = binary & shifted

        result.append((day.strftime("%Y-%m-%d"), value != 0))

    return result

# Example usage
planning_period_start = datetime.strptime("2023-12-10", "%Y-%m-%d")
planning_period_end = datetime.strptime("2025-12-13", "%Y-%m-%d")
bitfield = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFF7FFDFFFFFFFFEFFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

pprint(parse_bitfield(bitfield, planning_period_start, planning_period_end))
import Bitwise

defmodule Bitfield do
  def parse(bitfield, planning_period_start, planning_period_end) do
    days_count = days_count(planning_period_start, planning_period_end)

    # The amount of bits in the whole string, padding included
    bit_length = String.length(bitfield) * 4

    # The amount of padding added to make it align on a byte boundary
    byte_padding = bit_length - days_count

    binary =
      bitfield
      |> String.to_integer(16)
      |> bsr(byte_padding)

    Enum.reduce(0..(days_count - 1), [], fn i, acc ->
      day = Date.add(planning_period_start, i)

      shift_with = days_count - i - 1
      shifted = 1 <<< shift_with
      value = binary &&& shifted

      [{to_string(day), value != 0} | acc]
    end)
    |> Enum.reverse()
  end

  defp days_count(start_date, end_date) do
    Date.diff(end_date, start_date) + 1
  end
end

startDate = "2023-12-10"
endDate = "2025-12-13"

bitfield =
  "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFF7FFDFFFFFFFFEFFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

IO.inspect(
  Bitfield.parse(
    bitfield,
    Date.from_iso8601!(startDate),
    Date.from_iso8601!(endDate)
  ),
  limit: :infinity
)
<?php
function parseBitfield($bitfield, $planningPeriodStart, $planningPeriodEnd)
{
  $start = new DateTime($planningPeriodStart);
  $end = new DateTime($planningPeriodEnd);
  $totalDays = $start->diff($end)->days + 1; // Inclusive end date

  // Convert the bitfield from hexadecimal to a binary string
  $binary = '';
  foreach (str_split($bitfield) as $char) {
    // Convert hex character to integer
    $int = hexdec($char);

    // Convert to binary, pad to 4 bits
    $binary .= str_pad(decbin($int), 4, '0', STR_PAD_LEFT);
  }

  // Trim the binary string to the exact number of days
  $days = substr($binary, 0, $totalDays);

  $result = [];
  for ($i = 0; $i < $totalDays; $i++) {
    $day = clone $start;
    $day->modify('+' . $i . ' days');
    $dateKey = $day->format('Y-m-d');
    $result[$dateKey] = $days[$i] === '1';
  }

  return $result;
}

// Example usage
$planningPeriodStart = '2023-12-10';
$planningPeriodEnd = '2025-12-13';
$bitfield = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFF7FFDFFFFFFFFEFFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';

$result = parseBitfield($bitfield, $planningPeriodStart, $planningPeriodEnd);
print_r($result);
?>

Using the Journey List embed

For a ready-made user interface, consider using the Journey List Embed. The embed displays a list of journeys between two locations. It includes streamed results and pricing information, all without writing any custom UI code.

html
<aa-journey-list
  publicApiKey="your-api-key-here"
  origin="6C9s-Z7A"
  destination="sJbfD64u"
  departureDate="2026-12-02"></aa-journey-list>