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"). The API includes over 18,000 locations across Europe.
Download Location Data from Dashboard
We strongly recommend downloading the complete location dataset from the Dashboard rather than using the API's getLocations endpoint for lookups. This approach gives you:
- Faster lookups (no API calls needed)
- Better integration with your own systems
- Offline access to location data
- Two curated datasets to choose from
The Dashboard provides two location datasets:
Complete locations (18,000+): Every train station and stop we support. Use this when you need comprehensive coverage or are building a station picker.
Preferred locations (400+): A curated subset of the most commonly used locations. These are the locations you should always prefer when there's uncertainty. For example:
- Prefer "Paris" over "Paris Gare de Lyon"
- Prefer "London" over "London Paddington"
- Prefer "Berlin" over "Berlin Hauptbahnhof"
The preferred locations list helps ensure users find the right city-level location even if they're not sure which specific station they want. Use this dataset for city-level searches and autocomplete fields.
Download locations from Dashboard
Get both location datasets from the Dashboard. Store them in your system and use them for location lookups instead of making API calls.
Searching Locations via API
If you need to search locations dynamically (for example, in an autocomplete field), you can use the getLocations query. However, for most use cases, downloading the datasets is more efficient.
Example: Search for locations
graphql
query FindLocations {
getLocations(query: "london") {
uid
name
countryCode
coordinates {
type
coordinates
}
}
}query FindLocations {
getLocations(query: "london") {
uid
name
countryCode
coordinates {
type
coordinates
}
}
}When displaying search results, prioritize locations that appear in the preferred locations dataset. 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 getJourneys 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 GetJourneys {
getJourneys(
origin: "Sb0ISveC"
destination: "p6fERure"
date: "2026-03-03"
) {
id
status
itinerary {
... on SegmentCollection {
segments {
departureAt
identifier
}
}
}
}
}query GetJourneys {
getJourneys(
origin: "Sb0ISveC"
destination: "p6fERure"
date: "2026-03-03"
) {
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 GetJourneys {
getJourneys(
origin: "Sb0ISveC"
destination: "p6fERure"
date: "2026-03-03"
via: [{ uid: "ObB7ATsZ", duration: "P2D" }]
) {
status
itinerary {
... on SegmentCollection {
segments {
departureAt
identifier
}
}
}
}
}query GetJourneys {
getJourneys(
origin: "Sb0ISveC"
destination: "p6fERure"
date: "2026-03-03"
via: [{ uid: "ObB7ATsZ", duration: "P2D" }]
) {
status
itinerary {
... on SegmentCollection {
segments {
departureAt
identifier
}
}
}
}
}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 routesSUCCESS: All segments found successfullyERROR: 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 GetJourneys {
getJourneys(
origin: "6C9s-Z7A"
destination: "sJbfD64u"
date: "2026-03-03"
passengers: [{ type: ADULT }]
) {
status
itinerary {
... on SegmentCollection {
segments {
departureAt
identifier
}
availability {
status
priceFrom {
amount
currency
}
}
}
}
}
}query GetJourneys {
getJourneys(
origin: "6C9s-Z7A"
destination: "sJbfD64u"
date: "2026-03-03"
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 GetJourneys {
getJourneys(
origin: "6C9s-Z7A"
destination: "sJbfD64u"
date: "2026-03-03"
) {
status
itinerary {
... on SegmentCollection {
segments {
departureAt
identifier
}
planningPeriod {
startDate
endDate
bitfield
}
}
}
}
}query GetJourneys {
getJourneys(
origin: "6C9s-Z7A"
destination: "sJbfD64u"
date: "2026-03-03"
) {
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>