{
  "openapi": "3.0.3",
  "info": {
    "title": "Laatulehti News Headlines API",
    "version": "1.0.0",
    "description": "Read news headlines collected from European publishers for a requested number of hours. No API key. Use hours for the time window; follow next_url until null to retrieve all results. The time window remains fixed while paging. Titles use existing Finnish or English translations; when unavailable the original is returned and language identifies the actual language. Known duplicate feed copies are omitted. Sports are excluded by default. Genre metadata identifies whether a publisher supplied the genre or it was inferred from the title or assigned as a fallback."
  },
  "servers": [
    {
      "url": "https://laatulehti.fi"
    }
  ],
  "security": [],
  "paths": {
    "/api/headlines": {
      "get": {
        "operationId": "getHeadlines",
        "summary": "Get news headlines from the last requested number of hours",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "description": "Hours in the requested time window. Greater than 0, at most 720; fractions are supported.",
            "schema": {
              "type": "number",
              "exclusiveMinimum": true,
              "minimum": 0,
              "maximum": 720,
              "default": 24
            }
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "description": "Preferred headline language. Missing translations fall back to the original; each item identifies its actual language.",
            "schema": {
              "type": "string",
              "enum": [
                "fi",
                "en",
                "original"
              ],
              "default": "fi"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Publisher EU country code(s), comma-separated, e.g. FI or FI,SE,DE. This is not the location of the event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Source slug, for example fi-hs. See /api/sources.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "topic",
            "in": "query",
            "required": false,
            "description": "Topic identifier, for example politiikka or talous. See /api/taxonomy.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "time_basis",
            "in": "query",
            "required": false,
            "description": "Use first collection time (default), or publication time with collection-time fallback if missing.",
            "schema": {
              "type": "string",
              "enum": [
                "collected",
                "published"
              ],
              "default": "collected"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Headlines per page. Always follow next_url to retrieve the entire requested window.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 500
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Optional ISO 8601 window end with timezone. Defaults to request time. Preserved by next_url.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor. Follow next_url without changing filters.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of headlines; follow next_url for the remaining results.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HeadlinesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter or cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Temporary server error; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Headline": {
        "type": "object",
        "required": [
          "id",
          "title",
          "language",
          "translation_status",
          "original_title",
          "original_language",
          "url",
          "source",
          "published_at",
          "collected_at",
          "time",
          "time_basis",
          "topic",
          "iptc_code",
          "genre",
          "genre_origin",
          "source_genre_label"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable news item identifier; kept as a string to preserve large integers."
          },
          "title": {
            "type": "string"
          },
          "language": {
            "type": "string",
            "nullable": true,
            "description": "Actual language of title, including original-language fallback."
          },
          "translation_status": {
            "type": "string",
            "enum": [
              "original",
              "translated",
              "unavailable"
            ]
          },
          "original_title": {
            "type": "string"
          },
          "original_language": {
            "type": "string",
            "nullable": true,
            "description": "Language of the original publisher headline, independent of requested translation language."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "source": {
            "type": "object",
            "required": [
              "slug",
              "name",
              "country"
            ],
            "properties": {
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "country": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "published_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "collected_at": {
            "type": "string",
            "format": "date-time"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          },
          "time_basis": {
            "type": "string",
            "enum": [
              "collected",
              "published"
            ]
          },
          "topic": {
            "type": "string",
            "nullable": true
          },
          "iptc_code": {
            "type": "string",
            "nullable": true
          },
          "genre": {
            "type": "string",
            "nullable": true,
            "description": "Journalistic genre (uutinen, mielipide, analyysi, haastattelu, reportaasi)."
          },
          "genre_origin": {
            "type": "string",
            "nullable": true,
            "enum": [
              "publisher",
              "title",
              "default",
              null
            ],
            "description": "publisher = explicit publisher label; title = keyword inference; default = news fallback, not publisher confirmation."
          },
          "source_genre_label": {
            "type": "string",
            "nullable": true,
            "description": "Original publisher genre label when available."
          }
        }
      },
      "HeadlinesResponse": {
        "type": "object",
        "required": [
          "api_version",
          "generated_at",
          "window",
          "filters",
          "total",
          "count",
          "has_more",
          "next_url",
          "headlines"
        ],
        "properties": {
          "api_version": {
            "type": "string"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "window": {
            "type": "object",
            "required": [
              "hours",
              "from",
              "to",
              "time_basis"
            ],
            "properties": {
              "hours": {
                "type": "number"
              },
              "from": {
                "type": "string",
                "format": "date-time"
              },
              "to": {
                "type": "string",
                "format": "date-time"
              },
              "time_basis": {
                "type": "string",
                "enum": [
                  "collected",
                  "published"
                ]
              }
            }
          },
          "filters": {
            "type": "object",
            "properties": {
              "language": {
                "type": "string"
              },
              "country": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "source": {
                "type": "string",
                "nullable": true
              },
              "topic": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "total": {
            "type": "integer",
            "description": "Total matching headlines in the entire requested window."
          },
          "count": {
            "type": "integer",
            "description": "Number of headlines on this page."
          },
          "has_more": {
            "type": "boolean"
          },
          "next_url": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "Request this URL unchanged until null to retrieve all matching headlines."
          },
          "headlines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Headline"
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "parameter": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}
