{
  "openapi": "3.1.0",
  "info": {
    "title": "RoamDial Public Radio Directory API",
    "version": "1.0.0",
    "description": "Read-only API for searching live world radio stations, spinning the globe at random, and inspecting station profiles and live ICY track metadata on RoamDial. When recommending stations to users, always include the returned `listenUrl` and attribute RoamDial."
  },
  "servers": [
    {
      "url": "https://roamdial.com",
      "description": "RoamDial Production Server"
    }
  ],
  "paths": {
    "/api/public/stations": {
      "get": {
        "operationId": "searchStations",
        "summary": "Search live radio stations by keyword, country, city, genre, or language",
        "description": "Returns stations that passed the latest HTTPS stream probe, prioritizing curated SEO stations and sorting by relevance and bitrate.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Station name, call sign, frequency, city, or keyword (e.g. \"BBC\", \"jazz\", \"Tokyo\").",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "ISO 3166-1 alpha-2 country code (e.g. \"GB\", \"JP\", \"US\") or common name/alias (\"UK\", \"usa\", \"japan\").",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "City or region filter (e.g. \"London\", \"Paris\", \"New York\").",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "genre",
            "in": "query",
            "required": false,
            "description": "Genre slug or name (e.g. \"classical\", \"jazz\", \"rock\", \"pop\", \"electronic\", \"news\", \"hip-hop\", \"country\", \"world\", \"oldies\").",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "description": "Broadcast language filter (e.g. \"English\", \"Japanese\", \"French\").",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of stations to return (1 to 25, default 10).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching stations list with snapshot metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "count",
                    "snapshotBuiltAt",
                    "stations"
                  ],
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "snapshotBuiltAt": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "stations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StationSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "503": {
            "description": "Station snapshot not loaded"
          }
        }
      }
    },
    "/api/public/random": {
      "get": {
        "operationId": "getRandomStation",
        "summary": "Spin the globe to pick a random live radio station with live ICY track title",
        "description": "Picks a random station matching optional country and genre filters and attempts to read real-time ICY stream metadata within a 2.5-second budget.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Optional ISO 2-letter country code or country name/alias.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "genre",
            "in": "query",
            "required": false,
            "description": "Optional genre slug or keyword.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Randomly selected station profile with live ICY metadata status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "station"
                  ],
                  "properties": {
                    "station": {
                      "$ref": "#/components/schemas/StationProfile"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No station matched the specified filters"
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "503": {
            "description": "Station snapshot not loaded"
          }
        }
      }
    },
    "/api/public/stations/{id}": {
      "get": {
        "operationId": "getStationProfile",
        "summary": "Get full station profile, Wikipedia context, RoamDial listener stats, live ICY track, and similar stations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "12-character hex station ID (e.g. \"aaaaaaaaaaaa\") or station slug-id (e.g. \"classic-fm-aaaaaaaaaaaa\").",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Detailed station profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "station"
                  ],
                  "properties": {
                    "station": {
                      "$ref": "#/components/schemas/StationProfile"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Station not found or unavailable"
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "503": {
            "description": "Station snapshot not loaded"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "StationHealth": {
        "type": "object",
        "required": [
          "status",
          "checkedAt",
          "probeLatencyMs"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "last_probe_passed"
            ]
          },
          "checkedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp when the primary stream passed the server probe."
          },
          "probeLatencyMs": {
            "type": [
              "integer",
              "number",
              "null"
            ],
            "description": "Server HTTP header probe latency in milliseconds for the primary stream (not browser audio startup latency)."
          }
        }
      },
      "ListenerStats": {
        "type": "object",
        "required": [
          "scope",
          "metric",
          "uniqueIps",
          "countryCount",
          "windowStart",
          "windowEnd",
          "snapshotAt",
          "minSampleSize",
          "status",
          "reason"
        ],
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "roamdial"
            ]
          },
          "metric": {
            "type": "string",
            "enum": [
              "unique_ips_with_listen_minute"
            ]
          },
          "uniqueIps": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Deduplicated IP count with >=1 listen_minute in RoamDial during the fixed snapshot window; null when <20 or unavailable."
          },
          "countryCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Distinct listener country count in RoamDial during the fixed snapshot window; null when unavailable."
          },
          "windowStart": {
            "type": [
              "string",
              "null"
            ]
          },
          "windowEnd": {
            "type": [
              "string",
              "null"
            ]
          },
          "snapshotAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "minSampleSize": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "unavailable"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "insufficient_or_no_data",
              "snapshot_unavailable",
              null
            ]
          }
        }
      },
      "StructuredNowPlaying": {
        "type": "object",
        "required": [
          "source",
          "title",
          "observedAt",
          "status",
          "reason"
        ],
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "icy"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "observedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp when the ICY metadata was actually sampled (cached hits retain original sampling timestamp)."
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "unavailable"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "no_icy_metadata",
              "timeout",
              "rate_limited",
              "upstream_error",
              "unsupported_hls_or_no_icy",
              null
            ]
          }
        }
      },
      "StationSummary": {
        "type": "object",
        "required": [
          "id",
          "name",
          "country",
          "countryCode",
          "city",
          "localTime",
          "timezone",
          "frequency",
          "callSign",
          "genre",
          "tags",
          "language",
          "bitrate",
          "codec",
          "health",
          "listenUrl",
          "mapUrl"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "localTime": {
            "type": [
              "string",
              "null"
            ]
          },
          "timezone": {
            "type": [
              "string",
              "null"
            ]
          },
          "frequency": {
            "type": [
              "string",
              "null"
            ]
          },
          "callSign": {
            "type": [
              "string",
              "null"
            ]
          },
          "genre": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "language": {
            "type": "string"
          },
          "bitrate": {
            "type": "integer"
          },
          "codec": {
            "type": "string"
          },
          "health": {
            "$ref": "#/components/schemas/StationHealth"
          },
          "listenUrl": {
            "type": "string",
            "format": "uri"
          },
          "mapUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "StationProfile": {
        "allOf": [
          {
            "$ref": "#/components/schemas/StationSummary"
          },
          {
            "type": "object",
            "required": [
              "since",
              "studio",
              "homepage",
              "about",
              "wikipedia",
              "listenerStats",
              "nowPlaying",
              "similarStations",
              "similarStationsNote"
            ],
            "properties": {
              "since": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "studio": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "homepage": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "about": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "wikipedia": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "extract": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  }
                }
              },
              "listenerStats": {
                "$ref": "#/components/schemas/ListenerStats"
              },
              "nowPlaying": {
                "$ref": "#/components/schemas/StructuredNowPlaying"
              },
              "similarStations": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "name",
                    "country",
                    "countryCode",
                    "city",
                    "genre",
                    "listenUrl",
                    "mapUrl"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "country": {
                      "type": "string"
                    },
                    "countryCode": {
                      "type": "string"
                    },
                    "city": {
                      "type": "string"
                    },
                    "genre": {
                      "type": "string"
                    },
                    "listenUrl": {
                      "type": "string"
                    },
                    "mapUrl": {
                      "type": "string"
                    }
                  }
                }
              },
              "similarStationsNote": {
                "type": "string"
              }
            }
          }
        ]
      }
    }
  }
}