Travel-hacking-toolkit scandinavia-transit

Search trains, buses, and ferries across Norway (Entur), Sweden (ResRobot), and Denmark (Rejseplanen) for intra-Scandinavia travel planning. Use when planning ground transport between cities within Scandinavia. Triggers on "train", "bus", "ferry", "Vy", "SJ", "DSB", "Oslo to Bergen", "Stockholm to", "Copenhagen to", "Entur", "ResRobot", "Rejseplanen", "how to get between", "ground transport", "rail", or any intra-Scandinavia routing question.

install
source · Clone the upstream repo
git clone https://github.com/borski/travel-hacking-toolkit
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/borski/travel-hacking-toolkit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/scandinavia-transit" ~/.claude/skills/borski-travel-hacking-toolkit-scandinavia-transit && rm -rf "$T"
manifest: skills/scandinavia-transit/SKILL.md
source content

Scandinavia Transit Skill

Search ground transport (trains, buses, ferries) within Norway, Sweden, and Denmark using their national transit APIs.

Sources:

Norway: Entur (Journey Planner v3)

Open GraphQL API. No key needed (but a client name header is required). Covers ALL Norwegian transit: Vy trains, buses, ferries, trams, metro. 60+ operators.

Trip Search (Oslo to Bergen example)

curl -s -X POST "https://api.entur.io/journey-planner/v3/graphql" \
  -H "Content-Type: application/json" \
  -H "ET-Client-Name: $ENTUR_CLIENT_NAME" \
  -d '{"query": "{ trip(from: {place: \"NSR:StopPlace:59872\"}, to: {place: \"NSR:StopPlace:548\"}, numTripPatterns: 5) { tripPatterns { startTime duration legs { mode expectedStartTime expectedEndTime fromPlace { name } toPlace { name } line { publicCode name authority { name } } } } } }"}' | jq '.data.trip.tripPatterns[] | {start: .startTime, duration_min: (.duration / 60), legs: [.legs[] | {mode: .mode, from: .fromPlace.name, to: .toPlace.name, line: .line.publicCode, operator: .line.authority.name, depart: .expectedStartTime, arrive: .expectedEndTime}]}'

Find Stop IDs

Stop IDs use the format

NSR:StopPlace:XXXXX
. Find them via the Geocoder:

curl -s "https://api.entur.io/geocoder/v1/autocomplete?text=Oslo%20S&size=3" \
  -H "ET-Client-Name: $ENTUR_CLIENT_NAME" | jq '[.features[] | {name: .properties.name, id: .properties.id, type: .properties.layer}]'

Key Stop IDs

CityStop IDName
Oslo SNSR:StopPlace:59872Oslo S
BergenNSR:StopPlace:548Bergen stasjon
StavangerNSR:StopPlace:4130Stavanger
TrondheimNSR:StopPlace:41742Trondheim S
BodoNSR:StopPlace:49484Bodo stasjon

Use the Geocoder to find any stop. Works for airports, ferry terminals, bus stops too.

Departure Board

curl -s -X POST "https://api.entur.io/journey-planner/v3/graphql" \
  -H "Content-Type: application/json" \
  -H "ET-Client-Name: $ENTUR_CLIENT_NAME" \
  -d '{"query": "{ stopPlace(id: \"NSR:StopPlace:59872\") { name estimatedCalls(numberOfDepartures: 10) { expectedDepartureTime destinationDisplay { frontText } serviceJourney { journeyPattern { line { publicCode name transportMode } } } } } }"}' | jq '.data.stopPlace | {name: .name, departures: [.estimatedCalls[] | {time: .expectedDepartureTime, destination: .destinationDisplay.frontText, line: .serviceJourney.journeyPattern.line.publicCode, mode: .serviceJourney.journeyPattern.line.transportMode}]}'

Notes

  • GraphQL API. POST only. One endpoint:
    https://api.entur.io/journey-planner/v3/graphql
  • Set
    ET-Client-Name
    header on all requests.
  • No rate limit key, but respectful usage expected.
  • Schema explorer: https://api.entur.io/graphql-explorer/journey-planner-v3
  • Covers some cross-border routes into Sweden via Vy and SJ Nord.

Sweden: ResRobot v2.1

REST API via Trafiklab. Covers ALL Swedish transit: SJ trains, regional buses, ferries, metro, commuter rail.

Authentication

RESROBOT_API_KEY
in
.env
. Use
accessId
query parameter. Free key at trafiklab.se. 30,000 calls/month.

Trip Search (Stockholm to Gothenburg)

curl -s "https://api.resrobot.se/v2.1/trip?originId=740000001&destId=740000002&format=json&accessId=$RESROBOT_API_KEY" | jq '[.Trip[] | {start: .LegList.Leg[0].Origin.time, date: .LegList.Leg[0].Origin.date, duration: .duration, legs: [.LegList.Leg[] | {mode: .type, name: .name, from: .Origin.name, to: .Destination.name, depart: .Origin.time, arrive: .Destination.time}]}] | .[0:5]'

Find Stop IDs

curl -s "https://api.resrobot.se/v2.1/location.name?input=Stockholm&format=json&accessId=$RESROBOT_API_KEY" | jq '[.stopLocationOrCoordLocation[] | .StopLocation | {name: .name, id: .extId}] | .[0:5]'

Key Stop IDs

CityStop IDName
Stockholm C740000001Stockholm Centralstation
Gothenburg C740000002Goteborg Centralstation
Malmo C740000003Malmo Centralstation
Uppsala740000025Uppsala Centralstation
Linkoping740000009Linkoping Centralstation

Trip Parameters

ParamRequiredDescription
originId
YesDeparture stop ID
destId
YesArrival stop ID
date
NoYYYY-MM-DD (default today)
time
NoHH:MM (default now)
format
No
json
or
xml
numTrips
NoNumber of results (default 5)
products
NoBitmask for transport types

Notes

  • REST API. Base:
    https://api.resrobot.se/v2.1/
  • Includes cross-border Oresund trains to Copenhagen.
  • No pricing data. Schedule/route only.

Denmark: Rejseplanen API 2.0

REST API (HAFAS v2.50.4.1). Covers ALL Danish transit: DSB InterCity/InterCityLyn, S-Tog, Metro, regional trains, buses, express buses, night buses, ferries, Flextur. Unique: includes fare/zone pricing data.

Docs: https://labs.rejseplanen.dk (login required). XSD schemas: https://www.rejseplanen.dk/api/xsd

Authentication

REJSEPLANEN_API_KEY
in
.env
. Pass as
accessId
query parameter. Apply at labs.rejseplanen.dk. 50,000 calls/month free (non-commercial).

Common Parameters

ParamDescription
accessId
Required. API key.
format
json
or
xml
. Always use
format=json
.
lang
Default
da
. Use
lang=en
for English.

Product Bitmask

ValueProductDescription
1ICInterCity
2ICLInterCityLyn (express)
4ReRegional train
8OtherECE, Eurostar, RailJet, Togbus, DSB Udland
16S-TogCopenhagen commuter rail
32BusLocal bus
64ExpressBusExpress bus (250S, X-bus)
128NightBusNatbus
256FlexturDemand-responsive transport
1024MetroCopenhagen Metro

Trains only =

products=31
. All transit = omit param.

Key Stop IDs

StopextIdName
København H8600626København H
CPH Lufthavn8600858CPH Lufthavn
Odense St.8600512Odense St.
Aarhus H8600053Aarhus H
Aalborg St.8600020Aalborg St.
Svendborg St.8600551Svendborg St.
Svendborg Færgehavn100200261Svendborg Færgehavn
Ærøskøbing Havn (færge)100200222Ærøskøbing Havn (færge)

Location Endpoints

location.name (Search by Name)

curl -s "https://www.rejseplanen.dk/api/location.name?accessId=$REJSEPLANEN_API_KEY&input=København&format=json&type=S" | jq '[.stopLocationOrCoordLocation[] | .StopLocation | {name: .name, id: .extId, lat: .lat, lon: .lon}] | .[0:10]'
ParamRequiredDescription
input
YesSearch query
type
No
S
(stops),
A
(addresses),
P
(POI), or combine
maxNo
NoMax results (default 10, max 1000)
products
NoProduct bitmask filter
r
,
refCoordLat
,
refCoordLong
NoRadius + reference coordinate for distance ranking

location.nearbystops (Stops Near Coordinate)

curl -s "https://www.rejseplanen.dk/api/location.nearbystops?accessId=$REJSEPLANEN_API_KEY&originCoordLat=55.672793&originCoordLong=12.564590&r=500&format=json" | jq '[.stopLocationOrCoordLocation[] | .StopLocation | {name: .name, id: .extId, dist: .dist}] | .[0:10]'

Requires

originCoordLat
/
originCoordLong
. Optional:
r
(radius, default 1000m),
maxNo
,
type
,
products
.

location.details

Single location details by full HAFAS ID (URL-encoded).

location.data

Locations in a numeric ID range (

llId
to
urId
).

location.search

Filtered location search. Same params as

location.name
plus additional filters.

location.boundingbox

All stops in a geographic box (

llLat
/
llLon
to
urLat
/
urLon
).

addresslookup

Addresses near a coordinate. Reverse geocoding. Requires

originCoordLat
/
originCoordLong
.


Trip Planning Endpoints

trip (Route Planning)

curl -s "https://www.rejseplanen.dk/api/trip?accessId=$REJSEPLANEN_API_KEY&originId=8600626&destId=8600551&date=2026-08-28&time=08:00&format=json&lang=en" | jq '[.TripList.Trip[] | {duration: .duration, legs: [.LegList.Leg[] | {name: .name, type: .type, from: .Origin.name, to: .Destination.name, depTime: .Origin.time, arrTime: .Destination.time, track: .Origin.track}]}] | .[0:3]'
ParamRequiredDescription
originId
Yes*Origin stop extId
destId
Yes*Destination stop extId
originCoordLat/Long
Yes*Origin by coordinate (alt to originId)
destCoordLat/Long
Yes*Destination by coordinate (alt to destId)
date
NoYYYY-MM-DD
time
NoHH:MM
searchForArrival
No
1
= search by arrival time
numF
NoForward results (1-6)
numB
NoBackward results (0-3)
maxChange
NoMax transfers (0-11)
products
NoProduct bitmask
operators
NoComma-separated operator codes
lines
NoLine filter
via
NoVia stop ID
avoid
NoAvoid stop ID
passlist
No
1
= include intermediate stops
tariff
No
1
= include fare/zone pricing
poly
No
1
= include polyline
context
NoScroll token for paging
originWalk/Bike/Car
NoAccess mode distance
destWalk/Bike/Car
NoEgress mode distance
rtMode
No
FULL
,
INFOS
,
OFF
,
REALTIME
,
SERVER_DEFAULT

interval (Interval Trip Search)

All departures in a time window. Same params as

trip
.

recon (Trip Reconstruction)

Reconstruct trip from

ctxRecon
token. Params:
ctx
(required),
poly
,
passlist
,
tariff
.

reachability (All Reachable Stops)

All stops reachable within time/transfer budget. Params:

originId
or coordinates,
duration
(1-1439 min),
maxChange
(0-11),
forward
,
products
.


Departure and Arrival Boards

Default duration: 60 minutes.

departureBoard / arrivalBoard

curl -s "https://www.rejseplanen.dk/api/departureBoard?accessId=$REJSEPLANEN_API_KEY&id=8600626&date=2026-08-28&time=08:00&format=json&lang=en" | jq '[.DepartureBoard.Departure[] | {name: .name, time: .time, direction: .direction, track: .track, rtTime: .rtTime}] | .[0:10]'

Params:

id
(required),
date
,
time
,
duration
(0-1439),
maxJourneys
,
products
,
operators
,
passlist
.

multiDepartureBoard / multiArrivalBoard

Multiple stations in one call. Pass multiple

id
params.

nearbyDepartureBoard / nearbyArrivalBoard

Stations near a coordinate. Params:

originCoordLat/Long
,
r
(default 1000m),
maxStops
(default 30).


Line and Journey Endpoints

lineinfo

Line details on a date. Requires

lineId
,
date
.

linesched

Full schedule for a line on a date. Requires

lineId
,
date
.

linesearch

All lines by operator. Requires

operators
(comma-separated, from
datainfo
).

linematch

Match lines by pattern string.

trainSearch

Find journeys by train name. Returns first/last stop only.

journeyDetail

Full stop list for a journey. Requires

id
(journey ref from trip/board). Optional:
fromId
/
toId
,
poly
,
rtMode
.

journeyMatch

Like

trainSearch
but full details for first match only.

journeypos

Real-time vehicle positions in bounding box. Requires

llLat/llLon/urLat/urLon
.


Tariff / Pricing Endpoints

Denmark uses zone-based pricing via Rejsekort. Unique in Scandinavia.

EndpointPurpose
trip
with
tariff=1
Inline pricing in trip results
spPrice
Season pass price by origin/destination
spPriceForZones
Price per day for zone count in fare set
spDailyZonalPrices
Full zone price matrix, all passenger types
spPriceReconstruction
Price from trip reconstruction context
spRefund
Season pass refund calculation
spCheck
Season pass validation (SELF system)
spZoneCheck
Check zone connectivity and metro zones
stRoutes
Tariff routes between origin/destination
stRoutesAddon
Tariff routes from single origin
stStops
Tariff zones and stops (mode=DSB)
trfStop
Default stop for a zone
convertZones
Tariff details for context
zoneFromCoordinate
Zone lookup by lat/lon

Travel Detail Endpoints

gisroute

Walking/cycling route details from trip result. Requires

ctx
(GIS reference).

himsearch

Real-time disruption messages. Filterable by products, operators, lines, dates, stops.


System Information

datainfo

All operators, products, categories. Use to discover operator codes.

curl -s "https://www.rejseplanen.dk/api/datainfo?accessId=$REJSEPLANEN_API_KEY&format=json" | jq '[.DataInfo.operators.Operator[] | {name: .name, id: .id}] | .[0:20]'

tti

Timetable data pool info: ID, creation date, type (ST/ADR/POI), validity period.


Response Structure

  • extId
    : Numeric stop ID (use for queries).
    id
    is the full HAFAS string.
  • rtTime
    /
    rtDate
    : Real-time actual/predicted times (when different from scheduled
    time
    /
    date
    ).
  • Leg
    type
    :
    JNY
    (journey),
    WALK
    (walking),
    TRSF
    (transfer).
  • JourneyDetailRef
    : Use with
    journeyDetail
    for full stop list.

Cross-Border Routes

RouteCovered ByNotes
Oslo to StockholmEntur + ResRobotSJ trains, ~6 hours
Malmö to CopenhagenResRobot + RejseplanenØresund trains, 35 min
Gothenburg to OsloEntur + ResRobotVy/SJ, ~4 hours
Stockholm to CopenhagenResRobotSJ/DSB, ~5 hours via Malmö
Copenhagen to MalmöRejseplanenØresund trains from Danish side

When to Use

Load this skill when:

  • Planning train/bus/ferry routes between Scandinavian cities
  • Checking schedules and durations for ground transport
  • Finding stop IDs for trip planning
  • Comparing train vs flight for intra-Scandinavia legs
  • Looking up Danish transit fares/zones
  • Checking real-time disruptions (
    himsearch
    )
  • Tracking live vehicle positions (
    journeypos
    )

Do not:

  • Use for booking (search/schedule only)
  • Use for flights (use Seats.aero, SerpAPI, or Duffel)
  • Expect pricing from Entur or ResRobot (only Rejseplanen has pricing)

Search Workflows

1. "How do I get from A to B?"

Step 1: Pick API by country (Norway=Entur, Sweden=ResRobot, Denmark=Rejseplanen, cross-border=both).

Step 2: Find stop IDs:

# Norway
curl -s "https://api.entur.io/geocoder/v1/autocomplete?text=Flåm&size=3" \
  -H "ET-Client-Name: $ENTUR_CLIENT_NAME" | jq '[.features[] | {name: .properties.name, id: .properties.id}]'

# Sweden
curl -s "https://api.resrobot.se/v2.1/location.name?input=Malmö&format=json&accessId=$RESROBOT_API_KEY" | jq '[.stopLocationOrCoordLocation[] | .StopLocation | {name: .name, id: .extId}] | .[0:5]'

# Denmark
curl -s "https://www.rejseplanen.dk/api/location.name?accessId=$REJSEPLANEN_API_KEY&input=Svendborg&format=json&type=S&lang=en" | jq '[.stopLocationOrCoordLocation[] | .StopLocation | {name: .name, id: .extId}] | .[0:5]'

Step 3: Trip search with

date
and
time
. Step 4 (Denmark): Add
tariff=1
for pricing.

2. "What trains leave from X?"

curl -s "https://www.rejseplanen.dk/api/departureBoard?accessId=$REJSEPLANEN_API_KEY&id=8600626&duration=120&products=31&format=json&lang=en" | jq '[.DepartureBoard.Departure[] | {time: .time, name: .name, direction: .direction, track: .track}]'

3. "What's near me?"

curl -s "https://www.rejseplanen.dk/api/location.nearbystops?accessId=$REJSEPLANEN_API_KEY&originCoordLat=55.6736&originCoordLong=12.5681&r=500&format=json&lang=en" | jq '[.stopLocationOrCoordLocation[] | .StopLocation | {name: .name, id: .extId, dist: .dist}]'

Or

nearbyDepartureBoard
to skip stop lookup.

4. "What ferry to Ærø?"

curl -s "https://www.rejseplanen.dk/api/trip?accessId=$REJSEPLANEN_API_KEY&originId=100200261&destId=100200222&date=2026-08-28&time=08:00&format=json&lang=en" | jq '[.TripList.Trip[] | {legs: [.LegList.Leg[] | {name: .name, from: .Origin.name, to: .Destination.name, depTime: .Origin.time, arrTime: .Destination.time}]}] | .[0:5]'

5. "I know the train number."

trainSearch
to find it, then
journeyDetail
with the ref for full stops.

6. "What can I reach in 2 hours?"

curl -s "https://www.rejseplanen.dk/api/reachability?accessId=$REJSEPLANEN_API_KEY&originId=8600626&duration=120&maxChange=2&format=json" | jq '.'

7. "Any disruptions?"

curl -s "https://www.rejseplanen.dk/api/himsearch?accessId=$REJSEPLANEN_API_KEY&format=json&lang=en" | jq '.'

8. Multi-country trips

Search from both ends. Øresund trains appear in both ResRobot and Rejseplanen.

Tips

  • Always
    format=json
    . Always
    lang=en
    for Rejseplanen.
  • Use
    extId
    (numeric) for queries, not the long HAFAS
    id
    string.
  • Check
    rtTime
    /
    rtDate
    for delays.
  • passlist=1
    shows intermediate stops.
  • maxChange=0
    for direct connections only.
  • Page with
    numF
    /
    numB
    or
    context
    scroll tokens.
  • Product bitmask is additive. Omit for all types.