# SJ Train Tickets & Connections Scraper (`jindrich.bar/sj-ticket-scraper`) Actor

Scrape live SJ train connections, schedules, ticket prices, transfers, delays, and fares across Sweden and Europe. Extract structured SJ timetable data for travel apps, price monitoring, analytics, and AI agents.

- **URL**: https://apify.com/jindrich.bar/sj-ticket-scraper.md
- **Developed by:** [Jindřich Bär](https://apify.com/jindrich.bar) (community)
- **Categories:** AI, Developer tools, Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.40 / 1,000 trips

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## SJ (Swedish Railways) train tickets & connections scraper

Search SJ train connections and ticket prices across Sweden, and get back a structured dataset of
routes, times, changes, and fares — ready to plug into spreadsheets, databases, dashboards, or AI
agents.

Pulls live data from the official SJ booking API, so you always get the same trains, prices, and
schedules a passenger would see on sj.se or in the SJ app.

### What you can do with it

- **Compare ticket prices** between any two Swedish stations in real time — Stockholm to Göteborg,
  Malmö to Lund, Stockholm to Kiruna, and every other route SJ sells (SJ Snabbtåg, SJ InterCity, SJ
  Regional, SJ Night Train, plus partner traffic such as Öresundståg, Mälartåg, Norrtåg and Snälltåget
  where SJ sells the ticket).
- **Get every ticket type, not just the headline price** — 2 klass, 2 klass Lugn (quiet coach) and
  1 klass are each returned in all three flexibility tiers (`NOFLEX` "kan ej ombokas", `SEMIFLEX`
  "kan ombokas", `FULLFLEX` "kan återbetalas"), so you can see exactly which ticket produced the
  cheapest result.
- **Price night trains properly** — couchettes and sleeper berths (shared, private, first class) come
  back as their own offers, so a Stockholm → Luleå search shows both a seat price and a bed price.
- **Find direct trains** or allow changes, with a minimum transfer time and optional via stations.
- **Search by arrival time** — "get me there by 09:00" — not just by departure.
- **Scrape the timetable without prices** — set `includePrices: false` and the whole day costs one
  request, which is the cheapest way to build a departure board.
- **Monitor fares over time** by scheduling the actor to run daily / hourly and writing the results
  to your own datastore. SJ's cheapest non-rebookable fares move a lot as departure approaches.
- **Build a travel-planning assistant** — feed the JSON output directly into an LLM agent that
  answers "what's the cheapest train from Stockholm to Göteborg next Tuesday morning."
- **See SJ Prio point prices** — every offer also carries the points it can be bought with.

Typical use cases: travel comparison sites, price-monitoring tools, business-trip planners, rail
enthusiasts, journalists working on transport coverage, and AI agents that need a structured
rail-data source for Sweden.

### Input

| Field | Required | Default | Description |
| --- | --- | --- | --- |
| `from` | yes | — | Origin station name or UIC code (e.g. `"Stockholm Central"`, `"740000001"`, or a city like `"Stockholm"`) |
| `to` | yes | — | Destination station name or UIC code |
| `date` | no | today | Travel date (date picker, ISO `YYYY-MM-DD`), Swedish local time |
| `time` | no | whole day | Travel time (`HH:MM`, 24-hour), Swedish local time |
| `timeType` | no | `"DEPART"` | `"DEPART"` treats the time as the earliest departure, `"ARRIVE"` as the latest acceptable arrival |
| `adults` | no | `1` | Adult passengers (26+) |
| `childrenAges` | no | `[]` | One age per child/youth passenger, 0–25 (e.g. `[4, 12]`). Max 5 per booking |
| `studentAges` | no | `[]` | One age per student passenger, 15+ |
| `seniorAges` | no | `[]` | One age per senior passenger, 18+ (SJ's senior discount starts at 65) |
| `onlyDirectJourneys` | no | `false` | Only return journeys with no change of train |
| `maxTransfers` | no | unlimited | Drop journeys with more changes than this |
| `minTransferTime` | no | SJ default | Minimum time at every change: `"TEN_MINUTES"` … `"SIXTY_MINUTES"` |
| `viaStations` | no | `[]` | Station names or UIC codes the journey must pass through |
| `allowedServiceTypes` | no | all | Restrict to `SJ_HIGH`, `SJ_IC`, `SJ_REG`, `SJ_NT`, `X_TRAINOPS`, `X_PTA`, `X_EXPBUS` |
| `excludedServiceTypes` | no | `[]` | Exclude those same service groups — e.g. `["X_EXPBUS"]` to drop bus legs |
| `includePrices` | no | `true` | Fetch ticket prices. Disable for a pure timetable scrape |
| `includeAllOffers` | no | `false` | Store every ticket in `offers[]`, not just the cheapest ones |
| `includeServiceDetails` | no | `false` | List each leg's on-board amenities in `amenities[]` |
| `maxResults` | no | `20` | Maximum number of departures to push to the dataset |

Ages are mandatory for everyone except adults, because SJ prices child, youth, student, and senior
tickets by age — that is why the passenger inputs are age lists rather than plain counts.

Station names are matched against the official SJ station catalogue, which is bundled with the actor
and refreshed on every build — so no lookup request is made at run time. You can pass a full name
(`"Göteborg Central"`), a short name (`"Malmö C"`), a partial name, a name without Swedish
diacritics (`"Goteborg"`, `"Malmo"`), a UIC code (`"740000002"`), or the short station code from a
ticket (`"2"`). A bare city name resolves to that city's main station (`"Uppsala"` →
Uppsala Central), and the five city groups SJ publishes — `Stockholm`, `Göteborg`,
`Arlanda Flygplats`, plus `Berlin` and `Hamburg` — search every station in the city at once.

#### Example input

```json
{
    "from": "Stockholm Central",
    "to": "Göteborg",
    "date": "2026-08-26",
    "time": "08:00",
    "adults": 1,
    "maxResults": 20
}
```

### Output

Each item in the dataset describes one departure, with its legs and the cheapest tickets found for
it.

#### Example item

```json
{
    "id": "64bdb83e-091f-395f-a156-dea1e043f433",
    "from": "Stockholm Central",
    "to": "Göteborg",
    "fromStationCode": "740000001",
    "toStationCode": "740098002",
    "departure": "2026-08-26T08:14:00+02:00",
    "arrival": "2026-08-26T13:30:00+02:00",
    "durationMinutes": 316,
    "durationText": "5h 16m",
    "transfers": 0,
    "operators": ["SJ"],
    "serviceTypes": ["SJREG"],
    "serviceNames": ["165"],
    "producer": "SJ_ONLY",
    "resplus": false,
    "nightTrain": false,
    "status": "AVAILABLE",
    "unavailableReasons": [],
    "passengerCount": 1,
    "priceAmount": 295,
    "priceCurrency": "SEK",
    "cheapestOffer": {
        "offerType": "SEAT",
        "fareClass": "SECOND",
        "comfortType": null,
        "flexibility": "NOFLEX",
        "priceAmount": 295,
        "priceCurrency": "SEK",
        "originalPriceAmount": 295,
        "discounted": false,
        "pointPrice": 5100,
        "productName": "2 klass, Kan ej ombokas",
        "productCode": "JNON_2",
        "offerId": "a5fee194-ba4a-3e44-b8bd-41ed9afc2b7a",
        "available": true
    },
    "cheapestSecondClassOffer": { "fareClass": "SECOND", "flexibility": "NOFLEX", "priceAmount": 295 },
    "cheapestFirstClassOffer": { "fareClass": "FIRST", "flexibility": "NOFLEX", "priceAmount": 475 },
    "cheapestBedOffer": null,
    "legs": [
        {
            "from": "Stockholm Central",
            "to": "Göteborg Central",
            "fromStationCode": "740000001",
            "toStationCode": "740000002",
            "departure": "2026-08-26T08:14:00+02:00",
            "arrival": "2026-08-26T13:30:00+02:00",
            "durationMinutes": 316,
            "changeTimeMinutes": null,
            "serviceName": "165",
            "serviceType": "SJREG",
            "serviceTypeName": "SJ Regional",
            "modality": "REGIONAL_RAIL_SERVICE",
            "operator": "SJ",
            "transportMethod": "TRAIN",
            "vehicle": "X40",
            "nightTrain": false,
            "international": false,
            "seatMapAvailable": false
        }
    ]
}
```

#### Field reference

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | SJ's departure id. Stable — the same train on the same date keeps it across runs, so it is safe to deduplicate on |
| `from` / `to` | string | Station names |
| `fromStationCode` / `toStationCode` | string | UIC station codes (`740000001` = Stockholm Central) |
| `departure` / `arrival` | ISO 8601 with timezone offset | Scheduled times, Swedish local time (`+02:00` in summer, `+01:00` in winter) |
| `durationMinutes` | integer | Total travel time |
| `durationText` | string | Duration formatted for reading (e.g. `"5h 16m"`) |
| `transfers` | integer | Number of changes (0 = direct) |
| `operators` | string\[] | Operators running the legs (`SJ`, `Öresundståg`, `Mälartåg`, …) |
| `serviceTypes` | string\[] | Service codes per leg: `SJHIGH` (Snabbtåg), `SJIC`, `SJREG`, `SJNIGHT`, `XORESUNDSTAG`, `XMALARTAG`, … |
| `serviceNames` | string\[] | Train/bus numbers in travel order |
| `producer` | string | `SJ_ONLY`, `SJ_AND_OTHER`, or `MIX_OF_OTHER` — who runs the legs |
| `resplus` | boolean | True for a Resplus ticket, which combines SJ with regional operators |
| `nightTrain` | boolean | True when any leg is an overnight service |
| `status` | string or `null` | Bookability from the ticket response (`"AVAILABLE"`, `"SOLD_OUT"`). `null` when `includePrices` is off |
| `passengerCount` | integer | How many passengers the prices cover |
| `priceAmount` | number or `null` | Cheapest available ticket in SEK. `null` when SJ sells no ticket for it |
| `priceCurrency` | string or `null` | Always `"SEK"` when a price is present |
| `cheapestOffer` | object or `null` | The ticket behind `priceAmount`, with its class, flexibility tier, product name and code |
| `cheapestSecondClassOffer` / `cheapestFirstClassOffer` | object or `null` | Cheapest ticket per class, so you can compare without re-querying |
| `cheapestBedOffer` | object or `null` | Cheapest couchette or sleeper berth on a night train |
| `offers[]` | array | Every ticket on offer, cheapest first. Only present when `includeAllOffers` is enabled |
| `legs[]` | array | Per-leg breakdown: stations and UIC codes, times, duration, wait before the next leg, train number, service type, operator, vehicle type |

**Prices are totals for the whole passenger list, in SEK** — a search with two adults and a child
returns the price of all three tickets together, not a per-person price. `passengerCount` tells you
how many people that covers. `originalPriceAmount` and `discounted` show when SJ applied a family,
campaign, or last-minute reduction.

`priceAmount` is `null` when SJ doesn't sell a through-ticket for the journey — common where a leg
belongs to a regional operator that only sells its own tickets. The departure is still returned with
full timing and leg detail; only the price is missing.

### Pricing

Pay-per-event — one `search-result` event is charged for each departure pushed to the dataset. That
means a query with `maxResults: 10` charges for at most ten events, regardless of how many API calls
the actor makes under the hood.

### Using the API

Trigger runs from your own code via the Apify API. With your Apify API token, a `POST` request runs
the actor synchronously and returns the dataset items:

```bash
curl -X POST "https://api.apify.com/v2/acts/jindrich.bar~sj-ticket-scraper/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
    -H "Content-Type: application/json" \
    -d '{"from": "Stockholm Central", "to": "Göteborg", "maxResults": 10}'
```

Or run asynchronously and poll for status / dataset items:

```bash
## Start a run
curl -X POST "https://api.apify.com/v2/acts/jindrich.bar~sj-ticket-scraper/runs?token=<APIFY_TOKEN>" \
    -H "Content-Type: application/json" \
    -d '{"from": "Stockholm Central", "to": "Göteborg"}'

## When it's done, read the dataset
curl "https://api.apify.com/v2/datasets/<DATASET_ID>/items?token=<APIFY_TOKEN>"
```

Official client libraries are available for [JavaScript / TypeScript](https://docs.apify.com/sdk/js),
[Python](https://docs.apify.com/sdk/python), and via the [Apify REST API](https://docs.apify.com/api/v2)
directly.

### Scheduling

Run the actor on a cron schedule from the **Schedules** tab in the Apify console — daily, hourly, or
any custom cron expression. Common patterns:

- **Daily price snapshot at 09:00** — track how the cheapest Stockholm → Göteborg fare moves as the
  departure date approaches.
- **Hourly refresh during a peak week** — keep a live price board for a route over midsummer or a
  major event.
- **Weekly market scan** — compare a basket of routes (`Stockholm↔Göteborg`, `Stockholm↔Malmö`,
  `Göteborg↔Malmö`) for week-over-week price changes.

Schedules can fan out into multiple datasets, push to a webhook, or trigger downstream actors when
the run finishes.

### Use with AI Agents (Apify MCP)

This actor is exposed through the
[Apify Model Context Protocol (MCP) server](https://docs.apify.com/platform/integrations/mcp), so
any AI agent that speaks MCP — Claude, ChatGPT custom agents, OpenAI Agents SDK, Cursor, etc. — can
call it directly to fetch live SJ ticket prices and connection options.

Once the Apify MCP server is connected, the agent picks up the actor's input schema automatically.
Typical prompts that work out of the box:

- *"What's the cheapest train from Stockholm to Göteborg next Tuesday morning?"*
- *"Find me a direct train from Malmö C to Stockholm Central on 2026-09-15."*
- *"I need to be in Uppsala by 09:00 from Stockholm — what should I catch?"*
- *"How much is a sleeper berth on the night train from Stockholm to Kiruna?"*
- *"Price a trip for two adults and a 6-year-old from Göteborg to Malmö on Friday."*

The agent fills the input, runs the actor, and reads the structured dataset items back — no
scraping, no HTML parsing, no scheduling logic on the agent side.

#### Why this works well for agents

- **Typed input schema** — every field has a title, type, default, and validation rules, so an agent
  can call the actor without trial-and-error prompting.
- **Typed output** — `departure`, `arrival`, `durationMinutes`, `transfers`, `priceAmount` are all
  numbers / ISO timestamps, ready to be diffed, sorted, or compared directly.
- **Tickets are pre-summarised** — `cheapestOffer`, `cheapestSecondClassOffer`,
  `cheapestFirstClassOffer` and `cheapestBedOffer` mean the agent doesn't have to reason over a
  matrix of comfort classes and flexibility tiers to answer "how much?".
- **Whole-day results in chronological order** — SJ returns every departure for the date in one
  response, so the agent never has to reason about paging.
- **Pay-per-event cost control** — an agent that asks for five connections pays for five.

### Troubleshooting & support

Most issues come from date formatting, station naming, or how far ahead you're searching. Try the
fixes below before opening an issue.

#### Common problems

**No results returned (`reason: "no-results"`)**

The run's status message spells out what was searched and which of your filters could have excluded
everything — read that first. The usual causes:

- The travel date is in the past, or more than 365 days ahead. SJ's sales window is one year, and
  the actor rejects dates outside it before making a request.
- The route genuinely has no service that day, or your `time` is past the last departure. Try an
  earlier time or drop `time` to get the whole day.
- `onlyDirectJourneys` is enabled on a route with no through train (e.g. Uppsala → Malmö, where
  every service changes in Stockholm). Turn it off, or use `maxTransfers` instead.
- SJ doesn't sell the route at all. Purely local traffic — a bus stop or a halt served only by a
  regional authority — is often bookable only through that operator, and SJ returns nothing for it.
- `maxTransfers` dropped everything. This filter runs in the actor after the search, so the status
  message reports exactly how many departures it removed.

**The run failed (`reason: "error"`)**

The actor fails loudly rather than finishing with an empty dataset. The status message and the log
carry the reason — an unknown station name (with suggestions), an unreachable API, or an HTTP error
from SJ, including the field SJ itself complained about. Re-running usually fixes transient network
and 5xx errors.

**The wrong station was picked**

- Station queries are resolved in this order: UIC or short station code, exact name (or short name
  or synonym), then a name that starts with your query, then one that contains it. Swedish
  diacritics are optional — `"Malmo C"` and `"Malmö C"` both work.
- A bare city name prefers that city's main station, which SJ names `"<City> Central"` — so
  `"Luleå"` resolves to Luleå Central, not to Luleå Airport. `"Stockholm"` and `"Göteborg"` are
  exceptions: SJ publishes them as city groups, and they search every station in the city.
- If nothing matches the whole query, trailing words are dropped and the search retried, so
  `"Malmö Centralstation"` becomes Malmö Central. This is logged as a warning so you can see which
  station was actually searched.
- If you need a specific station, pass its UIC code (`740000001` Stockholm Central, `740000002`
  Göteborg Central, `740000003` Malmö Central). Codes are unambiguous and always win.

**`priceAmount` is `null` on some departures**

- SJ doesn't sell a through-ticket for that connection — usually a leg run by a regional operator
  that retails its own tickets. Timing and leg data are still complete.
- Or `includePrices` is `false`, in which case no ticket data is fetched at all and `status` is
  `null` too.

**Some departures came back without prices, and the log says so**

- One ticket request failed (typically because the search's ticket session expired mid-run). Those
  departures are kept with timetable data only rather than being dropped. Re-run to retry them.

**`Invalid date` or `Invalid time` error**

- `date` must be `YYYY-MM-DD` (e.g. `2026-09-15`). `time` must be `HH:MM` in 24-hour format
  (e.g. `08:00`, not `8 AM`). Both are optional.

**`Age cannot be null` / age out of bounds**

- Every non-adult passenger needs an age, because SJ prices those tickets by age. Use
  `childrenAges` (0–25), `studentAges` (15+), or `seniorAges` (18+) — one entry per passenger, not
  a count. A booking takes at most 9 passengers, of whom at most 5 may be children/youths.

**Different prices than sj.se shows**

- The actor returns the cheapest ticket available at the moment it runs. SJ's cheapest
  non-rebookable quotas sell out continuously, so a price can change within minutes.
- Prices cover the whole passenger list. If sj.se shows a lower number, check `passengerCount` — you
  may be comparing a per-person price with a group total.
- Discount cards (SJ Prio, travel passes, corporate contracts, Interrail) are not applied. Season
  tickets and promo codes aren't either.

**No delay or real-time information**

- The booking API is a timetable and sales interface: it publishes scheduled times only. SJ serves
  live running information from a separate traffic-information service, which this actor does not
  query.

#### FAQs

**Can I search by station code instead of name?**
Yes — `from` and `to` accept the 9-digit UIC code (`740000001`) and the short code printed on
tickets (`1`). UIC codes are also returned as `fromStationCode` / `toStationCode`.

**What are city groups?**
SJ publishes five "agglomerations" that cover several stations under one code — `Stockholm`,
`Göteborg`, `Arlanda Flygplats`, `Berlin`, `Hamburg`. Searching from one returns the best journey
from any station in that city, which is usually what you want for intercity travel.

**Can I search for a return journey?**
Not in one run — each run searches one direction. Run the actor twice (outbound and return) and
merge the datasets. SJ prices singles anyway, so two singles is the normal way to price a return.

**Does it cover buses, the metro, and local traffic?**
It covers what SJ sells. Express buses and replacement buses appear as legs (`transportMethod:
"BUS"`, service types `X_EXPBUS` / `X_PTA`) and can be excluded with `excludedServiceTypes`. Local
metro and city-bus travel is not sold by SJ and is not returned.

**How many requests does a run make?**
One search, one timetable request for the whole day, and then one ticket request per departure it
keeps. With `includePrices: false` it is just the first two, which makes whole-day timetable scrapes
very cheap.

**Can I apply an SJ Prio or Interrail discount?**
No — prices are returned undiscounted. Every offer does carry `pointPrice`, the SJ Prio points the
ticket can be bought with.

**Can I get seat availability or book a ticket?**
No — the actor returns timetable and price data only. `seatMapAvailable` tells you whether a seat
map exists for a leg, but booking requires SJ's own checkout flow.

#### Support

Open an issue on the actor's **Issues** tab in the Apify console. Include the full input JSON, the
run ID (visible in the run URL, e.g. `console.apify.com/actors/runs/<RUN_ID>`), and the expected
vs. actual output — that lets the maintainer pull the exact logs and reproduce the issue quickly.

### Need a different data source?

If you're scraping connections across multiple operators, check our companion actors:

- [**National Rail (UK) ticket scraper**](https://apify.com/jindrich.bar/national-rail-ticket-scraper) — British rail timetable and ticket prices.
- [**Deutsche Bahn ticket scraper**](https://apify.com/jindrich.bar/deutsche-bahn-ticket-scraper) — German and European rail timetable and ticket prices.
- [**SNCF train ticket scraper**](https://apify.com/jindrich.bar/sncf-train-ticket-scraper) — French rail timetable and ticket prices.
- [**ÖBB ticket scraper**](https://apify.com/jindrich.bar/oebb-ticket-scraper) — Austrian and Central-European rail timetable and ticket prices.
- [**Trenitalia connection scraper**](https://apify.com/jindrich.bar/trenitalia-connections) — Italian rail timetable and ticket prices.
- [**České dráhy connection scraper**](https://apify.com/jindrich.bar/ceske-drahy-connections) — Czech rail timetable and ticket prices.
- [**TCDD ticket scraper**](https://apify.com/jindrich.bar/tcdd-ticket-scraper) — Turkish State Railways timetable and ticket prices.
- [**Flixbus connection scraper**](https://apify.com/jindrich.bar/flixbus-connections) — long-distance bus routes across Europe and the US.

All these actors emit a comparable schema (`from`, `to`, `departure`, `arrival`, price, leg-level
breakdown), so an aggregator agent can merge their outputs into a single multi-modal travel search.

### Development

```bash
npm install
npm test              # vitest, no network access needed
npm run typecheck
npm run main:dev      # one live search printed to stdout
npm run start:dev     # run the actor locally against storage/key_value_stores/default/INPUT.json
npm run update-stations   # refresh stations.json from the SJ booking config
```

The code is split so that everything except the thin Apify wrapper is testable in isolation:

| File | Role |
| --- | --- |
| `index.ts` | API types and the pure parsing helpers that flatten responses into dataset items |
| `extended.ts` | HTTP layer, station resolution, and the `iterateDepartures` generator |
| `actor-logic.ts` | Input validation, the run loop, and the "why was this empty" reporting |
| `actor.ts` | Apify entry point — wires the SDK to `runActor` |
| `stations.json` | Station catalogue snapshot, regenerated at build time by the Dockerfile |

# Actor input Schema

## `from` (type: `string`):

Origin station name or UIC code (e.g. "Stockholm Central", "740000001"). City names such as "Stockholm" or "Göteborg" search every station in that city.

## `to` (type: `string`):

Destination station name or UIC code (e.g. "Göteborg Central", "Malmö C").

## `date` (type: `string`):

Travel date (YYYY-MM-DD), Swedish local time. Leave empty to use today. SJ sells tickets up to 365 days ahead.

## `time` (type: `string`):

Time in HH:MM (24h) format, Swedish local time. Leave empty to get every departure of the day.

## `timeType` (type: `string`):

Whether the travel time is the earliest departure or the latest acceptable arrival.

## `adults` (type: `integer`):

Number of adult passengers (26+) used for ticket pricing.

## `childrenAges` (type: `array`):

One entry per child or youth passenger (0–25), giving their age. SJ prices these tickets by age, so the age is required — e.g. \[4, 12] books one 4-year-old and one 12-year-old. Max 5 per booking.

## `studentAges` (type: `array`):

One entry per student passenger (15+), giving their age. Student tickets need a valid student ID when travelling.

## `seniorAges` (type: `array`):

One entry per senior passenger (18+), giving their age. SJ applies its senior discount from age 65.

## `onlyDirectJourneys` (type: `boolean`):

If enabled, only journeys without a change of train are returned.

## `maxTransfers` (type: `integer`):

Drop journeys with more than this many changes. Leave empty for no limit. Applied by the actor after the search, so it never suppresses a direct alternative.

## `minTransferTime` (type: `string`):

Require at least this much time at every change. Only these values are accepted by the API.

## `viaStations` (type: `array`):

Station names or UIC codes the journey must pass through (e.g. \["Hallsberg station"]).

## `allowedServiceTypes` (type: `array`):

Restrict the search to these services. SJ\_HIGH = SJ Snabbtåg, SJ\_IC = InterCity, SJ\_REG = SJ Regional, SJ\_NT = night train, X\_TRAINOPS = other train operators, X\_PTA = regional public transport, X\_EXPBUS = express bus. Leave empty for all.

## `excludedServiceTypes` (type: `array`):

Exclude these services from the search — e.g. X\_EXPBUS to keep replacement and express buses out of the results.

## `includePrices` (type: `boolean`):

Fetch ticket prices for every departure. Disable for a pure timetable scrape — that needs a single request for the whole day instead of one per departure.

## `includeAllOffers` (type: `boolean`):

If enabled, every ticket on offer (each comfort class × flexibility tier) is stored in the "offers" field, not just the cheapest ones.

## `includeServiceDetails` (type: `boolean`):

If enabled, each leg lists its on-board amenities (wifi, bistro, quiet coach, accessibility) in "amenities".

## `maxResults` (type: `integer`):

Maximum number of departures to push to the dataset. SJ returns the whole day at once, so a lower limit mainly saves ticket-price requests.

## Actor input object example

```json
{
  "from": "Stockholm Central",
  "to": "Göteborg",
  "timeType": "DEPART",
  "adults": 1,
  "childrenAges": [],
  "studentAges": [],
  "seniorAges": [],
  "onlyDirectJourneys": false,
  "viaStations": [],
  "allowedServiceTypes": [],
  "excludedServiceTypes": [],
  "includePrices": true,
  "includeAllOffers": false,
  "includeServiceDetails": false,
  "maxResults": 20
}
```

# Actor output Schema

## `departures` (type: `string`):

Default dataset items — one entry per departure, with legs, change count, and ticket prices (when requested).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("jindrich.bar/sj-ticket-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("jindrich.bar/sj-ticket-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call jindrich.bar/sj-ticket-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jindrich.bar/sj-ticket-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/zfZ3a1W9xC4Usx6uf/builds/hPKnF0g98gHCAm3nq/openapi.json
