{
  "openapi": "3.1.0",
  "info": {
    "title": "RadarDeTrenes API",
    "version": "1.0.0",
    "summary": "Real-time positions, station boards, lines, rolling stock and punctuality statistics for trains in Spain.",
    "description": "RadarDeTrenes (radardetrenes.com) exposes a free, public, read-only JSON API over the live rail data it aggregates from public Renfe and ADIF sources: GPS positions of 200+ trains refreshed about every 20 seconds, official departure/arrival boards, Cercanías lines with geometry, the rolling-stock catalogue and punctuality statistics.\n\nNo API key or account is required. Every endpoint under /api/v1 answers anonymous requests; the optional bearer token issued by POST /agent/auth only personalises the token subject. Responses are JSON (UTF-8) with CORS enabled for any origin.\n\nLimits: 100 requests per second per client IP, advertised through the RateLimit and RateLimit-Policy response headers (draft-ietf-httpapi-ratelimit-headers); a refused request answers 429 with Retry-After. Errors are RFC 9457 problem details (application/problem+json) carrying a stable `code` and a resolution `hint`.\n\nPricing: free, no paid plans, no API key; the only condition is attribution. Versioning: v1 is stable and nothing is scheduled for removal; any incompatible change would be announced on https://radardetrenes.com/docs and signalled with Deprecation and Sunset headers (RFC 9745 / RFC 8594).\n\nData: published for informational purposes with attribution to Renfe and ADIF; positions and boards can lag or be interrupted, and a missing train does not by itself mean a cancellation. RadarDeTrenes is an independent, unofficial project. Human documentation: https://radardetrenes.com/docs — agent summary: https://radardetrenes.com/llms.txt",
    "termsOfService": "https://radardetrenes.com/sobre",
    "contact": {
      "name": "RadarDeTrenes",
      "url": "https://radardetrenes.com/contacto",
      "email": "contacto@radardetrenes.com"
    }
  },
  "externalDocs": {
    "description": "Developer documentation (Spanish), with examples and the error catalogue",
    "url": "https://radardetrenes.com/docs"
  },
  "servers": [
    {
      "url": "https://radardetrenes.com",
      "description": "Production"
    }
  ],
  "tags": [
    { "name": "fleet", "description": "Live train positions and per-train details" },
    { "name": "stations", "description": "Station catalogue, boards, alerts and details" },
    { "name": "lines", "description": "Cercanías commuter lines" },
    { "name": "rolling-stock", "description": "Technical catalogue of train series" },
    { "name": "stats", "description": "Punctuality statistics (require the statistics database)" },
    { "name": "routes", "description": "City-to-city corridors with published reliability pages" },
    { "name": "health", "description": "Liveness and readiness" },
    { "name": "agent-auth", "description": "Optional agent registration and OAuth 2.0 token endpoints (see /auth.md)" },
    { "name": "mcp", "description": "Model Context Protocol server (Streamable HTTP)" }
  ],
  "security": [
    {},
    { "bearerAuth": [] }
  ],
  "paths": {
    "/api/v1/fleet": {
      "get": {
        "tags": ["fleet"],
        "operationId": "listFleet",
        "summary": "List the trains currently running",
        "description": "Returns the latest snapshot of every tracked train with its GPS position, delay, origin/destination and next stop. The snapshot is refreshed about every 20 seconds; `updatedAt` tells when it was taken. Filter by network with `type`.",
        "parameters": [{ "$ref": "#/components/parameters/FleetType" }],
        "responses": {
          "200": {
            "description": "Fleet snapshot",
            "headers": {
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" },
              "ETag": { "$ref": "#/components/headers/ETag" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FleetResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/fleet/stream": {
      "get": {
        "tags": ["fleet"],
        "operationId": "streamFleet",
        "summary": "Stream fleet snapshots over Server-Sent Events",
        "description": "Long-lived `text/event-stream` connection. The server first sends an `event: connected` line with the active filter, then an `event: fleet` whose `data` is a FleetResponse JSON document, and afterwards a new `fleet` event every time the snapshot changes (about every 20 seconds) plus `: keepalive` comments every 15 seconds. Use HTTP/1.1 and disable client buffering.",
        "parameters": [{ "$ref": "#/components/parameters/StreamType" }],
        "responses": {
          "200": {
            "description": "Event stream (each `fleet` event carries a FleetResponse)",
            "content": { "text/event-stream": { "schema": { "type": "string", "description": "SSE frames: `event: connected`, then `event: fleet` with FleetResponse JSON in `data`." } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/api/v1/trains/{id}": {
      "get": {
        "tags": ["fleet"],
        "operationId": "getTrain",
        "summary": "Get a running train by id",
        "description": "Returns the live record of one train from the current fleet snapshot. Ids come from listFleet (`ld-05173` for long distance, commuter ids carry their trip). A train that has finished its journey is no longer here — use getTrainStatus for a stable answer.",
        "parameters": [{ "$ref": "#/components/parameters/TrainID" }],
        "responses": {
          "200": {
            "description": "Live train",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Train" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/trains/{id}/route": {
      "get": {
        "tags": ["fleet"],
        "operationId": "getTrainRoute",
        "summary": "Get the scheduled stops and geometry of a train",
        "description": "Returns the ordered list of stops with scheduled times (HH:MM) and the polyline the train follows. Only available for trains whose route the upstream feed publishes.",
        "parameters": [{ "$ref": "#/components/parameters/TrainID" }],
        "responses": {
          "200": {
            "description": "Route with stops and geometry",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TrainRoute" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/trains/{id}/stops": {
      "get": {
        "tags": ["fleet"],
        "operationId": "getTrainStops",
        "summary": "Get the detailed itinerary of a train",
        "description": "Returns the stop-by-stop itinerary built from the operational boards: planned and real arrival/departure times (epoch milliseconds), delays and platforms per stop. `detail` is null when no itinerary is known for the train.",
        "parameters": [{ "$ref": "#/components/parameters/TrainID" }],
        "responses": {
          "200": {
            "description": "Itinerary detail",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TrainPathDetailResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/BadGateway" }
        }
      }
    },
    "/api/v1/trains/{id}/status": {
      "get": {
        "tags": ["fleet"],
        "operationId": "getTrainStatus",
        "summary": "Tell whether a train is live, finished or unknown",
        "description": "Always answers 200 with a `status` discriminator so shared links keep working after a train arrives: `live` includes the current Train, `finished` includes the last known Train, `lastSeenAt` and whether it `arrived`, `not_found` means no live, recent or historical data.",
        "parameters": [{ "$ref": "#/components/parameters/TrainID" }],
        "responses": {
          "200": {
            "description": "Train status",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TrainStatusResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/stations": {
      "get": {
        "tags": ["stations"],
        "operationId": "listStations",
        "summary": "List stations",
        "description": "Returns the unified station catalogue (long-distance and Cercanías networks merged by code) with coordinates, aliases, lines served and the URL slug used by the station pages at https://radardetrenes.com/estacion/{slug}. Large payload (~600 KB); cache it.",
        "parameters": [{ "$ref": "#/components/parameters/StationType" }],
        "responses": {
          "200": {
            "description": "Station catalogue",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StationsResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/stations/{code}": {
      "get": {
        "tags": ["stations"],
        "operationId": "getStation",
        "summary": "Get a station by code",
        "description": "Returns one station by its numeric Renfe/ADIF code (for example 60000 is Madrid Puerta de Atocha). Codes are listed by listStations.",
        "parameters": [{ "$ref": "#/components/parameters/StationCode" }],
        "responses": {
          "200": {
            "description": "Station",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Station" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/stations/{code}/alerts": {
      "get": {
        "tags": ["stations"],
        "operationId": "getStationAlerts",
        "summary": "Get operational alerts for a station",
        "description": "Returns the operational observations (incidences and information notices) currently published for the station, with the network and lines they apply to. An empty list means no active alert is known.",
        "parameters": [{ "$ref": "#/components/parameters/StationCode" }],
        "responses": {
          "200": {
            "description": "Alerts",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StationObservationsResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/BadGateway" }
        }
      }
    },
    "/api/v1/stations/{code}/detail": {
      "get": {
        "tags": ["stations"],
        "operationId": "getStationDetail",
        "summary": "Get the enriched station sheet",
        "description": "Returns the synthesized station sheet: names and aliases, traffic types, lines, commercial zone, accessibility, coordinates and the services available on site. `detail` is null when the upstream source has no sheet for the code.",
        "parameters": [{ "$ref": "#/components/parameters/StationCode" }],
        "responses": {
          "200": {
            "description": "Station detail",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StationDetailResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/BadGateway" }
        }
      }
    },
    "/api/v1/stations/{code}/board-renfe": {
      "get": {
        "tags": ["stations"],
        "operationId": "getStationBoard",
        "summary": "Get the official departures and arrivals board",
        "description": "Returns the departures and arrivals board published by the official operator source for the station, including planned times (epoch milliseconds), delays, platforms, products and — when published — the train composition. `source` names the feed used and `mode` whether times are estimated; the payload is cacheable for 15 seconds.",
        "parameters": [{ "$ref": "#/components/parameters/StationCode" }],
        "responses": {
          "200": {
            "description": "Board snapshot",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StationBoardSnapshot" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/BadGateway" }
        }
      }
    },
    "/api/v1/lines": {
      "get": {
        "tags": ["lines"],
        "operationId": "listLines",
        "summary": "List Cercanías lines with geometry",
        "description": "Returns every commuter line with its code, colour, network (cluster), the URL slug of its page at https://radardetrenes.com/cercanias/{city}/linea/{slug} and its GeoJSON geometry. Geometries change only when the network changes, so the response is cacheable for an hour and supports ETag revalidation. Large payload (~500 KB).",
        "responses": {
          "200": {
            "description": "Lines",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinesResponse" } } }
          },
          "304": { "description": "Not modified (If-None-Match matched the ETag)" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/rolling-stock": {
      "get": {
        "tags": ["rolling-stock"],
        "operationId": "listRollingStock",
        "summary": "Get the rolling-stock catalogue",
        "description": "Returns the technical catalogue of train series (AVE, Avant, Alvia, Cercanías units…) keyed by series number: model, purpose, maximum speed, capacity, manufacturer, gauge, power, voltage, length, mass, signalling, units built and an image path under https://radardetrenes.com/.",
        "responses": {
          "200": {
            "description": "Catalogue keyed by series",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RollingStockCatalog" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/rolling-stock/{series}": {
      "get": {
        "tags": ["rolling-stock"],
        "operationId": "getRollingStock",
        "summary": "Get one rolling-stock series",
        "description": "Returns the technical sheet of a series, e.g. `100` for the AVE Serie 100 or `449` for the Cercanías Civia-type units listed in listRollingStock.",
        "parameters": [
          {
            "name": "series",
            "in": "path",
            "required": true,
            "description": "Series identifier as listed by listRollingStock (e.g. 100, 102, 103, 130, 449).",
            "schema": { "type": "string", "pattern": "^[A-Za-z0-9-]{1,16}$", "example": "100" }
          }
        ],
        "responses": {
          "200": {
            "description": "Series sheet",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RollingStockInfo" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/stats/station/{code}": {
      "get": {
        "tags": ["stats"],
        "operationId": "getStationStats",
        "summary": "Get punctuality statistics for a station",
        "description": "Aggregates the departures recorded at a station over a time window: average and median delay, on-time and severe-delay percentages, breakdown by traffic type, hourly pattern, delay distribution and best/worst trains. Available only when the statistics database is configured (otherwise the route is absent and answers 404).",
        "parameters": [
          { "$ref": "#/components/parameters/StationCode" },
          { "$ref": "#/components/parameters/StatsWindow" }
        ],
        "responses": {
          "200": {
            "description": "Station statistics",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StationStats" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/stats/train/{trainCode}": {
      "get": {
        "tags": ["stats"],
        "operationId": "getTrainStats",
        "summary": "Get punctuality statistics for a train number",
        "description": "Aggregates the recorded trips of a commercial train number (for example 03097) over a time window: trips, average and median delay, on-time percentage, maximum delay, daily evolution and delay distribution. Available only when the statistics database is configured.",
        "parameters": [
          {
            "name": "trainCode",
            "in": "path",
            "required": true,
            "description": "Commercial train number as printed on boards (digits, occasionally with a short alphabetic prefix).",
            "schema": { "type": "string", "pattern": "^[A-Za-z0-9]{1,16}$", "example": "03097" }
          },
          { "$ref": "#/components/parameters/StatsWindowTrain" }
        ],
        "responses": {
          "200": {
            "description": "Train statistics",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TrainStats" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/api/v1/routes": {
      "get": {
        "tags": ["routes"],
        "operationId": "listPublishedRoutes",
        "summary": "List city-to-city routes with a published reliability page",
        "description": "Returns the corridors (e.g. madrid-barcelona) that have enough recorded trips to publish a reliability page at https://radardetrenes.com/trenes/{slug}, with trips, average delay, on-time percentage and the products operating them. Available only when the statistics database is configured.",
        "responses": {
          "200": {
            "description": "Published routes",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoutesResponse" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": ["health"],
        "operationId": "getHealth",
        "summary": "Liveness probe",
        "description": "Answers 200 while the process is running, with uptime and build metadata.",
        "responses": {
          "200": {
            "description": "Alive",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } }
          }
        }
      }
    },
    "/readyz": {
      "get": {
        "tags": ["health"],
        "operationId": "getReadiness",
        "summary": "Readiness probe",
        "description": "Answers 200 once the station catalogue and the other critical caches have been populated, 503 while the server is still warming up after a start.",
        "responses": {
          "200": {
            "description": "Ready",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Readiness" } } }
          },
          "503": {
            "description": "Warming up",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Readiness" } } }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": ["mcp"],
        "operationId": "mcpJsonRpc",
        "summary": "Model Context Protocol endpoint (Streamable HTTP, JSON-RPC 2.0)",
        "description": "Read-only MCP server (protocol version 2025-06-18, no SSE stream: GET answers 405). Supported methods: `initialize`, `ping`, `tools/list` and `tools/call`. Tools: `buscar_tren` (find a running train by number), `ver_tablero` (link to a station board by name or code) and `ver_flota_actual` (summary of the running fleet). The server card is published at /.well-known/mcp/server-card.json. Notifications (requests without `id`) answer 202 with no body.",
        "security": [{}],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcRequest" } } }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcResponse" } } }
          },
          "202": { "description": "Notification accepted (request without id)" },
          "403": { "description": "Origin not allowed", "content": { "text/plain": { "schema": { "type": "string" } } } },
          "405": { "description": "GET is not supported (no SSE stream)", "content": { "text/plain": { "schema": { "type": "string" } } } }
        }
      }
    },
    "/agent/auth": {
      "post": {
        "tags": ["agent-auth"],
        "operationId": "registerAgent",
        "summary": "Register an agent and receive a bearer token",
        "description": "Self-serve registration described in /auth.md. Anonymous registration (`{\"type\":\"anonymous\"}`) or an identity assertion (`verified_email` or an ID-JAG) returns an access token scoped to the public read-only API (`read:trains read:stations`). The token is optional: every /api/v1 endpoint already answers anonymous requests. Tokens are stateless and short-lived; re-register when one expires.",
        "security": [{}],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentRegistration" } } }
        },
        "responses": {
          "201": {
            "description": "Token issued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenResponse" } } }
          },
          "400": { "$ref": "#/components/responses/OAuthError" },
          "405": { "$ref": "#/components/responses/OAuthError" }
        }
      }
    },
    "/agent/auth/claim": {
      "post": {
        "tags": ["agent-auth"],
        "operationId": "claimAgentRegistration",
        "summary": "Start a claim ceremony (not needed today)",
        "x-success-unavailable": "Registration never issues a claim_token, so there is no claim to start and every call answers 400 invalid_grant.",
        "description": "Registration never returns a claim_token because the API is public and read-only, so every claim attempt refers to a claim that does not exist and answers 400 invalid_grant. Documented for completeness of the agent-auth metadata.",
        "security": [{}],
        "requestBody": {
          "required": false,
          "content": { "application/json": { "schema": { "type": "object", "properties": { "claim_token": { "type": "string" }, "email": { "type": "string", "format": "email" } } } } }
        },
        "responses": {
          "400": { "$ref": "#/components/responses/OAuthError" },
          "405": { "$ref": "#/components/responses/OAuthError" }
        }
      }
    },
    "/agent/auth/revoke": {
      "post": {
        "tags": ["agent-auth"],
        "operationId": "revokeAgentToken",
        "summary": "Revoke an agent token (RFC 7009)",
        "description": "Acknowledges a revocation request for a token issued by registerAgent. Tokens are stateless, so the endpoint answers 200 even for unknown tokens, as RFC 7009 requires; the token stops verifying at its embedded expiry.",
        "security": [{}],
        "requestBody": {
          "required": true,
          "content": { "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/RevocationRequest" } } }
        },
        "responses": {
          "200": { "description": "Revocation acknowledged (empty body)" },
          "400": { "$ref": "#/components/responses/OAuthError" },
          "405": { "$ref": "#/components/responses/OAuthError" }
        }
      }
    },
    "/oauth/authorize": {
      "get": {
        "tags": ["agent-auth"],
        "operationId": "showAuthorizationConsent",
        "summary": "OAuth 2.0 authorization endpoint (consent page)",
        "description": "Renders the consent page for the authorization-code flow with PKCE (S256 required). Loopback and out-of-band (`urn:ietf:wg:oauth:2.0:oob`) redirect URIs are supported in addition to HTTPS redirects. Metadata: /.well-known/oauth-authorization-server.",
        "security": [{}],
        "parameters": [
          { "name": "response_type", "in": "query", "required": true, "schema": { "type": "string", "enum": ["code"] } },
          { "name": "client_id", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string", "format": "uri" } },
          { "name": "code_challenge", "in": "query", "required": true, "schema": { "type": "string", "minLength": 43, "maxLength": 128 } },
          { "name": "code_challenge_method", "in": "query", "required": false, "schema": { "type": "string", "enum": ["S256"] } },
          { "name": "scope", "in": "query", "required": false, "schema": { "type": "string", "example": "read:trains read:stations" } },
          { "name": "state", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Consent page", "content": { "text/html": { "schema": { "type": "string" } } } },
          "400": { "description": "Invalid authorization request (HTML)", "content": { "text/html": { "schema": { "type": "string" } } } }
        }
      },
      "post": {
        "tags": ["agent-auth"],
        "operationId": "submitAuthorizationDecision",
        "summary": "Submit the consent decision",
        "description": "Form post of the consent page. `decision=approve` issues a single-use authorization code and redirects to `redirect_uri` (302) with `code` and `state`; any other decision redirects with `error=access_denied` or renders a cancellation page for out-of-band clients.",
        "security": [{}],
        "requestBody": {
          "required": true,
          "content": { "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/AuthorizationDecision" } } }
        },
        "responses": {
          "302": { "description": "Redirect to redirect_uri with code (or error)" },
          "200": { "description": "Out-of-band continuation or cancellation page", "content": { "text/html": { "schema": { "type": "string" } } } },
          "400": { "description": "Invalid authorization request (HTML)", "content": { "text/html": { "schema": { "type": "string" } } } }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "tags": ["agent-auth"],
        "operationId": "issueToken",
        "summary": "OAuth 2.0 token endpoint",
        "description": "Exchanges an authorization code (with PKCE `code_verifier`) or a JWT bearer assertion for an access token. Client authentication is `none`. Supported grant types: `authorization_code`, `urn:ietf:params:oauth:grant-type:jwt-bearer` and `urn:workos:agent-auth:grant-type:claim` (the latter always answers invalid_grant today).",
        "security": [{}],
        "requestBody": {
          "required": true,
          "content": { "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/TokenRequest" } } }
        },
        "responses": {
          "200": {
            "description": "Token issued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenResponse" } } }
          },
          "400": { "$ref": "#/components/responses/OAuthError" },
          "405": { "$ref": "#/components/responses/OAuthError" }
        }
      }
    },
    "/oauth/revoke": {
      "post": {
        "tags": ["agent-auth"],
        "operationId": "revokeToken",
        "summary": "OAuth 2.0 token revocation (RFC 7009)",
        "description": "Acknowledges revocation of an access token issued by the token endpoint. Answers 200 even for unknown tokens, as RFC 7009 requires.",
        "security": [{}],
        "requestBody": {
          "required": true,
          "content": { "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/RevocationRequest" } } }
        },
        "responses": {
          "200": { "description": "Revocation acknowledged (empty body)" },
          "400": { "$ref": "#/components/responses/OAuthError" },
          "405": { "$ref": "#/components/responses/OAuthError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Optional. Tokens come from POST /agent/auth or POST /oauth/token (see /auth.md). The public API answers without one."
      }
    },
    "headers": {
      "RateLimit": {
        "description": "Remaining quota and seconds until it resets for the per-IP policy (draft-ietf-httpapi-ratelimit-headers). Example: \"per-ip\";r=98;t=1",
        "schema": { "type": "string", "example": "\"per-ip\";r=98;t=1" }
      },
      "RateLimit-Policy": {
        "description": "Quota policy: 100 requests per 1-second window per client IP. Example: \"per-ip\";q=100;w=1",
        "schema": { "type": "string", "example": "\"per-ip\";q=100;w=1" }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying a refused request.",
        "schema": { "type": "integer", "minimum": 1, "example": 1 }
      },
      "ETag": {
        "description": "Validator of the JSON body; send it back in If-None-Match to receive 304 when unchanged.",
        "schema": { "type": "string" }
      }
    },
    "parameters": {
      "FleetType": {
        "name": "type",
        "in": "query",
        "required": false,
        "description": "Network filter: `ld` (or `long_distance`) for long/medium distance, `commuter` for Cercanías, `all` (default) for both.",
        "schema": { "type": "string", "enum": ["ld", "long_distance", "commuter", "all"], "default": "all" }
      },
      "StreamType": {
        "name": "type",
        "in": "query",
        "required": false,
        "description": "Network filter for the stream: `ld`, `commuter` or `all` (default).",
        "schema": { "type": "string", "enum": ["ld", "commuter", "all"], "default": "all" }
      },
      "StationType": {
        "name": "type",
        "in": "query",
        "required": false,
        "description": "Catalogue to return: `ld`/`long_distance`, `commuter`, or `all` (default, merged).",
        "schema": { "type": "string", "enum": ["ld", "long_distance", "commuter", "all"], "default": "all" }
      },
      "StationCode": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "Numeric station code as published by Renfe/ADIF (e.g. 60000 Madrid Puerta de Atocha, 71801 Barcelona Sants, 17000 Madrid Sol). Listed by listStations.",
        "schema": { "type": "string", "pattern": "^[0-9]{1,8}$", "example": "60000" }
      },
      "TrainID": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Train id as returned by listFleet (e.g. `ld-05173`).",
        "schema": { "type": "string", "minLength": 1, "maxLength": 64, "example": "ld-05173" }
      },
      "StatsWindow": {
        "name": "window",
        "in": "query",
        "required": false,
        "description": "Aggregation window (default 24h).",
        "schema": { "type": "string", "enum": ["24h", "7d", "30d"], "default": "24h" }
      },
      "StatsWindowTrain": {
        "name": "window",
        "in": "query",
        "required": false,
        "description": "Aggregation window (default 30d).",
        "schema": { "type": "string", "enum": ["24h", "7d", "30d"], "default": "30d" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid parameter or identifier",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "NotFound": {
        "description": "No resource matches the identifier",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "TooManyRequests": {
        "description": "Per-IP quota exhausted; wait Retry-After seconds",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/Retry-After" },
          "RateLimit": { "$ref": "#/components/headers/RateLimit" },
          "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "InternalError": {
        "description": "Unexpected server error",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "BadGateway": {
        "description": "The upstream rail data source did not answer",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "ServiceUnavailable": {
        "description": "Temporarily unavailable (for the stream: too many active connections)",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "OAuthError": {
        "description": "OAuth 2.0 error (RFC 6749 §5.2)",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OAuthError" } } }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details with two extension members: a stable machine `code` and a resolution `hint`. `error` mirrors `detail` for clients written against the original {\"error\": ...} shape.",
        "required": ["type", "title", "status", "detail", "code", "error"],
        "properties": {
          "type": { "type": "string", "format": "uri", "description": "Anchor of the code in the error catalogue, e.g. https://radardetrenes.com/docs#error-not_found" },
          "title": { "type": "string", "description": "HTTP reason phrase" },
          "status": { "type": "integer", "description": "HTTP status code" },
          "detail": { "type": "string", "description": "Human-readable explanation of this occurrence" },
          "instance": { "type": "string", "description": "Request path that produced the problem" },
          "code": {
            "type": "string",
            "description": "Stable machine-readable code",
            "enum": ["bad_request", "unauthorized", "forbidden", "not_found", "method_not_allowed", "not_acceptable", "rate_limited", "internal_error", "upstream_unavailable", "service_unavailable"]
          },
          "hint": { "type": "string", "description": "How to resolve the problem" },
          "error": { "type": "string", "description": "Legacy message member, same as detail" }
        },
        "example": {
          "type": "https://radardetrenes.com/docs#error-not_found",
          "title": "Not Found",
          "status": 404,
          "detail": "station not found",
          "instance": "/api/v1/stations/99999999",
          "code": "not_found",
          "hint": "Verify the identifier; the endpoints and identifier formats are described in https://radardetrenes.com/openapi.json.",
          "error": "station not found"
        }
      },
      "Train": {
        "type": "object",
        "description": "Live record of a running train.",
        "required": ["id", "trainCode", "type", "latitude", "longitude", "timestamp", "originCode", "destinationCode", "nextStationCode", "delayMinutes", "accessible"],
        "properties": {
          "id": { "type": "string", "description": "Stable id within the snapshot (e.g. ld-05173)", "example": "ld-05173" },
          "tripId": { "type": "string", "description": "GTFS trip id when known" },
          "trainCode": { "type": "string", "description": "Commercial train number", "example": "05173" },
          "type": { "type": "string", "enum": ["long_distance", "commuter"] },
          "latitude": { "type": "number", "format": "double" },
          "longitude": { "type": "number", "format": "double" },
          "timestamp": { "type": "string", "format": "date-time", "description": "Time of the position fix" },
          "originCode": { "type": "string" },
          "destinationCode": { "type": "string" },
          "originName": { "type": "string" },
          "destinationName": { "type": "string" },
          "previousStationCode": { "type": "string" },
          "currentStationCode": { "type": "string" },
          "nextStationCode": { "type": "string" },
          "nextStationName": { "type": "string" },
          "nextStationArrival": { "type": "string", "format": "date-time" },
          "productCode": { "type": "integer" },
          "productName": { "type": "string", "description": "Commercial product (AVE, Alvia, Regional Express, Cercanías…)" },
          "productCategory": { "type": "string", "description": "Category code (e.g. AV, LD, MD)" },
          "productColor": { "type": "string", "description": "Hex colour used on the map" },
          "line": { "type": "string", "description": "Cercanías line code" },
          "corridor": { "type": "string" },
          "delayMinutes": { "type": "integer", "description": "Current delay in minutes (negative = early)" },
          "accessible": { "type": "boolean" },
          "platform": { "type": "string" },
          "nextPlatform": { "type": "string" },
          "progress": { "type": "string", "description": "Progress along the route reported by the source" },
          "rollingStock": { "type": "array", "items": { "type": "string" }, "description": "Unit numbers of the composition" },
          "clusterCode": { "type": "string", "description": "Cercanías network code" }
        }
      },
      "FleetResponse": {
        "type": "object",
        "required": ["trains", "count", "updatedAt"],
        "properties": {
          "trains": { "type": "array", "items": { "$ref": "#/components/schemas/Train" } },
          "count": { "type": "integer" },
          "updatedAt": { "type": "string", "format": "date-time", "description": "When the snapshot was taken" }
        }
      },
      "TrainRoute": {
        "type": "object",
        "required": ["trainId", "stops", "geometry"],
        "properties": {
          "trainId": { "type": "string" },
          "stops": { "type": "array", "items": { "$ref": "#/components/schemas/RouteStop" } },
          "geometry": { "type": "array", "items": { "$ref": "#/components/schemas/RoutePoint" } }
        }
      },
      "RouteStop": {
        "type": "object",
        "required": ["stationCode", "time"],
        "properties": {
          "stationCode": { "type": "string" },
          "stationName": { "type": "string" },
          "time": { "type": "string", "description": "Scheduled time HH:MM", "example": "09:18" },
          "latitude": { "type": "number", "format": "double" },
          "longitude": { "type": "number", "format": "double" }
        }
      },
      "RoutePoint": {
        "type": "object",
        "required": ["latitude", "longitude"],
        "properties": {
          "latitude": { "type": "number", "format": "double" },
          "longitude": { "type": "number", "format": "double" },
          "code": { "type": "string", "description": "Station code when the point is a stop" }
        }
      },
      "TrainStatusResponse": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status": { "type": "string", "enum": ["live", "finished", "not_found"] },
          "train": { "$ref": "#/components/schemas/Train" },
          "lastSeenAt": { "type": "string", "format": "date-time", "description": "Last time the train was seen (finished only)" },
          "arrived": { "type": "boolean", "description": "Conservative estimate that the train reached its destination (finished only)" }
        }
      },
      "TrainPathDetailResponse": {
        "type": "object",
        "required": ["detail"],
        "properties": {
          "detail": { "oneOf": [{ "$ref": "#/components/schemas/TrainPathDetail" }, { "type": "null" }] }
        }
      },
      "TrainPathDetail": {
        "type": "object",
        "required": ["trainCode", "launchingDate", "originCode", "destinationCode", "trafficType", "operator", "product", "stops"],
        "properties": {
          "trainCode": { "type": "string" },
          "launchingDate": { "type": "integer", "format": "int64", "description": "Service date, epoch milliseconds" },
          "originCode": { "type": "string" },
          "destinationCode": { "type": "string" },
          "trafficType": { "type": "string", "description": "e.g. AVLDMD, CERCANIAS" },
          "operator": { "type": "string" },
          "product": { "type": "string" },
          "stops": { "type": "array", "items": { "$ref": "#/components/schemas/TrainPathStop" } }
        }
      },
      "TrainPathStop": {
        "type": "object",
        "required": ["stationCode", "stopType"],
        "properties": {
          "stationCode": { "type": "string" },
          "stationName": { "type": "string" },
          "stopType": { "type": "string", "description": "e.g. COMMERCIAL_STOP" },
          "arrivalTime": { "type": "integer", "format": "int64", "description": "Epoch milliseconds" },
          "arrivalDelay": { "type": "integer", "description": "Minutes" },
          "arrivalPlatform": { "type": "string" },
          "arrivalState": { "type": "string" },
          "departureTime": { "type": "integer", "format": "int64", "description": "Epoch milliseconds" },
          "departureDelay": { "type": "integer", "description": "Minutes" },
          "departurePlatform": { "type": "string" },
          "departureState": { "type": "string" }
        }
      },
      "Station": {
        "type": "object",
        "required": ["code", "name", "latitude", "longitude", "type", "isCommuter", "hasStaffing"],
        "properties": {
          "code": { "type": "integer", "description": "Renfe/ADIF station code", "example": 60000 },
          "name": { "type": "string", "example": "Madrid Pta.Atocha - Almudena Grandes" },
          "slug": { "type": "string", "description": "URL slug of the station page at /estacion/{slug}" },
          "latitude": { "type": "number", "format": "double" },
          "longitude": { "type": "number", "format": "double" },
          "type": { "type": "string", "enum": ["long_distance", "commuter"] },
          "accessible": { "type": "integer", "description": "Accessibility level reported by the source" },
          "address": { "type": "string" },
          "city": { "type": "string" },
          "province": { "type": "string" },
          "isCommuter": { "type": "boolean" },
          "serviceLevel": { "type": "integer" },
          "hasStaffing": { "type": "boolean" },
          "clusterCode": { "type": "integer", "description": "Cercanías network code" },
          "clusterName": { "type": "string", "description": "Cercanías network name (e.g. Madrid)" },
          "color": { "type": "string" },
          "bikeParking": { "type": "string" },
          "accessibility": { "type": "string" },
          "busConnections": { "type": "string" },
          "metroConnections": { "type": "string" },
          "lines": { "type": "array", "items": { "type": "string" }, "description": "Cercanías line codes served" },
          "aliases": { "type": "array", "items": { "type": "string" } },
          "networks": { "type": "array", "items": { "type": "string" } },
          "isShared": { "type": "boolean", "description": "Served by both long-distance and commuter networks" },
          "hasLongDistance": { "type": "boolean" },
          "hasCommuter": { "type": "boolean" }
        }
      },
      "StationsResponse": {
        "type": "object",
        "required": ["stations", "count"],
        "properties": {
          "stations": { "type": "array", "items": { "$ref": "#/components/schemas/Station" } },
          "count": { "type": "integer" }
        }
      },
      "StationObservation": {
        "type": "object",
        "required": ["stationCode", "text", "type", "appliesToLD", "appliesToCercanias"],
        "properties": {
          "stationCode": { "type": "string" },
          "text": { "type": "string" },
          "type": { "type": "string", "enum": ["INCIDENCE", "INFO"] },
          "severity": { "type": "string" },
          "appliesToLD": { "type": "boolean" },
          "appliesToCercanias": { "type": "boolean" },
          "lines": { "type": "array", "items": { "type": "string" } },
          "source": { "type": "string" }
        }
      },
      "StationObservationsResponse": {
        "type": "object",
        "required": ["observations", "count"],
        "properties": {
          "observations": { "type": "array", "items": { "$ref": "#/components/schemas/StationObservation" } },
          "count": { "type": "integer" }
        }
      },
      "StationDetail": {
        "type": "object",
        "required": ["stationCode", "longName", "accessible"],
        "properties": {
          "stationCode": { "type": "string" },
          "longName": { "type": "string" },
          "shortName": { "type": "string" },
          "aliases": { "type": "array", "items": { "type": "string" } },
          "trafficTypes": { "type": "array", "items": { "type": "string" } },
          "lines": { "type": "array", "items": { "type": "string" } },
          "commercialZone": { "type": "string" },
          "commuterNetwork": { "type": "string" },
          "accessible": { "type": "boolean" },
          "latitude": { "type": "number", "format": "double" },
          "longitude": { "type": "number", "format": "double" },
          "services": { "type": "array", "items": { "$ref": "#/components/schemas/NamedDescription" } },
          "activities": { "type": "array", "items": { "$ref": "#/components/schemas/NamedDescription" } }
        }
      },
      "NamedDescription": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": { "type": "string" },
          "description": { "type": "string" }
        }
      },
      "StationDetailResponse": {
        "type": "object",
        "required": ["detail"],
        "properties": {
          "detail": { "oneOf": [{ "$ref": "#/components/schemas/StationDetail" }, { "type": "null" }] }
        }
      },
      "StationBoardSnapshot": {
        "type": "object",
        "required": ["departures", "arrivals", "updatedAt"],
        "properties": {
          "departures": { "type": "array", "items": { "$ref": "#/components/schemas/Departure" } },
          "arrivals": { "type": "array", "items": { "$ref": "#/components/schemas/Departure" } },
          "updatedAt": { "type": "integer", "format": "int64", "description": "Epoch milliseconds" },
          "source": { "type": "string", "description": "Feed that produced the board (e.g. renfe_official)" },
          "mode": { "type": "string", "description": "Whether times are real-time or estimated" }
        }
      },
      "Departure": {
        "type": "object",
        "description": "One row of a station board (departure or arrival).",
        "required": ["trainCode", "launchingDate", "originCode", "destinationCode", "trafficType", "operator", "product", "commercialProduct", "stationCode", "stopType", "plannedTime", "delayMinutes", "timeType", "circulationState", "timestamp"],
        "properties": {
          "trainCode": { "type": "string" },
          "launchingDate": { "type": "integer", "format": "int64", "description": "Service date, epoch milliseconds" },
          "originCode": { "type": "string" },
          "originName": { "type": "string" },
          "destinationCode": { "type": "string" },
          "destinationName": { "type": "string" },
          "trafficType": { "type": "string" },
          "operator": { "type": "string", "description": "Operator code (e.g. RF for Renfe)" },
          "product": { "type": "string" },
          "commercialProduct": { "type": "string" },
          "line": { "type": "string" },
          "lineColor": { "type": "string" },
          "routePreview": { "type": "string", "description": "Intermediate stops preview" },
          "observation": { "type": "string" },
          "stationCode": { "type": "string" },
          "stopType": { "type": "string" },
          "plannedTime": { "type": "integer", "format": "int64", "description": "Epoch milliseconds" },
          "delayMinutes": { "type": "integer" },
          "timeType": { "type": "string", "description": "SCHEDULED, ESTIMATED or REAL" },
          "platform": { "type": "string" },
          "platformReliability": { "type": "string" },
          "circulationState": { "type": "string" },
          "announceState": { "type": "string" },
          "timestamp": { "type": "integer", "format": "int64", "description": "Epoch milliseconds" },
          "composition": { "$ref": "#/components/schemas/TrainComposition" },
          "accessOpeningTime": { "type": "integer", "format": "int64" },
          "isAlightingOnly": { "type": "boolean" },
          "isBoardingOnly": { "type": "boolean" },
          "company": { "type": "string" },
          "nextOnPlatform": { "type": "boolean" },
          "platformPreview": { "type": "string" },
          "platformLocations": { "type": "array", "items": { "type": "string" } },
          "sectors": { "type": "array", "items": { "type": "string" } },
          "trainLength": { "type": "integer", "description": "Metres" }
        }
      },
      "TrainComposition": {
        "type": "object",
        "required": ["coaches"],
        "properties": {
          "coaches": { "type": "array", "items": { "$ref": "#/components/schemas/TrainCoach" } },
          "sectorization": { "type": "string" },
          "totalLength": { "type": "integer" },
          "direction": { "type": "string" }
        }
      },
      "TrainCoach": {
        "type": "object",
        "required": ["id", "type", "accessible", "position"],
        "properties": {
          "id": { "type": "string" },
          "type": { "type": "string" },
          "accessible": { "type": "boolean" },
          "position": { "type": "integer" },
          "length": { "type": "integer" },
          "occupancy": { "type": "string" },
          "features": { "type": "array", "items": { "type": "string" } },
          "sector": { "type": "string" },
          "label": { "type": "string" }
        }
      },
      "GeoJSONGeometry": {
        "type": "object",
        "required": ["type"],
        "properties": {
          "type": { "type": "string", "description": "GeoJSON geometry type (LineString, MultiLineString, GeometryCollection)" },
          "coordinates": { "description": "GeoJSON coordinates array ([lon, lat] pairs)" },
          "geometries": { "type": "array", "items": { "$ref": "#/components/schemas/GeoJSONGeometry" } }
        }
      },
      "Line": {
        "type": "object",
        "required": ["id", "code", "name", "color", "clusterCode", "clusterName", "geometry"],
        "properties": {
          "id": { "type": "string" },
          "code": { "type": "string", "example": "C1" },
          "name": { "type": "string" },
          "color": { "type": "string", "example": "#75B2E0" },
          "clusterCode": { "type": "string", "description": "Cercanías network code" },
          "clusterName": { "type": "string", "example": "Madrid" },
          "slug": { "type": "string", "description": "URL slug of the line page (/cercanias/{city}/linea/{slug})" },
          "geometry": { "$ref": "#/components/schemas/GeoJSONGeometry" }
        }
      },
      "LinesResponse": {
        "type": "object",
        "required": ["lines", "count"],
        "properties": {
          "lines": { "type": "array", "items": { "$ref": "#/components/schemas/Line" } },
          "count": { "type": "integer" }
        }
      },
      "RollingStockInfo": {
        "type": "object",
        "required": ["series", "model", "purpose", "maxSpeed", "seatedCap", "manufacturer", "trackGauge", "totalPower", "voltage", "totalLength", "unladenMass", "signaling", "unitsBuilt"],
        "properties": {
          "series": { "type": "string", "example": "100" },
          "model": { "type": "string", "example": "AVE Serie 100" },
          "purpose": { "type": "string" },
          "maxSpeed": { "type": "string", "example": "300 Km/h" },
          "seatedCap": { "type": "string" },
          "manufacturer": { "type": "string" },
          "trackGauge": { "type": "string" },
          "totalPower": { "type": "string" },
          "voltage": { "type": "string" },
          "totalLength": { "type": "string" },
          "unladenMass": { "type": "string" },
          "signaling": { "type": "string" },
          "unitsBuilt": { "type": "string" },
          "imageUrl": { "type": "string", "description": "Path under https://radardetrenes.com/ (e.g. /trains/s100.jpg)" }
        }
      },
      "RollingStockCatalog": {
        "type": "object",
        "description": "Series sheets keyed by series number.",
        "additionalProperties": { "$ref": "#/components/schemas/RollingStockInfo" }
      },
      "TrafficStats": {
        "type": "object",
        "required": ["count", "avgDelay", "severeCount", "maxDelay"],
        "properties": {
          "count": { "type": "integer" },
          "avgDelay": { "type": "number" },
          "severeCount": { "type": "integer" },
          "maxDelay": { "type": "integer" }
        }
      },
      "DailyDelayStat": {
        "type": "object",
        "required": ["date", "avgDelay", "count"],
        "properties": {
          "date": { "type": "string", "format": "date" },
          "avgDelay": { "type": "number" },
          "count": { "type": "integer" }
        }
      },
      "HourlyDelayStat": {
        "type": "object",
        "required": ["hour", "count", "avgDelay", "delayedCount"],
        "properties": {
          "hour": { "type": "integer", "minimum": 0, "maximum": 23 },
          "count": { "type": "integer" },
          "avgDelay": { "type": "number" },
          "delayedCount": { "type": "integer" }
        }
      },
      "DelayBucket": {
        "type": "object",
        "required": ["label", "min", "max", "count", "percent"],
        "properties": {
          "label": { "type": "string", "example": "on_time" },
          "min": { "type": "integer" },
          "max": { "type": "integer" },
          "count": { "type": "integer" },
          "percent": { "type": "number" }
        }
      },
      "TrainRanking": {
        "type": "object",
        "required": ["trainCode", "trips", "avgDelay", "onTimePercent"],
        "properties": {
          "trainCode": { "type": "string" },
          "trips": { "type": "integer" },
          "avgDelay": { "type": "number" },
          "onTimePercent": { "type": "number" }
        }
      },
      "StationStats": {
        "type": "object",
        "required": ["stationCode", "window", "totalTrains", "avgDelay", "medianDelay", "onTimePercent", "severeDelayPercent", "maxDelay", "minDelay", "byTrafficType"],
        "properties": {
          "stationCode": { "type": "string" },
          "window": { "type": "string", "enum": ["24h", "7d", "30d"] },
          "totalTrains": { "type": "integer" },
          "avgDelay": { "type": "number", "description": "Minutes" },
          "medianDelay": { "type": "number", "description": "Minutes" },
          "onTimePercent": { "type": "number" },
          "severeDelayPercent": { "type": "number" },
          "maxDelay": { "type": "integer" },
          "minDelay": { "type": "integer" },
          "byTrafficType": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/TrafficStats" } },
          "dailyEvolution": { "type": "array", "items": { "$ref": "#/components/schemas/DailyDelayStat" } },
          "hourlyPattern": { "type": "array", "items": { "$ref": "#/components/schemas/HourlyDelayStat" } },
          "delayDistribution": { "type": "array", "items": { "$ref": "#/components/schemas/DelayBucket" } },
          "bestTrains": { "type": "array", "items": { "$ref": "#/components/schemas/TrainRanking" } },
          "worstTrains": { "type": "array", "items": { "$ref": "#/components/schemas/TrainRanking" } },
          "trendDelta": { "type": "number", "description": "Change of the average delay versus the previous window, in minutes" }
        }
      },
      "TrainStats": {
        "type": "object",
        "required": ["trainCode", "window", "totalTrips", "avgDelay", "medianDelay", "onTimePercent", "maxDelay"],
        "properties": {
          "trainCode": { "type": "string" },
          "window": { "type": "string", "enum": ["24h", "7d", "30d"] },
          "totalTrips": { "type": "integer" },
          "avgDelay": { "type": "number" },
          "medianDelay": { "type": "number" },
          "onTimePercent": { "type": "number" },
          "maxDelay": { "type": "integer" },
          "dailyEvolution": { "type": "array", "items": { "$ref": "#/components/schemas/DailyDelayStat" } },
          "delayDistribution": { "type": "array", "items": { "$ref": "#/components/schemas/DelayBucket" } }
        }
      },
      "RouteSummary": {
        "type": "object",
        "required": ["slug", "trips", "avgDelay", "onTimePercent", "products"],
        "properties": {
          "slug": { "type": "string", "example": "madrid-barcelona", "description": "Page at https://radardetrenes.com/trenes/{slug}" },
          "trips": { "type": "integer" },
          "avgDelay": { "type": "number" },
          "onTimePercent": { "type": "number" },
          "products": { "type": "array", "items": { "type": "string" } }
        }
      },
      "RoutesResponse": {
        "type": "object",
        "required": ["routes", "count"],
        "properties": {
          "routes": { "type": "array", "items": { "$ref": "#/components/schemas/RouteSummary" } },
          "count": { "type": "integer" }
        }
      },
      "BuildInfo": {
        "type": "object",
        "required": ["version", "commit", "buildTime"],
        "properties": {
          "version": { "type": "string" },
          "commit": { "type": "string" },
          "buildTime": { "type": "string" }
        }
      },
      "Health": {
        "type": "object",
        "required": ["status", "uptime", "build"],
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "uptime": { "type": "string", "description": "Go duration string" },
          "build": { "$ref": "#/components/schemas/BuildInfo" }
        }
      },
      "Readiness": {
        "type": "object",
        "required": ["status", "build"],
        "properties": {
          "status": { "type": "string", "enum": ["ready", "warming_up"] },
          "build": { "$ref": "#/components/schemas/BuildInfo" },
          "checks": { "type": "object", "additionalProperties": { "type": "boolean" } }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "required": ["jsonrpc", "method"],
        "properties": {
          "jsonrpc": { "type": "string", "enum": ["2.0"] },
          "id": { "description": "Request id (string or integer). Omit it to send a notification." },
          "method": { "type": "string", "enum": ["initialize", "ping", "tools/list", "tools/call"] },
          "params": { "type": "object", "description": "Method parameters; for tools/call: {\"name\": \"buscar_tren\", \"arguments\": {\"numero_tren\": \"03097\"}}" }
        },
        "example": { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "buscar_tren", "arguments": { "numero_tren": "03097" } } }
      },
      "JsonRpcResponse": {
        "type": "object",
        "required": ["jsonrpc"],
        "properties": {
          "jsonrpc": { "type": "string", "enum": ["2.0"] },
          "id": { "description": "Echo of the request id" },
          "result": { "type": "object" },
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "integer", "description": "JSON-RPC error code (-32600 invalid request, -32601 method not found, -32602 invalid params)" },
              "message": { "type": "string" }
            }
          }
        }
      },
      "AgentRegistration": {
        "type": "object",
        "required": ["type"],
        "properties": {
          "type": { "type": "string", "enum": ["anonymous", "identity_assertion"] },
          "assertion_type": { "type": "string", "enum": ["verified_email", "urn:ietf:params:oauth:token-type:id-jag"], "description": "Required when type is identity_assertion" },
          "assertion": { "type": "string", "description": "The verified email address or the compact-serialized ID-JAG" }
        },
        "example": { "type": "anonymous" }
      },
      "TokenResponse": {
        "type": "object",
        "required": ["access_token", "token_type", "expires_in", "scope"],
        "properties": {
          "access_token": { "type": "string" },
          "token_type": { "type": "string", "enum": ["Bearer"] },
          "expires_in": { "type": "integer", "description": "Seconds until expiry" },
          "scope": { "type": "string", "example": "read:trains read:stations" }
        }
      },
      "TokenRequest": {
        "type": "object",
        "required": ["grant_type"],
        "properties": {
          "grant_type": { "type": "string", "enum": ["authorization_code", "urn:ietf:params:oauth:grant-type:jwt-bearer", "urn:workos:agent-auth:grant-type:claim"] },
          "code": { "type": "string", "description": "Authorization code (authorization_code grant)" },
          "code_verifier": { "type": "string", "description": "PKCE verifier (authorization_code grant)" },
          "redirect_uri": { "type": "string", "format": "uri", "description": "Must match the authorization request" },
          "assertion": { "type": "string", "description": "Compact-serialized JWT (jwt-bearer grant)" }
        }
      },
      "RevocationRequest": {
        "type": "object",
        "required": ["token"],
        "properties": {
          "token": { "type": "string" },
          "token_type_hint": { "type": "string", "enum": ["access_token"] }
        }
      },
      "AuthorizationDecision": {
        "type": "object",
        "required": ["decision", "client_id", "redirect_uri", "code_challenge"],
        "properties": {
          "decision": { "type": "string", "enum": ["approve", "deny"] },
          "client_id": { "type": "string" },
          "redirect_uri": { "type": "string", "format": "uri" },
          "code_challenge": { "type": "string" },
          "code_challenge_method": { "type": "string", "enum": ["S256"] },
          "scope": { "type": "string" },
          "state": { "type": "string" }
        }
      },
      "OAuthError": {
        "type": "object",
        "required": ["error", "error_description"],
        "properties": {
          "error": { "type": "string", "description": "RFC 6749 error code (invalid_request, invalid_grant, unsupported_grant_type…)" },
          "error_description": { "type": "string" }
        }
      }
    }
  }
}
