Finding Journeys

Locations

Before querying for journeys and fares you will need to get the origin and destination locations for the journey. You can either fetch and store all of them on your side, or search by name.

Example: Get the London location
graphql
query FindLocations {
  getLocations(query: "london") {
    uid
    name
    countryCode
    coordinates {
      type
      coordinates
    }
  }
}
query FindLocations {
  getLocations(query: "london") {
    uid
    name
    countryCode
    coordinates {
      type
      coordinates
    }
  }
}

Getting the full list of locations

The full list of locations is available for download on the Dashboard.

Finding Journeys

To find journeys between two locations, use the getJourneys query (or subscription). The query takes a number of arguments, but the most important ones are origin and destination. These are the locations you want to find journeys between. The date argument is also required, and specifies the date of departure. The getJourneys query will return all possible journeys departing on the given day.

Routing built in

It's always best to search for the complete journey from origin to destination, and not try and split and piece together a journey of your own. You will most likely use excessive lookups to reinvent the wheel and end up with a less than optimal journey.

Example: London to Rome
graphql
query GetJourneys {
  getJourneys(
    origin: "Sb0ISveC"
    destination: "p6fERure"
    date: "2024-12-10"
  ) {
    id
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}
query GetJourneys {
  getJourneys(
    origin: "Sb0ISveC"
    destination: "p6fERure"
    date: "2024-12-10"
  ) {
    id
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}

Searching Via Locations

You can influence the search algorithm to a certain degree by suppling a via argument when searching for journeys. This is useful when you want the journey to include one or more specific locations, optionally spending a given duration of time at the location(s).

Providing no duration means a simple change of trains. Providing duration in the hours or minutes means a longer stopover to change trains. Providing duration in days means an overnight stopover.

Example: London to Rome, spending 2 nights in Zürich
graphql
query GetJourneys {
  getJourneys(
    origin: "Sb0ISveC"
    destination: "p6fERure"
    date: "2024-12-10"
    via: [{ uid: "ObB7ATsZ", duration: "P2D" }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}
query GetJourneys {
  getJourneys(
    origin: "Sb0ISveC"
    destination: "p6fERure"
    date: "2024-12-10"
    via: [{ uid: "ObB7ATsZ", duration: "P2D" }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
      }
    }
  }
}

Journey Status

When routing a journey or looking up availability and prices from our ticketing providers there can be a several reason for why we don't get the expected result. It can be because of a temporary outage, or because the requested journey is not available.

A Journey can be composed of several SegmentCollections, some of which may complete successfully while others may fail. The Journey type has a status field which indicate whether the journey is loading or whether it completed successfully or not.

What's an error?

If a Journey has a ERROR status it does not neccesarily mean that the request has failed or that bad arguments were provided. It just means that one or more of the segments failed. Please see the status field of each SegmentCollection for more information.

Each SegmentCollection has a status field which indicate whether the segment collection is loading or if it was successful or not. This can be used to determine which segments failed and why.

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 expose availability which aim to eliminate the guesswork and help make train bookings an as smooth and predictable experience as possible.

Passengers required

Querying for availability required passengers to be provided.

Example: Get availability status and indicative price
graphql
query GetJourneys {
  getJourneys(
    origin: "6C9s-Z7A"
    destination: "sJbfD64u"
    date: "2024-12-10"
    passengers: [{ type: ADULT }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
        availability {
          status
          priceFrom {
            amount
            currency
          }
        }
      }
    }
  }
}
query GetJourneys {
  getJourneys(
    origin: "6C9s-Z7A"
    destination: "sJbfD64u"
    date: "2024-12-10"
    passengers: [{ type: ADULT }]
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
        availability {
          status
          priceFrom {
            amount
            currency
          }
        }
      }
    }
  }
}

Planning Period

Once you have queried for journeys, you may want to know which days a given journey is available. To avoid having to query for the same journey on multiple days you can parse the planningPeriod field on the SegmentCollection.

Example: Get the planning period for a journey
graphql
query GetJourneys {
  getJourneys(
    origin: "6C9s-Z7A"
    destination: "sJbfD64u"
    date: "2024-12-10"
  ) {
    status
    itinerary {
      ... on SegmentCollection {
        segments {
          departureAt
          identifier
        }
        planningPeriod {
          startDate
          endDate
          bitfield
        }
      }
    }
  }
}
query GetJourneys {
  getJourneys(
    origin: "6C9s-Z7A"
    destination: "sJbfD64u"
    date: "2024-12-10"
  ) {
    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);
?>