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.com

It 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 /trains more than once every 5 s brings nothing new.
  • Large responses carry an ETag: send If-None-Match and you will get a body-less 304 if 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; null if it does not publish one (FGC only says whether the train is on time: see flags).
  • reported_at / age_s: when the position was taken and how old it was when the response was generated. stale: true if 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), cercanias or otros (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).

ParameterInWhat it is
idpathThe 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.

ParameterInWhat it is
númeropathCommercial number, with or without leading zeros.
nucleoqueryOptional. 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.

ParameterInWhat it is
númeropathCommercial number.
nucleoqueryOptional, 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.

ParameterInWhat it is
códigopathRenfe 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.

ParameterInWhat it is
diasqueryDays 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, eu or gl; 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=8

Dark 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.com

RSS 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.

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.