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.

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
        }
      }
    }
  }
}
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 traveller 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 further away than that many metres.
  • 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
        }
      }
    }
  }
}
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-11-05"
  ) {
    id
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}
query Journeys {
  journeys(
    origin: { uid: "Sb0ISveC" }
    destination: { uid: "p6fERure" }
    date: "2026-11-05"
  ) {
    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: 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-11-05"
    via: [{ uid: "ObB7ATsZ", duration: "P2D" }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}
query Journeys {
  journeys(
    origin: { uid: "Sb0ISveC" }
    destination: { uid: "p6fERure" }
    date: "2026-11-05"
    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 traveller 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.

Partial success is normal

If a journey shows ERROR status, it doesn't mean your request was wrong. It just means some segments couldn't be found, perhaps due to a temporary outage or because that specific route isn't available. Check individual segment statuses to see what succeeded.

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

Availability ​

It’s not always clear when is the best time to book a train journey. If you’re looking for tickets long into the future, tickets may not be available yet and if you’re booking on a short notice tickets might already be sold out.

The SegmentCollection of a Journey exposes availability which aims to eliminate the guesswork and help make train bookings as smooth and predictable as possible.

Passengers required

Querying for availability requires passengers to be provided.

Example: Get availability status and indicative price
graphql
query Journeys {
  journeys(
    origin: { uid: "6C9s-Z7A" }
    destination: { uid: "sJbfD64u" }
    date: "2026-11-05"
    passengers: [{ type: ADULT }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
        availability {
          status
          priceFrom {
            amount
            currency
          }
        }
      }
    }
  }
}
query Journeys {
  journeys(
    origin: { uid: "6C9s-Z7A" }
    destination: { uid: "sJbfD64u" }
    date: "2026-11-05"
    passengers: [{ type: ADULT }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
        availability {
          status
          priceFrom {
            amount
            currency
          }
        }
      }
    }
  }
}

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
graphql
query Journeys {
  journeys(
    origin: { uid: "6C9s-Z7A" }
    destination: { uid: "sJbfD64u" }
    date: "2026-11-05"
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
        planningPeriod {
          startDate
          endDate
          bitfield
        }
      }
    }
  }
}
query Journeys {
  journeys(
    origin: { uid: "6C9s-Z7A" }
    destination: { uid: "sJbfD64u" }
    date: "2026-11-05"
  ) {
    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).

Padding

The bitfield is padded with zeros to the right to ensure that the length is a multiple of 4. So a bitfield for a planning period of 10 days will be padded to 12 characters.

js
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))
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))
python
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))
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))
elixir
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
)
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
<?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);
?>
<?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 is a feature-rich interface that can be used to display a list of journeys between two locations. It includes streamed results, availability and pricing information, all without writing any custom UI code.

html
<aa-journey-list
  publicApiKey="my-api-key"
  origin="6C9s-Z7A"
  destination="sJbfD64u"
  date="2025-06-01"></aa-journey-list>
<aa-journey-list
  publicApiKey="my-api-key"
  origin="6C9s-Z7A"
  destination="sJbfD64u"
  date="2025-06-01"></aa-journey-list>