{
  "openapi": "3.1.0",
  "info": {
    "title": "events.costico.eu — Timișoara Events API",
    "description": "Read-only public API exposing the upcoming-events index for Timișoara, Romania. Updated daily by a scheduled scraper. All timestamps are unix seconds. Display in Europe/Bucharest.",
    "version": "1.0.0",
    "contact": { "email": "costi.bleotu@gmail.com" },
    "license": { "name": "MIT" }
  },
  "servers": [
    { "url": "https://events.costico.eu", "description": "Production" }
  ],
  "externalDocs": {
    "url": "https://events.costico.eu/llms.txt",
    "description": "Site summary for LLMs"
  },
  "tags": [
    { "name": "events", "description": "Event listings and details" },
    { "name": "taxonomy", "description": "Type and subtype dictionaries" },
    { "name": "agents", "description": "Agent-friendly endpoints" }
  ],
  "paths": {
    "/api/events": {
      "get": {
        "tags": ["events"],
        "summary": "List upcoming events with optional filters",
        "operationId": "listEvents",
        "parameters": [
          { "name": "type", "in": "query", "schema": { "type": "string" }, "description": "Event type slug" },
          { "name": "subtype", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true, "description": "Subtype slug(s) — multiple allowed (OR semantics)" },
          { "name": "venue", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "organizer", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "source", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true, "description": "Hostname of the scraped source" },
          { "name": "age", "in": "query", "schema": { "type": "array", "items": { "type": "string", "enum": ["0-2", "3-5", "6-12", "13-17", "18+", "all"] } }, "style": "form", "explode": true },
          { "name": "free", "in": "query", "schema": { "type": "string", "enum": ["1"] } },
          { "name": "when", "in": "query", "schema": { "type": "array", "items": { "type": "string", "enum": ["today", "tonight", "tomorrow", "weekend", "month"] } }, "style": "form", "explode": true },
          { "name": "search", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Event list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": ["events"],
        "summary": "Ingest new events (scraper-only)",
        "operationId": "ingestEvents",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "events": { "type": "array", "items": { "$ref": "#/components/schemas/EventInput" } }
                },
                "required": ["events"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Number of events ingested",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "count": { "type": "integer" } } } } }
          },
          "401": { "description": "Invalid or missing bearer token" }
        }
      }
    },
    "/api/events/{id}": {
      "get": {
        "tags": ["events"],
        "summary": "Get one event by id",
        "operationId": "getEvent",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Event", "content": { "application/json": { "schema": { "type": "object", "properties": { "event": { "$ref": "#/components/schemas/Event" } } } } } },
          "404": { "description": "Not found" }
        }
      }
    },
    "/api/events/facets": {
      "get": {
        "tags": ["events"],
        "summary": "Distinct filter values for upcoming events",
        "operationId": "listFacets",
        "responses": {
          "200": { "description": "Facets payload", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Facets" } } } }
        }
      }
    },
    "/api/events/known": {
      "get": {
        "tags": ["events", "agents"],
        "summary": "Known externalIds per source (scraper pre-check)",
        "operationId": "listKnown",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Map of source → externalIds",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": { "type": "integer" },
                    "bySource": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } },
                    "oldestUpdate": { "type": "integer", "nullable": true },
                    "newestUpdate": { "type": "integer", "nullable": true }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing bearer token" }
        }
      }
    },
    "/api/types": {
      "get": {
        "tags": ["taxonomy"],
        "summary": "Event type dictionary (slug + ro/en label + color + icon)",
        "operationId": "listTypes",
        "responses": {
          "200": { "description": "Types", "content": { "application/json": { "schema": { "type": "object", "properties": { "types": { "type": "array", "items": { "$ref": "#/components/schemas/Type" } } } } } } }
        }
      }
    },
    "/api/subtypes": {
      "get": {
        "tags": ["taxonomy"],
        "summary": "Subtype dictionary with translations",
        "operationId": "listSubtypes",
        "responses": {
          "200": { "description": "Subtypes", "content": { "application/json": { "schema": { "type": "object", "properties": { "subtypes": { "type": "array", "items": { "$ref": "#/components/schemas/Subtype" } } } } } } }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": ["agents"],
        "summary": "JSON-RPC over HTTP MCP endpoint",
        "operationId": "mcpRpc",
        "description": "Model Context Protocol server. Methods: `initialize`, `tools/list`, `tools/call`. Tools available: `list_events`, `get_event`, `list_types`, `list_subtypes`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": { "type": "string", "const": "2.0" },
                  "id": { "oneOf": [{ "type": "string" }, { "type": "integer" }, { "type": "null" }] },
                  "method": { "type": "string" },
                  "params": { "type": "object" }
                },
                "required": ["jsonrpc", "method"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "JSON-RPC response envelope", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/sitemap.xml": { "get": { "tags": ["agents"], "summary": "XML sitemap (urlset)", "responses": { "200": { "description": "Sitemap XML", "content": { "application/xml": {} } } } } },
    "/feed.xml":    { "get": { "tags": ["agents"], "summary": "RSS 2.0 feed of upcoming events", "responses": { "200": { "description": "RSS XML", "content": { "application/rss+xml": {} } } } } }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "description": "Static ingest token provisioned out-of-band to the scheduled scraper." }
    },
    "schemas": {
      "Event": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "eventType": { "type": "string" },
          "subtypes": { "type": "array", "items": { "type": "string" } },
          "ageCategory": { "type": "string", "nullable": true, "enum": ["0-2", "3-5", "6-12", "13-17", "18+", "all", null] },
          "startsAt": { "type": "integer", "description": "Unix seconds" },
          "endsAt": { "type": "integer", "nullable": true, "description": "Unix seconds" },
          "venue": { "type": "string", "nullable": true },
          "organizer": { "type": "string", "nullable": true },
          "address": { "type": "string", "nullable": true },
          "city": { "type": "string", "nullable": true },
          "url": { "type": "string", "format": "uri", "nullable": true },
          "imageUrl": { "type": "string", "format": "uri", "nullable": true },
          "priceMin": { "type": "number", "nullable": true },
          "priceMax": { "type": "number", "nullable": true },
          "isFavorite": { "type": "boolean" }
        },
        "required": ["id", "title", "eventType", "subtypes", "startsAt", "isFavorite"]
      },
      "EventInput": {
        "type": "object",
        "properties": {
          "title": { "type": "string" },
          "eventType": { "type": "string" },
          "eventSubtype": { "type": "string", "description": "Legacy single subtype (deprecated, use eventSubtypes)" },
          "eventSubtypes": { "type": "array", "items": { "type": "string" } },
          "startsAt": { "oneOf": [{ "type": "string", "format": "date-time" }, { "type": "integer" }] },
          "endsAt": { "oneOf": [{ "type": "string", "format": "date-time" }, { "type": "integer" }] },
          "description": { "type": "string" },
          "ageCategory": { "type": "string" },
          "venue": { "type": "string" },
          "organizer": { "type": "string" },
          "address": { "type": "string" },
          "city": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "imageUrl": { "type": "string", "format": "uri" },
          "priceMin": { "type": "number" },
          "priceMax": { "type": "number" },
          "source": { "type": "string" },
          "externalId": { "type": "string" }
        },
        "required": ["title", "eventType", "startsAt"]
      },
      "Type": {
        "type": "object",
        "properties": {
          "slug": { "type": "string" },
          "ro": { "type": "string" },
          "en": { "type": "string" },
          "color": { "type": "string" },
          "icon": { "type": "string" },
          "position": { "type": "integer" }
        }
      },
      "Subtype": {
        "type": "object",
        "properties": {
          "type_slug": { "type": "string" },
          "slug": { "type": "string" },
          "ro": { "type": "string", "nullable": true },
          "en": { "type": "string", "nullable": true }
        }
      },
      "Facets": {
        "type": "object",
        "properties": {
          "types": { "type": "array", "items": { "type": "string" } },
          "total": { "type": "integer" },
          "lastUpdated": { "type": "integer", "nullable": true },
          "global": { "$ref": "#/components/schemas/FacetsBucket" },
          "byType": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/FacetsBucket" } }
        }
      },
      "FacetsBucket": {
        "type": "object",
        "properties": {
          "subtypes": { "type": "array", "items": { "type": "string" } },
          "venues": { "type": "array", "items": { "type": "string" } },
          "organizers": { "type": "array", "items": { "type": "string" } },
          "sources": { "type": "array", "items": { "type": "string" } },
          "ages": { "type": "array", "items": { "type": "string" } },
          "hasFree": { "type": "boolean" },
          "count": { "type": "integer" }
        }
      }
    }
  }
}
