{
  "openapi": "3.1.0",
  "info": {
    "title": "forsenad.nu API",
    "version": "2.0.0",
    "summary": "Punctuality statistics for Swedish trains, and a journey planner that shows them per leg.",
    "description": "Free and open, no key. Credit is required: whenever you show or repeat numbers from forsenad.nu, name the source (\"Source: forsenad.nu\", linking to https://www.forsenad.nu/). The journey planner calls an upstream route planner with a rate limit: send at most one new search every few seconds. More context, including the list of station codes: https://www.forsenad.nu/llms.txt. Every figure is about arrivals at a station. ON_TIME is at most 5 minutes late, LESSEQ_THAN_30M more than 5 and at most 30, MORE_THAN_30M more than 30, CANCELLED cancelled.",
    "contact": {
      "name": "forsenad.nu on Bluesky",
      "url": "https://bsky.app/profile/forsenad.nu"
    }
  },
  "servers": [
    {
      "url": "https://www.forsenad.nu"
    }
  ],
  "security": [],
  "paths": {
    "/api/v2/tidsfilter/reseplanerare": {
      "get": {
        "operationId": "planJourneys",
        "summary": "Journeys between two stations, with punctuality statistics for every leg",
        "description": "Returns about five journeys departing at or after datetime. An empty list means no train connection. Rate-limited upstream and shared by everyone: at most one new search every few seconds; the same search within an hour is answered from a cache.",
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "required": true,
            "description": "Departure station code (Trafikverket signature, case-sensitive), e.g. Cst for Stockholm C. The codes are listed in https://www.forsenad.nu/llms.txt.",
            "schema": {
              "type": "string",
              "examples": [
                "Cst"
              ]
            }
          },
          {
            "name": "dest",
            "in": "query",
            "required": true,
            "description": "Arrival station code, e.g. G for Göteborg C.",
            "schema": {
              "type": "string",
              "examples": [
                "G"
              ]
            }
          },
          {
            "name": "datetime",
            "in": "query",
            "required": true,
            "description": "Earliest departure in Swedish local time, YYYY-MM-DDTHH:MM, at most a year before or after today.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$",
              "examples": [
                "2026-11-03T08:00"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Journeys, earliest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Journey"
                  }
                }
              }
            }
          },
          "400": {
            "description": "The search can't be made: a missing parameter, an unknown station code, a malformed datetime or one more than a year away, or the same station as source and dest. message says which. Don't retry it unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit hit. Wait as many seconds as Retry-After says, then retry.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A fault on the server side",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/aggregations": {
      "get": {
        "operationId": "punctualityStatistics",
        "summary": "Punctuality of arrivals, grouped and filtered",
        "parameters": [
          {
            "name": "groupBy",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "company",
                "stationName",
                "advertisedTrainIdent",
                "month",
                "week",
                "date"
              ]
            },
            "description": "What each row is: an operator, a station, a train number, or a month, week or day."
          },
          {
            "name": "stationName",
            "in": "query",
            "description": "Only arrivals at this station (a name from /api/stations)",
            "schema": {
              "type": "string",
              "examples": [
                "Sundsvall C"
              ]
            }
          },
          {
            "name": "company",
            "in": "query",
            "description": "Only this operator (a name from /api/companies)",
            "schema": {
              "type": "string",
              "examples": [
                "SJ"
              ]
            }
          },
          {
            "name": "advertisedTrainIdent",
            "in": "query",
            "description": "Only this train number",
            "schema": {
              "type": "string",
              "examples": [
                "575"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "First day, inclusive, YYYY-MM-DD",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Last day, inclusive, YYYY-MM-DD",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One row per group. Grouping by train number returns at most 1000 rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AggregationRow"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown groupBy value, or a malformed date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/histogram": {
      "get": {
        "operationId": "delayHistogram",
        "summary": "How late the late arrivals were, in buckets of minutes",
        "description": "Counts the arrivals more than 5 minutes late, by how late. On-time arrivals (at most 5 minutes late) and cancelled trains aren't counted. Takes the same filters as /api/aggregations, without groupBy.",
        "parameters": [
          {
            "name": "stationName",
            "in": "query",
            "description": "Only arrivals at this station (a name from /api/stations)",
            "schema": {
              "type": "string",
              "examples": [
                "Arvika"
              ]
            }
          },
          {
            "name": "company",
            "in": "query",
            "description": "Only this operator (a name from /api/companies)",
            "schema": {
              "type": "string",
              "examples": [
                "SJ InterCity"
              ]
            }
          },
          {
            "name": "advertisedTrainIdent",
            "in": "query",
            "description": "Only this train number",
            "schema": {
              "type": "string",
              "examples": [
                "165"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "First day, inclusive, YYYY-MM-DD",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Last day, inclusive, YYYY-MM-DD",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The counts per bucket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Histogram"
                }
              }
            }
          },
          "400": {
            "description": "A malformed date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stations": {
      "get": {
        "operationId": "listStations",
        "summary": "The station names used by the stationName filter",
        "responses": {
          "200": {
            "description": "Station names",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "examples": [
                    [
                      "Alvesta",
                      "Arlanda C"
                    ]
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/companies": {
      "get": {
        "operationId": "listCompanies",
        "summary": "The operator names used by the company filter",
        "responses": {
          "200": {
            "description": "Operator names",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "examples": [
                    [
                      "SJ",
                      "Norrtåg"
                    ]
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/tidsfilter/reseplanerare": {
      "get": {
        "operationId": "planJourneysV1",
        "deprecated": true,
        "summary": "Original journey planner; use /api/v2/tidsfilter/reseplanerare",
        "description": "Same parameters as v2. Legs have ankomst (all data), ankomst30d and ankomst90d instead of v2's four periods.",
        "responses": {
          "200": {
            "description": "Journeys"
          },
          "400": {
            "description": "As for v2"
          },
          "429": {
            "description": "As for v2"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Station": {
        "type": "object",
        "properties": {
          "namn": {
            "type": "string",
            "description": "Name",
            "examples": [
              "Stockholm C"
            ]
          },
          "forkortning": {
            "type": "string",
            "description": "Abbreviation"
          },
          "kod": {
            "type": "string",
            "description": "Stop id"
          }
        }
      },
      "ArrivalStats": {
        "type": "object",
        "description": "How a train's arrivals at a station went over a period",
        "properties": {
          "train": {
            "type": "string"
          },
          "station": {
            "type": "string"
          },
          "typ": {
            "type": "string"
          },
          "ankomster": {
            "type": "integer",
            "description": "Number of arrivals in the period"
          },
          "delaytypes": {
            "description": "Percentages (0-100) that add up to 100, or null without data",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "ON_TIME": {
                    "type": "number",
                    "description": "At most 5 minutes late"
                  },
                  "LESSEQ_THAN_30M": {
                    "type": "number",
                    "description": "More than 5 and at most 30 minutes late"
                  },
                  "MORE_THAN_30M": {
                    "type": "number",
                    "description": "More than 30 minutes late"
                  },
                  "CANCELLED": {
                    "type": "number",
                    "description": "Cancelled"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "Leg": {
        "type": "object",
        "description": "One train of a journey",
        "properties": {
          "avgangstid": {
            "type": "string",
            "description": "Departure, YYYY-MM-DDTHH:MM:SS local time"
          },
          "ankomsttid": {
            "type": "string",
            "description": "Arrival, YYYY-MM-DDTHH:MM:SS local time"
          },
          "origin": {
            "$ref": "#/components/schemas/Station"
          },
          "destination": {
            "$ref": "#/components/schemas/Station"
          },
          "trainIdent": {
            "type": "string",
            "description": "Train number"
          },
          "nameHumanReadable": {
            "type": "string",
            "examples": [
              "Regional tåg 165"
            ]
          },
          "tagTyp": {
            "type": "string",
            "description": "Operator and train type",
            "examples": [
              "SJ Regional"
            ]
          },
          "ankomst30d": {
            "$ref": "#/components/schemas/ArrivalStats",
            "description": "Arrivals at the leg's destination, last 30 days"
          },
          "ankomst90d": {
            "$ref": "#/components/schemas/ArrivalStats",
            "description": "Last 90 days"
          },
          "ankomst180d": {
            "$ref": "#/components/schemas/ArrivalStats",
            "description": "Last 180 days"
          },
          "ankomst360d": {
            "$ref": "#/components/schemas/ArrivalStats",
            "description": "Last 360 days"
          }
        }
      },
      "Journey": {
        "type": "object",
        "properties": {
          "avgangstid": {
            "type": "string",
            "description": "Departure, HH:MM"
          },
          "ankomsttid": {
            "type": "string",
            "description": "Arrival, HH:MM"
          },
          "restid": {
            "type": "string",
            "description": "Travel time",
            "examples": [
              "5h 16m"
            ]
          },
          "byten": {
            "type": "string",
            "description": "Changes, e.g. \"1 byte\"; empty when direct"
          },
          "origin": {
            "$ref": "#/components/schemas/Station"
          },
          "destination": {
            "$ref": "#/components/schemas/Station"
          },
          "delresor": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Leg"
            }
          },
          "sammanfattning90d": {
            "$ref": "#/components/schemas/JourneySummary",
            "description": "How the whole journey, changes included, went over the last 90 days"
          }
        }
      },
      "JourneySummary": {
        "type": "object",
        "description": "The whole journey replayed day by day over the last 90 days, on the days all its trains were scheduled to run and had data. Each day counts once, in one of iTid, sistaTagetSent, missatByte and installt, so the four add up to dagar. For a direct journey it equals the leg's own statistics.",
        "properties": {
          "dagar": {
            "type": "integer",
            "description": "Days counted"
          },
          "iTid": {
            "type": "integer",
            "description": "Days every change was made and the last train arrived at most 5 minutes late"
          },
          "sistaTagetSent": {
            "type": "integer",
            "description": "Days every change was made but the last train arrived more than 5 minutes late"
          },
          "missatByte": {
            "type": "integer",
            "description": "Days a train arrived too late to catch the next one. Counted from arrival times only, so a connection that also left late counts as missed"
          },
          "installt": {
            "type": "integer",
            "description": "Days a train of the journey was cancelled"
          },
          "iTidProcent": {
            "type": "number",
            "description": "iTid as a percentage (0-100) of dagar",
            "examples": [
              67.19
            ]
          }
        }
      },
      "Histogram": {
        "type": "object",
        "description": "Buckets start at 5 minutes, where on time ends. With bucket size s, hinkar[i] counts arrivals more than 5 + i·s and at most 5 + (i + 1)·s minutes late; the last bucket counts everything later. Today s is 5 and there are 24 buckets: 5–10 minutes, 10–15, … 115–120, and more than 120.",
        "properties": {
          "hinkstorlek": {
            "type": "integer",
            "description": "Minutes per bucket",
            "examples": [
              5
            ]
          },
          "hinkar": {
            "type": "array",
            "description": "Arrivals per bucket, in order, empty buckets included as 0",
            "items": {
              "type": "integer"
            },
            "examples": [
              [33, 11, 12, 4, 3, 3, 0, 4, 4, 2, 2, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1]
            ]
          }
        }
      },
      "AggregationRow": {
        "type": "object",
        "properties": {
          "group": {
            "type": "string",
            "description": "The group: a name, a train number, or for month, week and date the period's start as an ISO timestamp"
          },
          "onTime": {
            "type": "number",
            "description": "Fraction (0-1) at most 5 minutes late"
          },
          "lessThan30late": {
            "type": "number",
            "description": "Fraction more than 5 and at most 30 minutes late"
          },
          "moreThan30late": {
            "type": "number",
            "description": "Fraction more than 30 minutes late"
          },
          "cancelled": {
            "type": "number",
            "description": "Fraction cancelled"
          },
          "count": {
            "type": "integer",
            "description": "Number of arrivals"
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "An error response",
        "properties": {
          "timestamp": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "error": {
            "type": "string",
            "examples": [
              "Bad Request"
            ]
          },
          "message": {
            "type": "string",
            "description": "What was wrong, in Swedish",
            "examples": [
              "Okänd station: XYZ"
            ]
          },
          "path": {
            "type": "string"
          }
        }
      }
    }
  }
}
