{
  "openapi": "3.1.0",
  "info": {
    "title": "RoTV Guide API",
    "version": "1.0.0",
    "description": "Free, read-only JSON API for Romanian TV data: live EPG across 254 channels, prime-time lineups, tonight picks, important events, title details with streaming availability, search and a decision-making concierge. All schedule times are Europe/Bucharest (local timestamps carry the UTC offset). The API is read-only: it never modifies user data. Attribution: when you display streaming platform availability, show \"Streaming availability data: JustWatch\". Anonymous access is allowed (60 req/h on dynamic endpoints); a free API key raises the limit to 300 requests/min.",
    "contact": {
      "email": "marian@mmatinca.eu"
    }
  },
  "servers": [
    {
      "url": "https://tv.madeinro.eu"
    }
  ],
  "security": [
    {},
    {
      "bearerAuth": []
    },
    {
      "apiKeyAuth": []
    }
  ],
  "paths": {
    "/api/v1/freshness": {
      "get": {
        "operationId": "checkFreshness",
        "summary": "Data freshness",
        "description": "Age of the EPG and streaming snapshots. EPG refreshes 4x/day; feeds every 15 min.",
        "responses": {
          "200": {
            "description": "Freshness info.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Freshness"
                },
                "example": {
                  "generated_at": "2026-07-20T15:00:00+03:00",
                  "data_as_of": "2026-07-20T14:00:00+03:00",
                  "epg_age_min": 60,
                  "streaming_age_min": 180,
                  "stale": false
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/now": {
      "get": {
        "operationId": "getNow",
        "summary": "On air now",
        "description": "Programs airing right now across Romanian TV channels (Europe/Bucharest).",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/now-next": {
      "get": {
        "operationId": "getNowNext",
        "summary": "Now and next",
        "description": "Current program plus the following one, per channel.",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/prime-time": {
      "get": {
        "operationId": "getPrimeTime",
        "summary": "Prime time tonight",
        "description": "Tonight’s prime-time lineup (roughly 20:00–23:00 Europe/Bucharest).",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tonight": {
      "get": {
        "operationId": "getTonight",
        "summary": "Tonight",
        "description": "What’s worth watching tonight on Romanian TV.",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/important-today": {
      "get": {
        "operationId": "getImportantToday",
        "summary": "Important today",
        "description": "Major broadcasts today (sports finals, major events) with tiered importance.",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/recommendations": {
      "get": {
        "operationId": "getRecommendations",
        "summary": "Recommendations",
        "description": "Editorially ranked recommendations for today across TV and streaming.",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "rotv_title_1318447",
                      "type": "movie",
                      "title": "Apex",
                      "summary": "O femeie îndoliată, plecată singură într-o aventură prin sălbăticia australiană, este prinsă într-un joc ciudat cu un ucigaș viclean, care o vede drept pradă.",
                      "image": {
                        "landscape": "https://tv.madeinro.eu/assets/streaming/netflix-apex-backdrop-5afce0b2fd.webp",
                        "poster": "https://tv.madeinro.eu/assets/streaming/netflix-apex-fbe5fd2c70.webp"
                      },
                      "canonical_url": "https://tv.madeinro.eu/film/apex",
                      "watch_url": "https://tv.madeinro.eu/unde-vad/apex",
                      "availability": {
                        "country": "RO",
                        "platforms": [
                          "Netflix"
                        ]
                      },
                      "updated_at": "2026-07-20T02:04:50.184Z"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/evening-plan": {
      "get": {
        "operationId": "getEveningPlan",
        "summary": "Evening plan",
        "description": "A pre-built evening viewing plan (ordered items for tonight).",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/titles": {
      "get": {
        "operationId": "listTitles",
        "summary": "Titles index",
        "description": "Index of movie/series titles known to the guide.",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "rotv_title_1318447",
                      "type": "movie",
                      "title": "Apex",
                      "summary": "O femeie îndoliată, plecată singură într-o aventură prin sălbăticia australiană, este prinsă într-un joc ciudat cu un ucigaș viclean, care o vede drept pradă.",
                      "image": {
                        "landscape": "https://tv.madeinro.eu/assets/streaming/netflix-apex-backdrop-5afce0b2fd.webp",
                        "poster": "https://tv.madeinro.eu/assets/streaming/netflix-apex-fbe5fd2c70.webp"
                      },
                      "canonical_url": "https://tv.madeinro.eu/film/apex",
                      "watch_url": "https://tv.madeinro.eu/unde-vad/apex",
                      "availability": {
                        "country": "RO",
                        "platforms": [
                          "Netflix"
                        ]
                      },
                      "updated_at": "2026-07-20T02:04:50.184Z"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/titles/{id}": {
      "get": {
        "operationId": "getTitleDetails",
        "summary": "Title details",
        "description": "Details for one title, including streaming availability in Romania. Display \"Streaming availability data: JustWatch\" when showing platforms.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Title id from /api/v1/titles or search results — rotv_title_<n> or plain numeric <n> (e.g. rotv_title_585)."
          }
        ],
        "responses": {
          "200": {
            "description": "One title.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Item"
                },
                "example": {
                  "id": "rotv_title_1318447",
                  "type": "movie",
                  "title": "Apex",
                  "summary": "O femeie îndoliată, plecată singură într-o aventură prin sălbăticia australiană, este prinsă într-un joc ciudat cu un ucigaș viclean, care o vede drept pradă.",
                  "image": {
                    "landscape": "https://tv.madeinro.eu/assets/streaming/netflix-apex-backdrop-5afce0b2fd.webp",
                    "poster": "https://tv.madeinro.eu/assets/streaming/netflix-apex-fbe5fd2c70.webp"
                  },
                  "canonical_url": "https://tv.madeinro.eu/film/apex",
                  "watch_url": "https://tv.madeinro.eu/unde-vad/apex",
                  "availability": {
                    "country": "RO",
                    "platforms": [
                      "Netflix"
                    ]
                  },
                  "updated_at": "2026-07-20T02:04:50.184Z"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "searchPrograms",
        "summary": "Search TV programs",
        "description": "Search programs across all Romanian TV channels by free text, channel, category and natural timeframe. Times are Europe/Bucharest.",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200,
              "description": "Free text to match against program title (case/diacritic-insensitive)"
            },
            "description": "Free text to match against program title (case/diacritic-insensitive)"
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Channel id, display name, or alias (e.g. \"PRO TV\", \"tv-pro-tv\", \"HBO\")"
            },
            "description": "Channel id, display name, or alias (e.g. \"PRO TV\", \"tv-pro-tv\", \"HBO\")"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Channel category: Generaliste | Știri | Sport | Filme & Seriale | Documentare | Copii | Muzică | Altele"
            },
            "description": "Channel category: Generaliste | Știri | Sport | Filme & Seriale | Documentare | Copii | Muzică | Altele"
          },
          {
            "name": "timeframe",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "now",
              "description": "Natural time reference: now | tonight | tomorrow | weekend | primetime | today | YYYY-MM-DD | ISO instant | ISO range \"A/B\""
            },
            "description": "Natural time reference: now | tonight | tomorrow | weekend | primetime | today | YYYY-MM-DD | ISO instant | ISO range \"A/B\""
          },
          {
            "name": "exclude_news",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching programs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                },
                "example": {
                  "generated_at": "2026-07-20T14:00:00+03:00",
                  "asked_at_utc": "2026-07-20T12:05:00.000Z",
                  "timeframe_label": "tonight",
                  "window": {
                    "from_utc": "2026-07-20T17:00:00.000Z",
                    "to_utc": "2026-07-20T20:59:00.000Z"
                  },
                  "count": 1,
                  "items": [
                    {
                      "channel_id": "tv-pro-tv",
                      "channel_name": "PRO TV",
                      "channel_category": "Generaliste",
                      "program": {
                        "title": "Las Fierbinți",
                        "start_local": "2026-07-20T20:30:00+03:00",
                        "start_utc": "2026-07-20T17:30:00.000Z",
                        "stop_local": "2026-07-20T21:30:00+03:00",
                        "stop_utc": "2026-07-20T18:30:00.000Z",
                        "duration_min": 60
                      },
                      "match_reason": "in timeframe"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "searchProgramsPost",
        "summary": "Search TV programs (JSON body)",
        "description": "Same as GET /api/v1/search, parameters as a JSON body.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchParams"
              },
              "example": {
                "query": "film",
                "timeframe": "tonight",
                "limit": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching programs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                },
                "example": {
                  "generated_at": "2026-07-20T14:00:00+03:00",
                  "asked_at_utc": "2026-07-20T12:05:00.000Z",
                  "timeframe_label": "tonight",
                  "window": {
                    "from_utc": "2026-07-20T17:00:00.000Z",
                    "to_utc": "2026-07-20T20:59:00.000Z"
                  },
                  "count": 1,
                  "items": [
                    {
                      "channel_id": "tv-pro-tv",
                      "channel_name": "PRO TV",
                      "channel_category": "Generaliste",
                      "program": {
                        "title": "Las Fierbinți",
                        "start_local": "2026-07-20T20:30:00+03:00",
                        "start_utc": "2026-07-20T17:30:00.000Z",
                        "stop_local": "2026-07-20T21:30:00+03:00",
                        "stop_utc": "2026-07-20T18:30:00.000Z",
                        "duration_min": 60
                      },
                      "match_reason": "in timeframe"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/concierge": {
      "get": {
        "operationId": "concierge",
        "summary": "Concierge — decide what to watch",
        "description": "One decision for a free-time window: a single primary pick (TV or streaming) with confidence and alternatives. Nested/array parameters are easier via POST.",
        "parameters": [
          {
            "name": "window",
            "in": "query",
            "required": false,
            "schema": {
              "type": "object",
              "properties": {
                "start": {
                  "type": "string",
                  "default": "now",
                  "description": "\"now\" | \"HH:MM\" (Europe/Bucharest) | ISO instant"
                },
                "duration_min": {
                  "type": "integer",
                  "minimum": 30,
                  "maximum": 360,
                  "default": 120
                }
              },
              "additionalProperties": false,
              "description": "Explicit window. Skips lookahead."
            },
            "description": "Explicit window. Skips lookahead.",
            "style": "deepObject",
            "explode": true
          },
          {
            "name": "duration_hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0.5,
              "maximum": 6,
              "description": "Shorthand when window is absent; starts at now."
            },
            "description": "Shorthand when window is absent; starts at now."
          },
          {
            "name": "mood",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "obosit | vesel | concentrat | romantic | familie | captivant (RO/EN aliases accepted)"
            },
            "description": "obosit | vesel | concentrat | romantic | familie | captivant (RO/EN aliases accepted)"
          },
          {
            "name": "exclude_categories",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "politica",
                  "reality",
                  "talkshow",
                  "stiri"
                ]
              },
              "default": [
                "politica",
                "reality",
                "talkshow",
                "stiri"
              ],
              "description": "Anti-noise filter (default: all four)."
            },
            "description": "Anti-noise filter (default: all four).",
            "style": "form",
            "explode": false
          },
          {
            "name": "exclude_keywords",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "name": "sources",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "tv",
                  "streaming"
                ]
              },
              "default": [
                "tv",
                "streaming"
              ]
            },
            "style": "form",
            "explode": false
          },
          {
            "name": "max_alternatives",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 5,
              "default": 3
            }
          },
          {
            "name": "risk_aversion",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "high"
              ],
              "default": "low",
              "description": "low = 3 alternatives, high = none"
            },
            "description": "low = 3 alternatives, high = none"
          },
          {
            "name": "allow_pauses",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "min_rating",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10,
              "default": 0
            }
          },
          {
            "name": "prefer",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": false
          }
        ],
        "responses": {
          "200": {
            "description": "The decision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConciergeResponse"
                },
                "example": {
                  "ok": true,
                  "asked_at_utc": "2026-07-20T17:02:00.000Z",
                  "window": {
                    "start_local": "2026-07-20T20:02:00+03:00",
                    "end_local": "2026-07-20T22:02:00+03:00",
                    "start_utc": "2026-07-20T17:02:00.000Z",
                    "end_utc": "2026-07-20T19:02:00.000Z",
                    "duration_min": 120,
                    "source": "default"
                  },
                  "context": {
                    "mood": "obosit",
                    "sources": [
                      "tv",
                      "streaming"
                    ],
                    "risk_aversion": "low"
                  },
                  "decision": {
                    "degraded": false,
                    "primary_kind": "tv",
                    "primary_title": "Las Fierbinți",
                    "primary_summary": "Las Fierbinți pe PRO TV la 20:30",
                    "segments": [],
                    "confidence_pct": 78,
                    "confidence_label": "high",
                    "confidence_breakdown": null
                  },
                  "reasoning": [
                    "PRO TV (Generaliste) la 20:30",
                    "Durata se potrivește bine cu fereastra ta"
                  ],
                  "anti_noise": {
                    "enabled_categories": [
                      "politica",
                      "reality",
                      "talkshow",
                      "stiri"
                    ],
                    "filtered_count": 41,
                    "by_category": {}
                  },
                  "alternatives": [
                    {
                      "title": "Un film liniștit",
                      "kind": "streaming",
                      "provider_name": "Netflix",
                      "reason_not_picked": "confidence sub primary"
                    }
                  ],
                  "lookahead": {
                    "found": false
                  },
                  "important_today": [],
                  "sources_used": [
                    "epg-normalized",
                    "streaming-full",
                    "moods",
                    "title-genre-extract",
                    "anti-noise"
                  ],
                  "freshness": {
                    "epg_age_min": 45,
                    "streaming_age_min": 120,
                    "stale": false
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "conciergePost",
        "summary": "Concierge (JSON body)",
        "description": "Same as GET /api/v1/concierge, parameters as a JSON body.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConciergeParams"
              },
              "example": {
                "mood": "obosit",
                "duration_hours": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConciergeResponse"
                },
                "example": {
                  "ok": true,
                  "asked_at_utc": "2026-07-20T17:02:00.000Z",
                  "window": {
                    "start_local": "2026-07-20T20:02:00+03:00",
                    "end_local": "2026-07-20T22:02:00+03:00",
                    "start_utc": "2026-07-20T17:02:00.000Z",
                    "end_utc": "2026-07-20T19:02:00.000Z",
                    "duration_min": 120,
                    "source": "default"
                  },
                  "context": {
                    "mood": "obosit",
                    "sources": [
                      "tv",
                      "streaming"
                    ],
                    "risk_aversion": "low"
                  },
                  "decision": {
                    "degraded": false,
                    "primary_kind": "tv",
                    "primary_title": "Las Fierbinți",
                    "primary_summary": "Las Fierbinți pe PRO TV la 20:30",
                    "segments": [],
                    "confidence_pct": 78,
                    "confidence_label": "high",
                    "confidence_breakdown": null
                  },
                  "reasoning": [
                    "PRO TV (Generaliste) la 20:30",
                    "Durata se potrivește bine cu fereastra ta"
                  ],
                  "anti_noise": {
                    "enabled_categories": [
                      "politica",
                      "reality",
                      "talkshow",
                      "stiri"
                    ],
                    "filtered_count": 41,
                    "by_category": {}
                  },
                  "alternatives": [
                    {
                      "title": "Un film liniștit",
                      "kind": "streaming",
                      "provider_name": "Netflix",
                      "reason_not_picked": "confidence sub primary"
                    }
                  ],
                  "lookahead": {
                    "found": false
                  },
                  "important_today": [],
                  "sources_used": [
                    "epg-normalized",
                    "streaming-full",
                    "moods",
                    "title-genre-extract",
                    "anti-noise"
                  ],
                  "freshness": {
                    "epg_age_min": 45,
                    "streaming_age_min": 120,
                    "stale": false
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/keys": {
      "post": {
        "operationId": "createApiKey",
        "summary": "Request a free API key",
        "description": "Returns a key (rotv_pk_…) with a 300 requests/min limit. The key is shown once.",
        "security": [
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyRequest"
              },
              "example": {
                "email": "dev@example.com",
                "note": "weather-tv assistant"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyResponse"
                },
                "example": {
                  "key": "rotv_pk_4f8a2b9c1d3e5f70",
                  "rate_limit_rpm": 300
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "operationId": "health",
        "summary": "Health check",
        "responses": {
          "200": {
            "description": "Service health.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "ok": true
                }
              }
            }
          }
        }
      }
    },
    "/feeds/important-today.json": {
      "get": {
        "operationId": "feedImportantToday",
        "summary": "Feed — important today",
        "description": "Static feed of today’s major broadcasts. Refreshed every 15 minutes.",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/feeds/tonight.json": {
      "get": {
        "operationId": "feedTonight",
        "summary": "Feed — tonight",
        "description": "Static feed of tonight’s picks. Refreshed every 15 minutes.",
        "responses": {
          "200": {
            "description": "Collection of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                },
                "example": {
                  "generated_at": "2026-07-20T15:36:31.244Z",
                  "data_as_of": "2026-07-20T02:04:50.184Z",
                  "source": "https://tv.madeinro.eu",
                  "attribution": "Streaming availability data: JustWatch",
                  "items": [
                    {
                      "id": "pro-tv-stirile-pro-tv-1900",
                      "type": "program",
                      "title": "Știrile PRO TV",
                      "summary": "Principalul jurnal de știri al serii.",
                      "channel": "PRO TV",
                      "channel_slug": "pro-tv",
                      "starts_at": "2026-07-20T19:00:00+03:00",
                      "ends_at": "2026-07-20T20:30:00+03:00",
                      "canonical_url": "https://tv.madeinro.eu/canal-tv/pro-tv",
                      "updated_at": "2026-07-20T15:00:00+03:00"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Optional. Authorization: Bearer rotv_pk_… — anonymous access is allowed with lower limits (60 req/h on dynamic endpoints)."
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Optional alternative to the bearer header."
      }
    },
    "schemas": {
      "Item": {
        "type": "object",
        "description": "Universal RoTV Guide object: a TV program, movie or series. All local times carry the Europe/Bucharest UTC offset.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable identifier, e.g. rotv_title_585 or rotv_prog_tv-pro-tv_2026-07-20T18:00:00Z."
          },
          "type": {
            "type": "string",
            "enum": [
              "movie",
              "series",
              "program"
            ]
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string",
            "description": "Short synopsis, when available."
          },
          "channel": {
            "type": "string",
            "description": "Broadcast channel display name (TV programs)."
          },
          "channel_slug": {
            "type": "string"
          },
          "starts_at": {
            "type": "string",
            "description": "ISO 8601 with Europe/Bucharest offset, e.g. 2026-07-20T20:30:00+03:00."
          },
          "ends_at": {
            "type": "string",
            "description": "ISO 8601 with Europe/Bucharest offset."
          },
          "image": {
            "type": "object",
            "properties": {
              "landscape": {
                "type": "string"
              },
              "poster": {
                "type": "string"
              }
            }
          },
          "canonical_url": {
            "type": "string",
            "description": "Human page on tv.madeinro.eu."
          },
          "watch_url": {
            "type": "string"
          },
          "availability": {
            "type": "object",
            "description": "Streaming availability. Attribution required when displayed: \"Streaming availability data: JustWatch\".",
            "properties": {
              "country": {
                "type": "string",
                "description": "ISO country code, currently RO."
              },
              "platforms": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "updated_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "type",
          "title",
          "canonical_url"
        ],
        "additionalProperties": true
      },
      "Collection": {
        "type": "object",
        "description": "Standard envelope returned by the static collection endpoints and feeds.",
        "properties": {
          "generated_at": {
            "type": "string",
            "description": "When this payload was generated (ISO 8601)."
          },
          "data_as_of": {
            "type": "string",
            "description": "Timestamp of the underlying EPG/streaming snapshot (ISO 8601)."
          },
          "source": {
            "type": "string",
            "description": "Data source label, e.g. epg-normalized."
          },
          "attribution": {
            "type": "string",
            "description": "Required attribution string for republishing."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Item"
            }
          }
        },
        "required": [
          "generated_at",
          "items"
        ],
        "additionalProperties": true
      },
      "Freshness": {
        "type": "object",
        "description": "Age of the underlying data. Use it to decide whether to trust cached answers.",
        "properties": {
          "generated_at": {
            "type": "string"
          },
          "data_as_of": {
            "type": "string"
          },
          "epg_age_min": {
            "type": "number",
            "description": "Minutes since the last EPG refresh."
          },
          "streaming_age_min": {
            "type": "number",
            "description": "Minutes since the last streaming refresh."
          },
          "stale": {
            "type": "boolean",
            "description": "True when data is older than the expected cadence."
          }
        },
        "required": [
          "stale"
        ],
        "additionalProperties": true
      },
      "SearchParams": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Free text to match against program title (case/diacritic-insensitive)"
          },
          "channel": {
            "type": "string",
            "description": "Channel id, display name, or alias (e.g. \"PRO TV\", \"tv-pro-tv\", \"HBO\")"
          },
          "category": {
            "type": "string",
            "description": "Channel category: Generaliste | Știri | Sport | Filme & Seriale | Documentare | Copii | Muzică | Altele"
          },
          "timeframe": {
            "type": "string",
            "default": "now",
            "description": "Natural time reference: now | tonight | tomorrow | weekend | primetime | today | YYYY-MM-DD | ISO instant | ISO range \"A/B\""
          },
          "exclude_news": {
            "type": "boolean",
            "default": false
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 10
          }
        },
        "description": "Search parameters (generated from the MCP tv_search_program zod schema)."
      },
      "SearchResponse": {
        "type": "object",
        "description": "Search result. Mirrors the MCP tv_search_program output shape.",
        "properties": {
          "generated_at": {
            "type": "string"
          },
          "asked_at_utc": {
            "type": "string"
          },
          "timeframe_label": {
            "type": "string"
          },
          "window": {
            "type": "object",
            "properties": {
              "from_utc": {
                "type": "string"
              },
              "to_utc": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "count": {
            "type": "number"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "channel_id": {
                  "type": "string"
                },
                "channel_name": {
                  "type": "string"
                },
                "channel_category": {
                  "type": "string"
                },
                "program": {
                  "type": "object",
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "start_local": {
                      "type": "string",
                      "description": "Europe/Bucharest local time."
                    },
                    "start_utc": {
                      "type": "string"
                    },
                    "stop_local": {
                      "type": "string"
                    },
                    "stop_utc": {
                      "type": "string"
                    },
                    "duration_min": {
                      "type": "number"
                    },
                    "category": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "title",
                    "start_utc",
                    "stop_utc"
                  ],
                  "additionalProperties": true
                },
                "match_reason": {
                  "type": "string"
                }
              },
              "required": [
                "channel_id",
                "channel_name",
                "program"
              ],
              "additionalProperties": true
            }
          }
        },
        "required": [
          "asked_at_utc",
          "count",
          "items"
        ],
        "additionalProperties": true
      },
      "ConciergeParams": {
        "type": "object",
        "properties": {
          "window": {
            "type": "object",
            "properties": {
              "start": {
                "type": "string",
                "default": "now",
                "description": "\"now\" | \"HH:MM\" (Europe/Bucharest) | ISO instant"
              },
              "duration_min": {
                "type": "integer",
                "minimum": 30,
                "maximum": 360,
                "default": 120
              }
            },
            "additionalProperties": false,
            "description": "Explicit window. Skips lookahead."
          },
          "duration_hours": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 6,
            "description": "Shorthand when window is absent; starts at now."
          },
          "mood": {
            "type": "string",
            "description": "obosit | vesel | concentrat | romantic | familie | captivant (RO/EN aliases accepted)"
          },
          "exclude_categories": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "politica",
                "reality",
                "talkshow",
                "stiri"
              ]
            },
            "default": [
              "politica",
              "reality",
              "talkshow",
              "stiri"
            ],
            "description": "Anti-noise filter (default: all four)."
          },
          "exclude_keywords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "tv",
                "streaming"
              ]
            },
            "default": [
              "tv",
              "streaming"
            ]
          },
          "max_alternatives": {
            "type": "integer",
            "minimum": 0,
            "maximum": 5,
            "default": 3
          },
          "risk_aversion": {
            "type": "string",
            "enum": [
              "low",
              "high"
            ],
            "default": "low",
            "description": "low = 3 alternatives, high = none"
          },
          "allow_pauses": {
            "type": "boolean",
            "default": true
          },
          "min_rating": {
            "type": "number",
            "minimum": 0,
            "maximum": 10,
            "default": 0
          },
          "prefer": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "description": "Concierge parameters (generated from the MCP tv_concierge zod schema)."
      },
      "ConciergeResponse": {
        "type": "object",
        "description": "One decision: what to watch in the given window, with confidence, reasoning and up to 3 alternatives. Mirrors the MCP tv_concierge output.",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "description": "Present when ok=false (e.g. no_candidates_after_filters)."
          },
          "asked_at_utc": {
            "type": "string"
          },
          "window": {
            "type": "object",
            "properties": {
              "start_local": {
                "type": "string"
              },
              "end_local": {
                "type": "string"
              },
              "start_utc": {
                "type": "string"
              },
              "end_utc": {
                "type": "string"
              },
              "duration_min": {
                "type": "number"
              },
              "source": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "context": {
            "type": "object",
            "additionalProperties": true
          },
          "decision": {
            "type": "object",
            "properties": {
              "degraded": {
                "type": "boolean"
              },
              "primary_kind": {
                "type": "string",
                "description": "tv | streaming | mixed | null."
              },
              "primary_title": {
                "type": "string"
              },
              "primary_summary": {
                "type": "string"
              },
              "segments": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "confidence_pct": {
                "type": "number"
              },
              "confidence_label": {
                "type": "string"
              },
              "confidence_breakdown": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "additionalProperties": true
          },
          "reasoning": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "anti_noise": {
            "type": "object",
            "additionalProperties": true
          },
          "alternatives": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "lookahead": {
            "type": "object",
            "additionalProperties": true
          },
          "important_today": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "sources_used": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "freshness": {
            "$ref": "#/components/schemas/Freshness"
          }
        },
        "required": [
          "ok",
          "asked_at_utc",
          "decision"
        ],
        "additionalProperties": true
      },
      "KeyRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Where usage notices are sent. Not shared."
          },
          "note": {
            "type": "string",
            "description": "Optional note about your integration."
          }
        },
        "required": [
          "email"
        ],
        "additionalProperties": false
      },
      "KeyResponse": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "API key (rotv_pk_…). Shown once — save it immediately."
          },
          "rate_limit_rpm": {
            "type": "number"
          }
        },
        "required": [
          "key",
          "rate_limit_rpm"
        ],
        "additionalProperties": true
      },
      "Health": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "additionalProperties": true
      }
    }
  }
}
