NS International APInsinternational.nl ↗
Look up international train stations, browse per-day lowest fares, and retrieve priced itineraries sold by NS International via a simple REST API.
What is the NS International API?
The NS International API exposes 3 endpoints covering station lookup, price calendars, and bookable train itineraries on the NS International network. Use search_stations to resolve any partial station name to a 5-letter station_code, then pass that code to get_price_calendar for a 300+ day fare overview or to search_train_offers to retrieve priced departure options with class and flexibility variants in EUR.
curl -X GET 'https://api.parse.bot/scraper/39f1ff74-3ac4-446d-856c-1959243053cd/search_stations?name=Paris' \ -H 'X-API-Key: $PARSE_API_KEY'
Typed, relational, agent-ready
A generated client with real types, enums, and the links between objects — the structure a flat JSON response can't carry. Autocompletes in your editor and reads cleanly to coding agents.
- Fully typed · autocompletes
- Objects link to objects
- Typed errors & pagination
Typed Python client. Set up the SDK in your uv project, then pull this API’s typed client:
uv add parse-sdk uv run parse init uv run parse add --marketplace nsinternational-nl-api
uv run parse add --marketplace pulls a pinned snapshot of this canonical API — it won’t change underneath you. To customize it, subscribe and swap to your own copy.
"""Walkthrough: NS International — find stations, check fares, browse itineraries."""
from parse_apis.nsinternational_nl_api import NsInternational, InputNotFound
client = NsInternational()
# Find the station code for Paris (needed by every other endpoint).
station = client.stations.search(name="Paris", limit=5).first()
if station is None:
raise SystemExit("no station matched")
print(station.name, station.station_code, station.country_code)
# Pull the price calendar for Amsterdam → Paris and pick a cheap bookable day.
calendar = client.price_calendars.get(origin="NLASC", destination=station.station_code)
print(f"{calendar.days_with_price} of {calendar.count} days have a price")
cheap_day = None
if calendar.days is not None:
for day in calendar.days:
if day.lowest_price is not None:
cheap_day = day
break # first priced day is enough
if cheap_day is not None:
print(cheap_day.date, f"€{cheap_day.lowest_price}", cheap_day.price_category)
# Search itineraries for that date, capped to the first few results.
if cheap_day is not None:
try:
for itin in client.itineraries.search(
origin="NLASC",
destination=station.station_code,
travel_date=cheap_day.date,
limit=3,
):
print(
itin.origin.name, "→", itin.destination.name,
itin.duration, f"changes={itin.number_of_changes}",
f"from €{itin.lowest_price}" if itin.lowest_price is not None else "sold out",
)
for offer in itin.offers:
print(f" {offer.name} class {offer.class_level} €{offer.total_price}")
except InputNotFound:
print("route not found for", cheap_day.date)
print("exercised: stations.search / price_calendars.get / itineraries.search")
Searches stations, cities and points of interest by (partial) name and returns matching stations with their station_code, the identifier accepted by get_price_calendar and search_train_offers. One upstream request; results are not paginated (the site returns at most a handful of matches ordered by relevance). Names are returned in Dutch where the site has a Dutch name (e.g. 'Parijs'), with other-language spellings in aliases. type is set only for special places (agglo-station, airport, theme-park, top-destination) and null for ordinary stations. An unknown name yields an empty stations list.
| Param | Type | Description |
|---|---|---|
| namerequired | string | Full or partial station/city name in any spelling the site knows (Dutch or local), minimum 2 characters observed to return results. |
{
"type": "object",
"fields": {
"count": "number of stations returned",
"query": "the name searched for (trimmed)",
"stations": "array of matching stations; each has station_code (5-letter code used by the other endpoints), name, type (nullable category), aliases (array of alternate names), country_code (2-letter, from the station code), longitude, latitude"
},
"sample": {
"data": {
"count": 2,
"query": "Paris",
"stations": [
{
"name": "Parijs",
"type": "agglo-station",
"aliases": [
"Paris"
],
"latitude": 48.861496,
"longitude": 2.33368,
"country_code": "FR",
"station_code": "FRPAR"
},
{
"name": "Paliseul",
"type": null,
"aliases": [],
"latitude": 49.895156,
"longitude": 5.118718,
"country_code": "BE",
"station_code": "BEPLS"
}
]
},
"status": "success"
}
}About the NS International API
Station Search
search_stations accepts a partial or full station name (minimum 2 characters, Dutch or local spelling) and returns an array of matching stations. Each station object includes a station_code (a 5-letter identifier), name, type, and aliases. This code is the required input for both other endpoints, so station lookup is typically the first call in any workflow.
Price Calendar
get_price_calendar takes an origin and destination station_code and returns one row per calendar day from today through roughly 10 months ahead — typically 300 or more rows. Each day carries a lowest_price (EUR, nullable) and a price_category band from the site (LOWEST, AVERAGE, or similar). The response also surfaces days_with_price, letting you quickly count how many dates have available fares without iterating the full array. This endpoint is suited for flexible-date fare searches and price-trend analysis.
Train Offers
search_train_offers queries concrete departures for a travel_date and optional departure_time. Each itinerary in the response includes itinerary_id, bookable_status (BOOKABLE, NOT_BOOKABLE_SOLD_OUT, NOT_BOOKABLE_NO_SERVICE), origin and destination station details, and an array of priced offers broken down by ticket class and flexibility tier. The travelers parameter accepts comma-separated age codes (S_65 for senior, YOUNG for youth, etc., up to 9 travelers), so you can price multi-traveler or mixed-age groups. Pagination is handled via an opaque next_cursor field — pass it back in a subsequent call to retrieve later departures on the same date.
The NS International API is a managed, monitored endpoint for nsinternational.nl — not a raw scraper you maintain. Every endpoint is automatically health-checked on a schedule, and when nsinternational.nl changes and a check fails, the API is automatically queued for repair and re-verified. It is built to keep working as the site underneath it changes.
This isn't an official nsinternational.nl API — it's an independent, maintained REST wrapper over public data. Where the source has no official API (or only a limited one), Parse gives you a stable contract over a source that never promised one, and keeps it current. Need a new endpoint or field? You can revise it yourself in plain English and the agent rebuilds it against the live site in minutes — contributing the change back to the shared API is free.
Will this API break when the source site changes?+
Is this an official API from the source site?+
Can I fix or extend this API myself if I need a new endpoint or field?+
What happens if I call an endpoint that has an issue?+
- Find the cheapest days to travel between two cities using get_price_calendar's lowest_price and price_category fields
- Build a fare-alert tool that monitors price_category changes across the 300-day calendar for a given route
- Display real-time departure boards with bookable_status and itinerary departure/arrival times from search_train_offers
- Price multi-passenger bookings by passing mixed age codes via the travelers parameter in search_train_offers
- Resolve ambiguous city names to exact station_codes using search_stations before querying fares
- Compare first-class vs second-class ticket prices across flexibility tiers using the priced offers in each itinerary
- Identify sold-out or suspended services on a route by filtering itineraries on bookable_status values
| Tier | Price | Credits/month | Rate limit |
|---|---|---|---|
| Free | $0/mo | 200 | 5 req/min |
| Hobby | $30/mo | 1,000 | 20 req/min |
| Developer | $100/mo | 5,000 | 100 req/min |
| Team | $300/mo | 20,000 | 300 req/min |
| Company | $1,000/mo | 100,000 | 500 req/min |
Each endpoint has a fixed posted price per successful call — most fall between 1 and 10 credits — shown on this API's page before you run it. Exceeding the rate limit returns a 429 response. Authenticate with the X-API-Key header.