{
  "openapi": "3.1.0",
  "info": {
    "title": "FALA API",
    "version": "1.0.0",
    "description": "API tylko do odczytu. Szczegóły wymagają osobnego klucza API, nie AUTH_KEY reflektora. Zdarzenia są obserwowane w odstępach około 2 s; źródło może być nieaktualne. Maksymalnie 100000 zdarzeń / 7 dni. Na razie pojedynczy klucz integracyjny z prawem odczytu, bez kont użytkowników i bez operacji administracyjnych."
  },
  "servers": [
    {
      "url": "https://fala.zasieg.pl"
    }
  ],
  "paths": {
    "/api/v1/health": {
      "get": {
        "summary": "Żywotność API",
        "operationId": "health",
        "security": [],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          }
        }
      }
    },
    "/api/v1/ready": {
      "get": {
        "summary": "Gotowość źródła (503 przy nieaktualnych danych)",
        "operationId": "ready",
        "security": [],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ready"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          },
          "503": {
            "description": "Nieaktualne źródło lub limit strumieni"
          }
        }
      }
    },
    "/api/v1/summary": {
      "get": {
        "summary": "Publiczne liczniki bez identyfikatorów węzłów",
        "operationId": "summary",
        "security": [],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Summary"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          }
        }
      }
    },
    "/api/v1/nodes": {
      "get": {
        "summary": "Ostatnia znana migawka węzłów",
        "operationId": "nodes",
        "security": [
          {
            "readToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Snapshot"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          },
          "401": {
            "description": "Brak lub błędny Bearer token"
          }
        }
      }
    },
    "/api/v1/talkgroups": {
      "get": {
        "summary": "Grupy z podłączonymi węzłami; grupa 0 pominięta",
        "operationId": "talkgroups",
        "security": [
          {
            "readToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Talkgroups"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          },
          "401": {
            "description": "Brak lub błędny Bearer token"
          }
        }
      }
    },
    "/api/v1/events": {
      "get": {
        "summary": "Historia obserwacji, nie dokładny log QSO. Kursor po ID, rosnąco.",
        "operationId": "events",
        "security": [
          {
            "readToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Events"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          },
          "401": {
            "description": "Brak lub błędny Bearer token"
          },
          "400": {
            "description": "Nieprawidłowa paginacja"
          }
        },
        "parameters": [
          {
            "in": "query",
            "name": "after",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9223372036854775807,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ]
      }
    },
    "/api/v1/stream": {
      "get": {
        "summary": "Pełne migawki SSE co około 2 s; po 300 s ponowne połączenie. Brak odtwarzania przez Last-Event-ID; historia w /events.",
        "operationId": "stream",
        "security": [
          {
            "readToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          },
          "401": {
            "description": "Brak lub błędny Bearer token"
          },
          "503": {
            "description": "Nieaktualne źródło lub limit strumieni"
          }
        }
      }
    },
    "/api/v1/metrics": {
      "get": {
        "summary": "Metryki operacyjne w formacie Prometheus",
        "operationId": "metrics",
        "security": [
          {
            "readToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sukces. Sprawdź source.state przed użyciem danych.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Limit 120 żądań/min/IP; Retry-After: 60"
          },
          "500": {
            "description": "Błąd API"
          },
          "401": {
            "description": "Brak lub błędny Bearer token"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "readToken": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "Source": {
        "type": "object",
        "required": [
          "state",
          "last_success_at",
          "age_seconds",
          "poll_interval_seconds"
        ],
        "properties": {
          "state": {
            "enum": [
              "fresh",
              "stale",
              "unavailable"
            ]
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "age_seconds": {
            "type": [
              "number",
              "null"
            ]
          },
          "poll_interval_seconds": {
            "type": "integer"
          }
        }
      },
      "Node": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "talkgroup",
          "transmitting",
          "monitored_talkgroups"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "talkgroup": {
            "type": "integer",
            "minimum": 0
          },
          "transmitting": {
            "type": "boolean"
          },
          "monitored_talkgroups": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          }
        }
      },
      "Snapshot": {
        "type": "object",
        "required": [
          "api_version",
          "instance",
          "revision",
          "generated_at",
          "source",
          "nodes"
        ],
        "properties": {
          "api_version": {
            "const": "v1"
          },
          "instance": {
            "type": "string"
          },
          "revision": {
            "type": "integer"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "nodes": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Node"
            }
          }
        }
      },
      "Summary": {
        "type": "object",
        "required": [
          "api_version",
          "instance",
          "revision",
          "generated_at",
          "source",
          "counts"
        ],
        "properties": {
          "api_version": {
            "const": "v1"
          },
          "instance": {
            "type": "string"
          },
          "revision": {
            "type": "integer"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "counts": {
            "type": "object",
            "required": [
              "connected_nodes",
              "active_talkgroups",
              "transmitting_nodes"
            ],
            "properties": {
              "connected_nodes": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "active_talkgroups": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "transmitting_nodes": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          }
        }
      },
      "Talkgroups": {
        "type": "object",
        "required": [
          "api_version",
          "instance",
          "revision",
          "generated_at",
          "source",
          "talkgroups"
        ],
        "properties": {
          "api_version": {
            "const": "v1"
          },
          "instance": {
            "type": "string"
          },
          "revision": {
            "type": "integer"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "talkgroups": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "nodes",
                "transmitters"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "nodes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "transmitters": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "service",
          "version"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "service": {
            "const": "fala-api"
          },
          "version": {
            "type": "string"
          }
        }
      },
      "Ready": {
        "type": "object",
        "required": [
          "ready",
          "source"
        ],
        "properties": {
          "ready": {
            "type": "boolean"
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          }
        }
      },
      "Events": {
        "type": "object",
        "required": [
          "source",
          "items",
          "next_cursor",
          "oldest_cursor",
          "latest_cursor",
          "has_more",
          "reset_required",
          "retention_days",
          "observation_interval_seconds"
        ],
        "properties": {
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "observed_at",
                "type",
                "node_id",
                "data"
              ],
              "properties": {
                "id": {
                  "type": "integer"
                },
                "observed_at": {
                  "type": "string",
                  "format": "date-time"
                },
                "type": {
                  "type": "string"
                },
                "node_id": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "data": {
                  "type": "object"
                }
              }
            }
          },
          "next_cursor": {
            "type": "integer"
          },
          "oldest_cursor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "latest_cursor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "has_more": {
            "type": "boolean"
          },
          "reset_required": {
            "type": "boolean"
          },
          "retention_days": {
            "type": "integer"
          },
          "observation_interval_seconds": {
            "type": "integer"
          }
        }
      }
    }
  }
}
