API for developers
The same data as the map, in JSON. Free, with no registration and no key. Use it on your website, in your app or in your research.
In one minute
- Base address:
https://trenes.valfiguer.com/api/v1 - Read-only (GET), responses in JSON or GeoJSON, in UTF-8 and compressed if you ask for gzip.
- Open CORS: you can call it from the browser, from any domain. It uses no cookies and no key.
- Times as a Unix timestamp in seconds (
*_ts) and as ISO 8601 in UTC (*_at). - OpenAPI specification: /api/openapi.json · Interactive console: /api/docs
curl -s --compressed https://trenes.valfiguer.com/api/v1/trains | jq '.features | length'Terms of use
The data comes from Renfe Data, FGC Dades Obertes and Open Data Euskadi (MovEuskadi), all under the CC BY 4.0 licence. You may use it for whatever you like, including commercial purposes, as long as you credit the source. One line is enough:
Data: Renfe Data, FGC and MovEuskadi (CC BY 4.0) via trenes.valfiguer.comIt is not a safety system. Positions arrive seconds or minutes late and trains may be missing. Do not use it for safety decisions and do not present it as official information; the official information is that of each operator and Adif.
Limits and caching
- Up to 30 requests per second per IP, with bursts of 100. Above that it responds
429: wait a second and try again. - Positions are refreshed every 15–20 s at the source. Requesting
/trainsmore than once every 5 s brings nothing new. - Large responses carry an
ETag: sendIf-None-Matchand you will get a body-less304if nothing has changed. - If you are going to move a lot of traffic or need the full history, write to us at info@valfiguer.com.
How to read a train
delay_s: delay in seconds according to the operator;nullif it does not publish one (FGC only says whether the train is on time: seeflags).reported_at/age_s: when the position was taken and how old it was when the response was generated.stale: trueif it has gone more than 3 minutes without updating.flags: corrections or warnings applied to the data, for example a corrected time or a position discarded as impossible.category:ld(high-speed and long distance),md(medium distance),cercaniasorotros(FGC and Metro Bilbao).
Routes
Trains in service
GET/api/v1/trains
Every train with a published position right now, as a GeoJSON FeatureCollection, with the status of each source in feeds.
Cache: 5 s, ETag · Try it
{
"type": "FeatureCollection",
"generated_at": "2026-11-01T08:12:30Z",
"generated_ts": 1793520750,
"attribution": "Fuentes: Renfe Data (data.renfe.com), FGC Dades Obertes … CC BY 4.0. …",
"feeds": [{ "id": "renfe_ld_positions", "ok": true, "stale": false, "data_age_s": 14 }],
"features": [{
"type": "Feature",
"id": "renfe_ld:<id>",
"geometry": { "type": "Point", "coordinates": [-3.6893, 40.4066] },
"properties": {
"source": "renfe_ld", "operator": "Renfe", "category": "ld",
"product": "AVE", "train_number": "00297", "nucleo": null,
"line_color": null, "origin": "Madrid-Puerta de Atocha", "destination": "Barcelona-Sants",
"status_text": "en", "stop_name": "Zaragoza-Delicias", "platform": "4",
"delay_s": 180, "reported_at": "2026-11-01T08:12:16Z", "age_s": 14,
"stale": false, "speed_kmh": 287, "bearing": 64, "flags": []
}
}]
}One train
GET/api/v1/trains/{id}
One train with its recent path (up to 80 positions in track: lon, lat, time) and its itinerary: stops with scheduled and estimated times (timetable plus the current delay).
| Parameter | In | What it is |
|---|---|---|
id | path | The id from /trains, for example renfe_cercanias:1004D27203C4a. |
Cache: 5 s · Try it
{
"id": "renfe_ld:00297",
"properties": { "…": "como en /trains" },
"track": [[-3.6893, 40.4066, 1793520736]],
"itinerary": {
"method": "horario_mas_retraso_actual",
"delay_s": 180,
"stops": [{
"code": "71801", "name": "Barcelona-Sants",
"arrival_ts": 1793530800, "estimated_arrival_ts": 1793530980,
"state": "upcoming"
}]
}
}A train by its number
GET/api/v1/trains/numero/{número}
The same as /trains/{id}, looked up by the commercial number printed on the ticket or shown on the departure board. It responds 404 if that number has no position right now.
| Parameter | In | What it is |
|---|---|---|
número | path | Commercial number, with or without leading zeros. |
nucleo | query | Optional. Tells Cercanías in different cities apart: Madrid, Barcelona, Valencia… |
Cache: 5 s · Try it
{ "id": "renfe_cercanias:1004D27704C5", "properties": { "…": "…" }, "track": [], "itinerary": null }Today's timetable for a train
GET/api/v1/horario/numero/{número}
Today's departures for a train number according to Renfe's GTFS calendar, with its stops and whether it is cancelled. Useful for trains that have not yet departed.
| Parameter | In | What it is |
|---|---|---|
número | path | Commercial number. |
nucleo | query | Optional, as above. |
Cache: 30 s · Try it
{
"method": "horario_gtfs",
"runs": [{
"trip_id": "0029712026-11-01", "day": "2026-11-01", "cancelled": false,
"product": "AVE", "origin": "Madrid-Puerta de Atocha", "destination": "Barcelona-Sants",
"departure_ts": 1793520000, "arrival_ts": 1793530800,
"itinerary": { "stops": [] }
}]
}A station's departure board
GET/api/v1/stations/{código}
Trains in the next two hours at a station: those running, with an estimated time, and those that have not yet departed according to today's timetable (state: scheduled). Includes the station's fact sheet when Adif publishes it.
| Parameter | In | What it is |
|---|---|---|
código | path | Renfe station code (up to 5 digits), the same as in /stations. |
Cache: 10 s · Try it
{
"station": { "code": "18000", "name": "Madrid-Puerta de Atocha", "lat": 40.4066, "lon": -3.6893 },
"trains": [{
"id": null, "trip_id": "…", "state": "scheduled", "category": "cercanias",
"product": "C5", "train_number": "27704", "destination": "Humanes",
"planned_ts": 1793520660, "estimated_ts": null, "delay_s": null,
"cancelled": false, "terminates": false
}]
}Stations
GET/api/v1/stations
All stations with their code and name, as GeoJSON points.
Cache: 1 h, ETag · Try it
{ "type": "FeatureCollection", "features": [{ "geometry": { "type": "Point", "coordinates": [-3.6893, 40.4066] }, "properties": { "code": "18000", "name": "Madrid-Puerta de Atocha" } }] }Lines
GET/api/v1/lines
Path of the Cercanías, FGC and Metro Bilbao lines with their official colour, as GeoJSON lines simplified to about 15 m.
Cache: 1 h, ETag · Try it
{ "type": "FeatureCollection", "features": [{ "properties": { "nucleo": "Madrid", "line": "C5", "name": "Móstoles-El Soto - Humanes", "color": "#FFCB06", "text_color": "#000000" }, "geometry": { "type": "LineString", "coordinates": [] } }] }Punctuality
GET/api/v1/puntualidad
Daily punctuality by line and product: trips, trips with a delay and the sum of delays, based on the last delay published for each train.
| Parameter | In | What it is |
|---|---|---|
dias | query | Days back, from 1 to 31 (7 by default). |
Cache: 5 min · Try it
{ "on_time_threshold_s": 300, "days": [{ "date": "2026-10-31", "partial": false, "groups": [{ "label": "C5", "nucleo": "Madrid", "trips": 412, "on_time": 371, "over_30": 2 }] }] }Alerts
GET/api/v1/alerts
Active Cercanías alerts published by Renfe, with the lines they affect.
Cache: 30 s · Try it
{ "alerts": [{ "id": "…", "text": "…", "start": "2026-11-01T05:00:00Z", "end": null, "lines": [{ "nucleo": "Madrid", "line": "C3" }] }] }Cancellations
GET/api/v1/cancellations
Trains that Renfe lists as cancelled right now.
Cache: 30 s · Try it
{ "count": 1, "trains": [{ "trip_id": "…", "category": "md", "product": "MD", "train_number": "18073", "origin": "…", "destination": "…" }] }Status
GET/api/v1/status
Summary of the service: number of trains, current punctuality and the health of each source.
Cache: 10 s · Try it
{ "trains": { "total": 812, "fresh": 790, "by_category": { "ld": 120, "cercanias": 610 } }, "delay": { "known": 700, "on_time_share": 0.91 }, "cancelled": 11, "feeds": [{ "id": "renfe_ld_positions", "ok": true, "data_age_s": 14 }] }Examples
JavaScript, in the browser or in Node 18+:
const res = await fetch("https://trenes.valfiguer.com/api/v1/trains");
const { features } = await res.json();
const late = features.filter((f) => (f.properties.delay_s ?? 0) > 600);
console.log(`${late.length} trains more than 10 min late`);Python:
import httpx
board = httpx.get("https://trenes.valfiguer.com/api/v1/stations/18000").json()
for t in board["trains"][:5]:
print(t["product"], t["destination"], t["delay_s"])Map with MapLibre or Leaflet: add the /trains response as a GeoJSON source and refresh it every 15 s.
map.addSource("trenes", { type: "geojson", data: "https://trenes.valfiguer.com/api/v1/trains" });
setInterval(() => map.getSource("trenes").setData("https://trenes.valfiguer.com/api/v1/trains"), 15000);Versions
v1 is stable: we may add fields, but we will not remove them or change their meaning without publishing a v2 and keeping v1 for at least six months. Your code should ignore fields it does not recognise.
More on the sources, how often they are polled and the historical record in Data.
Widgets for your website
A departure board for a station or the trains on a line, inside your page. They update themselves and adjust their height to the content. Paste the block where you want it to appear:
<div data-trenes-estacion="18000"></div>
<script src="https://trenes.valfiguer.com/embed.js" async></script>A line: network and line, as they appear on the map, in lower case and with hyphens.
<div data-trenes-linea="madrid/c5"></div>
<div data-trenes-linea="rodalies-de-catalunya/r2-nord"></div>
<div data-trenes-linea="fgc/s1"></div>
<script src="https://trenes.valfiguer.com/embed.js" async></script>data-tema="claro"(light) or"oscuro"(dark): fixes the theme. Without it, it follows the visitor's own. The attribute and its values stay in Spanish.data-filas="6": how many trains to show (station: 1–15, 6 by default; line: 1–20, 8 by default).data-idioma="en": the widget's language (es,en,ca,euorgl; Spanish by default). The board and the home-screen widget also exist in each language:/en/panel/18000.- The station code is Renfe's, the same as in /stations/{código} and in the address of each station page.
- If you add widgets after the page has loaded (a single-page application), call
window.TrenesEmbed.scan().
Without JavaScript, with a fixed-height iframe:
<iframe src="https://trenes.valfiguer.com/embed/estacion/18000?tema=claro&filas=5"
title="Next trains" loading="lazy"
style="width:100%;height:360px;border:0"></iframe>The widget's footer carries the attribution the licences require (Renfe Data, FGC and MovEuskadi, CC BY 4.0) and a link to the full page: do not cover it or remove it. Only the /embed/ routes may be displayed inside other websites.
Full-screen board
For a television or tablet in a lobby, an office or a café: /panel/{código} shows the next departures in large type, with a clock, refreshes every 15 s, fits the text size to the screen and, where the browser allows it, keeps the screen from switching off.
https://trenes.valfiguer.com/panel/18000
https://trenes.valfiguer.com/panel/18000?tema=claro&filas=8Dark by default; ?tema=claro (light) for bright rooms. ?filas= accepts 3 to 20. Each station page has a “Panel mode” link.
JavaScript client
A small module with no dependencies, for the browser or Node 18+. Types in /sdk/trenes.d.ts.
import { createClient } from "https://trenes.valfiguer.com/sdk/trenes.js";
const api = createClient(); // or createClient({ baseUrl, timeoutMs, fetch })
const { features } = await api.trains();
const board = await api.station("18000"); // null if the code does not exist
const runs = await api.schedule("00297");
const tren = await api.train(features[0].id);
const lineas = await api.lines();It also exports each function on its own (trains, train, station, schedule, lines…). A 404 returns null; any other error throws TrenesError with its status. For TypeScript, download the .d.ts next to the module or copy the types you need.
Python 3.10+, with the standard library only:
import json
import urllib.request
BASE = "https://trenes.valfiguer.com/api/v1"
def get(path: str):
req = urllib.request.Request(BASE + path, headers={"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=10) as res:
return json.load(res)
board = get("/stations/18000")
print(board["station"]["name"])
for t in board["trains"][:8]:
delay = "no data" if t["delay_s"] is None else f"{round(t['delay_s'] / 60)} min"
print(t["product"], t["train_number"], "→", t["destination"], delay)
# Data: Renfe Data, FGC and MovEuskadi (CC BY 4.0) via trenes.valfiguer.comRSS feeds
Alerts and cancellations in RSS format, for your news reader or to automate notifications. They can be filtered by network and by line with ?nucleo=madrid&linea=c5; each line page links to its own.
- /api/v1/feeds/avisos.xml: active Cercanías and Rodalies alerts.
- /api/v1/feeds/cancelaciones.xml: trains that Renfe marks as cancelled right now.
They reflect what Renfe publishes in its open data, with the same gaps. CC BY 4.0 licence. Alert texts are in Spanish, as published by Renfe.