{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "SDRX · contrato público v1",
    "version": "1.0.0",
    "description": "Contrato versionado de consultas GET en los endpoints existentes. No requiere cuenta ni token. Campos adicionales son compatibles; los clientes deben tolerarlos. Salud HTTP no certifica recepción RF/audio. Los ejemplos de escritura de telemetría están en el SDK separado."
  },
  "servers": [
    {
      "url": "https://sdr.j8.cl"
    }
  ],
  "security": [],
  "paths": {
    "/api/stations.json": {
      "get": {
        "operationId": "listPublicStations",
        "parameters": [
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Cantidad máxima por respuesta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaco, firmado, ligado a filtros y tamaño. Usa pagination.next antes de 30 minutos.",
            "schema": {
              "type": "string",
              "maxLength": 1024
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Filtro público; los textos que exceden el límite se truncan.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Filtro público; los textos que exceden el límite se truncan.",
            "schema": {
              "type": "string",
              "maxLength": 2
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Filtro público; los textos que exceden el límite se truncan.",
            "schema": {
              "type": "string",
              "maxLength": 24
            }
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "Filtro público; los textos que exceden el límite se truncan.",
            "schema": {
              "type": "string",
              "maxLength": 24
            }
          },
          {
            "name": "band",
            "in": "query",
            "required": false,
            "description": "Filtro público; los textos que exceden el límite se truncan.",
            "schema": {
              "type": "string",
              "maxLength": 30
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Codifica + como %2B.",
            "schema": {
              "type": "string",
              "enum": [
                "openwebrx",
                "openwebrx+",
                "kiwisdr",
                "modified"
              ]
            }
          },
          {
            "name": "available",
            "in": "query",
            "required": false,
            "description": "Exige una banda habilitada con telemetría vigente y salud web reciente.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "mhz",
            "in": "query",
            "required": false,
            "description": "Frecuencia MHz dentro de una banda habilitada. Entradas inválidas/no finitas/fuera de rango se ignoran.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Consulta correcta",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Directory"
                }
              }
            }
          },
          "400": {
            "description": "Parámetros o enlace de continuación inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Estación inexistente/no publicada o método no documentado; la respuesta puede ser HTML."
          }
        }
      }
    },
    "/api/locations/": {
      "get": {
        "operationId": "listAdministrativeLocations",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": true,
            "description": "Código del catálogo, por ejemplo mx.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z]{2}$"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Sin estado devuelve estados; con estado devuelve divisiones locales.",
            "schema": {
              "type": "string",
              "maxLength": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Consulta correcta",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Locations"
                }
              }
            }
          },
          "400": {
            "description": "Parámetros o enlace de continuación inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health/": {
      "get": {
        "operationId": "portalHealth",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Consulta correcta",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceHealth"
                }
              }
            }
          }
        }
      }
    },
    "/api/station/{station_id}/health-history/": {
      "get": {
        "operationId": "publicHttpHistory",
        "parameters": [
          {
            "name": "station_id",
            "in": "path",
            "required": true,
            "description": "UUID obtenido del directorio público.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "integer",
              "enum": [
                7,
                30,
                90
              ],
              "default": 7
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Consulta correcta",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/History"
                }
              }
            }
          },
          "400": {
            "description": "Parámetros o enlace de continuación inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Estación inexistente/no publicada o método no documentado; la respuesta puede ser HTML."
          },
          "405": {
            "description": "Estación inexistente/no publicada o método no documentado; la respuesta puede ser HTML."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": true
      },
      "Attribution": {
        "type": "object",
        "properties": {
          "username": {
            "type": "string"
          },
          "uid": {
            "type": "integer",
            "minimum": 1
          },
          "veteran": {
            "type": "boolean"
          }
        },
        "required": [
          "username",
          "uid",
          "veteran"
        ],
        "additionalProperties": true
      },
      "Passport": {
        "type": "object",
        "properties": {
          "hardware": {
            "type": "string"
          },
          "antenna": {
            "type": "string"
          },
          "software_version": {
            "type": "string"
          },
          "usage_conditions": {
            "type": "string"
          },
          "source": {
            "const": "operator_declared"
          }
        },
        "required": [
          "hardware",
          "antenna",
          "software_version",
          "usage_conditions",
          "source"
        ],
        "additionalProperties": true
      },
      "ScheduleWindow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "weekdays": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 7
            },
            "minItems": 1,
            "maxItems": 7,
            "uniqueItems": true
          },
          "timezone": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "pattern": "^(?:[01][0-9]|2[0-3]):[0-5][0-9]$"
          },
          "end": {
            "type": "string",
            "pattern": "^(?:[01][0-9]|2[0-3]):[0-5][0-9]$"
          },
          "overnight": {
            "type": "boolean"
          },
          "full_day": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "weekdays",
          "timezone",
          "start",
          "end",
          "overnight",
          "full_day"
        ],
        "additionalProperties": true
      },
      "Schedule": {
        "type": "object",
        "properties": {
          "source": {
            "const": "operator_schedule"
          },
          "state": {
            "enum": [
              "declared",
              "unspecified",
              "invalid"
            ]
          },
          "planned_now": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "evaluated_at": {
            "type": "string",
            "format": "date-time"
          },
          "windows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScheduleWindow"
            },
            "maxItems": 8
          }
        },
        "required": [
          "source",
          "state",
          "planned_now",
          "windows"
        ],
        "additionalProperties": true
      },
      "Maintenance": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "estimated_duration_minutes": {
            "type": "integer",
            "minimum": 0
          },
          "state": {
            "enum": [
              "scheduled",
              "in_progress"
            ]
          },
          "source": {
            "const": "operator_announcement"
          }
        },
        "required": [
          "id",
          "title",
          "description",
          "starts_at",
          "ends_at",
          "estimated_duration_minutes",
          "state",
          "source"
        ],
        "additionalProperties": true
      },
      "Band": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "low_hz": {
            "type": "integer",
            "minimum": 0
          },
          "high_hz": {
            "type": "integer",
            "minimum": 0
          },
          "enabled": {
            "type": "boolean"
          },
          "schedule": {
            "$ref": "#/components/schemas/Schedule"
          },
          "available": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "observed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "fresh": {
            "type": "boolean"
          },
          "expires_in_seconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 300
          },
          "state": {
            "enum": [
              "disabled",
              "unverified",
              "future",
              "stale",
              "receiver_unverified",
              "available",
              "unavailable"
            ]
          }
        },
        "required": [
          "name",
          "low_hz",
          "high_hz",
          "enabled",
          "schedule",
          "available",
          "observed_at",
          "expires_at",
          "fresh",
          "expires_in_seconds",
          "state"
        ],
        "additionalProperties": true
      },
      "Endpoint": {
        "type": "object",
        "properties": {
          "ready": {
            "type": "boolean"
          },
          "url": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri-reference"
              },
              {
                "type": "null"
              }
            ]
          },
          "checked_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "ready",
          "url",
          "checked_at"
        ],
        "additionalProperties": true
      },
      "Alias": {
        "type": "object",
        "properties": {
          "hostname": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri-reference"
          }
        },
        "required": [
          "hostname",
          "url"
        ],
        "additionalProperties": true
      },
      "Station": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "shared_by": {
            "$ref": "#/components/schemas/Attribution"
          },
          "title": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "kind": {
            "enum": [
              "openwebrx",
              "openwebrx+",
              "kiwisdr",
              "modified"
            ]
          },
          "maintenance": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Maintenance"
            }
          },
          "modes": {
            "type": "array",
            "items": {
              "enum": [
                "am",
                "fm",
                "wfm",
                "usb",
                "lsb",
                "cw",
                "drm",
                "digital",
                "other"
              ]
            },
            "uniqueItems": true
          },
          "modes_source": {
            "const": "operator_declared"
          },
          "passport": {
            "$ref": "#/components/schemas/Passport"
          },
          "health": {
            "enum": [
              "up",
              "down",
              "unknown"
            ]
          },
          "health_scope": {
            "const": "http"
          },
          "health_history_url": {
            "type": "string",
            "format": "uri-reference"
          },
          "observed_health": {
            "type": "string"
          },
          "checked_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "health_fresh": {
            "type": "boolean"
          },
          "host": {
            "type": "string"
          },
          "endpoint": {
            "$ref": "#/components/schemas/Endpoint"
          },
          "custom_domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Alias"
            }
          },
          "latitude": {
            "anyOf": [
              {
                "type": "number",
                "minimum": -90,
                "maximum": 90
              },
              {
                "type": "null"
              }
            ]
          },
          "longitude": {
            "anyOf": [
              {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              {
                "type": "null"
              }
            ]
          },
          "url": {
            "type": "string",
            "format": "uri-reference"
          },
          "capture": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri-reference"
              },
              {
                "type": "null"
              }
            ]
          },
          "bands": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Band"
            }
          }
        },
        "required": [
          "id",
          "shared_by",
          "title",
          "country",
          "state",
          "city",
          "kind",
          "maintenance",
          "modes",
          "modes_source",
          "passport",
          "health",
          "health_scope",
          "health_history_url",
          "observed_health",
          "checked_at",
          "health_fresh",
          "host",
          "endpoint",
          "custom_domains",
          "latitude",
          "longitude",
          "url",
          "capture",
          "bands"
        ],
        "additionalProperties": true
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "page_size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "returned": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "next": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri-reference"
              },
              {
                "type": "null"
              }
            ]
          },
          "order": {
            "const": "id"
          },
          "snapshot_at": {
            "type": "string",
            "format": "date-time"
          },
          "link_expires_in_seconds": {
            "const": 1800
          }
        },
        "required": [
          "page_size",
          "returned",
          "total",
          "next",
          "order",
          "snapshot_at",
          "link_expires_in_seconds"
        ],
        "additionalProperties": true
      },
      "Directory": {
        "type": "object",
        "properties": {
          "stations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Station"
            },
            "maxItems": 100
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        },
        "required": [
          "stations",
          "generated_at",
          "pagination"
        ],
        "additionalProperties": true
      },
      "Locations": {
        "type": "object",
        "properties": {
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              },
              "required": [
                "value",
                "label"
              ],
              "additionalProperties": true
            }
          }
        },
        "required": [
          "choices"
        ],
        "additionalProperties": true
      },
      "ServiceHealth": {
        "type": "object",
        "properties": {
          "service": {
            "const": "sdrx"
          },
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "service",
          "status",
          "version"
        ],
        "additionalProperties": true
      },
      "HistorySummary": {
        "type": "object",
        "properties": {
          "successful_checks": {
            "type": "integer",
            "minimum": 0
          },
          "failed_checks": {
            "type": "integer",
            "minimum": 0
          },
          "checks": {
            "type": "integer",
            "minimum": 0
          },
          "successful_checks_percent": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0,
                "maximum": 100
              },
              {
                "type": "null"
              }
            ]
          },
          "mean_latency_ms": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0,
                "maximum": 60000
              },
              {
                "type": "null"
              }
            ]
          },
          "observed_hours": {
            "type": "integer",
            "minimum": 0
          },
          "unknown_hours": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "successful_checks",
          "failed_checks",
          "checks",
          "successful_checks_percent",
          "mean_latency_ms",
          "observed_hours",
          "unknown_hours"
        ],
        "additionalProperties": true
      },
      "HistoryHour": {
        "type": "object",
        "properties": {
          "hour": {
            "type": "string",
            "format": "date-time"
          },
          "successful_checks": {
            "type": "integer",
            "minimum": 0
          },
          "failed_checks": {
            "type": "integer",
            "minimum": 0
          },
          "checks": {
            "type": "integer",
            "minimum": 0
          },
          "successful_checks_percent": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0,
                "maximum": 100
              },
              {
                "type": "null"
              }
            ]
          },
          "mean_latency_ms": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0,
                "maximum": 60000
              },
              {
                "type": "null"
              }
            ]
          },
          "latency_count": {
            "type": "integer",
            "minimum": 0
          },
          "latency_sum": {
            "type": "integer",
            "minimum": 0
          },
          "first_observed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "last_observed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "mixed_station_revisions": {
            "type": "boolean"
          }
        },
        "required": [
          "hour",
          "successful_checks",
          "failed_checks",
          "checks",
          "successful_checks_percent",
          "mean_latency_ms",
          "latency_count",
          "latency_sum",
          "first_observed_at",
          "last_observed_at",
          "mixed_station_revisions"
        ],
        "additionalProperties": true
      },
      "History": {
        "type": "object",
        "properties": {
          "station_id": {
            "type": "string",
            "format": "uuid"
          },
          "scope": {
            "const": "http"
          },
          "metric": {
            "const": "successful_checks_fraction"
          },
          "is_time_weighted_uptime": {
            "const": false
          },
          "sampling": {
            "const": "periodic_and_manual"
          },
          "current_bucket_partial": {
            "const": true
          },
          "latency_scope": {
            "const": "responses_with_measured_duration"
          },
          "retention_days": {
            "const": 90
          },
          "bucket_seconds": {
            "const": 3600
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "days": {
            "enum": [
              7,
              30,
              90
            ]
          },
          "summary": {
            "$ref": "#/components/schemas/HistorySummary"
          },
          "series": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HistoryHour"
            },
            "minItems": 168,
            "maxItems": 2160
          }
        },
        "required": [
          "station_id",
          "scope",
          "metric",
          "is_time_weighted_uptime",
          "sampling",
          "current_bucket_partial",
          "latency_scope",
          "retention_days",
          "bucket_seconds",
          "generated_at",
          "start",
          "end",
          "days",
          "summary",
          "series"
        ],
        "additionalProperties": true
      }
    }
  }
}
