{
  "openapi": "3.1.0",
  "info": {
    "title": "Static Radio public API",
    "version": "1.0.0",
    "summary": "The public data for building your own Static Radio player.",
    "description": "Everything a third-party player needs to play Static live: the schedule (with its bulletins) and listener counts. Start with the guide at https://staticradio.app/developers, which explains the timeline rules and the ground rules (stream audio straight from each show's host, play bulletins as scheduled, keep it live). No key is needed. Please send a User-Agent that names your app, such as YourApp-Static/1.0.",
    "contact": {
      "name": "Static Radio",
      "url": "https://staticradio.app/developers",
      "email": "hello@staticradio.app"
    },
    "termsOfService": "https://staticradio.app/developers#ground-rules"
  },
  "externalDocs": {
    "description": "Developer guide",
    "url": "https://staticradio.app/developers"
  },
  "servers": [
    {
      "url": "https://staticradio.app",
      "description": "Website and API"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Schedule",
      "description": "What plays when, on every station."
    },
    {
      "name": "Listeners",
      "description": "Live listener counts."
    }
  ],
  "paths": {
    "/schedule-wear.json": {
      "servers": [
        {
          "url": "https://data.staticradio.app",
          "description": "Schedule and art"
        }
      ],
      "get": {
        "operationId": "getSchedule",
        "tags": [
          "Schedule"
        ],
        "summary": "Get the schedule",
        "description": "Every station's loop of episodes for the current broadcast day. Rebuilt at least once a day. Revalidate with If-None-Match (a 304 means your copy is current) no more than every 15 minutes. In a browser, don't add If-None-Match yourself: it triggers a CORS preflight that this host refuses, and the browser's own cache revalidates anyway. The format is described by the JSON Schema at https://staticradio.app/schemas/schedule-wear.v8.json.",
        "parameters": [
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "The ETag from your last copy.",
            "schema": {
              "type": "string"
            },
            "example": "W/\"e2cc6d5aa94a4f501bd88aaa6701afe0\""
          }
        ],
        "responses": {
          "200": {
            "description": "The schedule (served gzipped).",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "description": "public, max-age=14400, must-revalidate",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "schemas/schedule-wear.v8.json"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the ETag you sent.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "403": {
            "description": "Refused. Some generic user agents (Python's default, for one) are blocked; send your own User-Agent."
          }
        }
      }
    },
    "/api/counts": {
      "get": {
        "operationId": "getCounts",
        "tags": [
          "Listeners"
        ],
        "summary": "Get every station's listener count",
        "description": "How many people are listening to each station right now. Listeners are counted through our official apps and ports. Refreshed every few seconds; polling once a minute is plenty.",
        "responses": {
          "200": {
            "description": "Counts by station id. null when a count isn't known.",
            "headers": {
              "Cache-Control": {
                "description": "public, max-age=5",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Counts"
                },
                "example": {
                  "counts": {
                    "nature_01": 0,
                    "work_01": 2,
                    "learn_01": 2,
                    "news_01": 0,
                    "weird_01": 1,
                    "unite_01": 3,
                    "make_01": 0,
                    "sheila_01": 1,
                    "sleep_01": 0
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/counts/{stationId}": {
      "get": {
        "operationId": "getStationCount",
        "tags": [
          "Listeners"
        ],
        "summary": "Get one station's listener count",
        "description": "The bare number for one station, or null when it isn't known (including for an id that isn't a station).",
        "parameters": [
          {
            "name": "stationId",
            "in": "path",
            "required": true,
            "description": "A station id from the schedule.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9_]{1,40}$"
            },
            "example": "news_01"
          }
        ],
        "responses": {
          "200": {
            "description": "The count.",
            "headers": {
              "Cache-Control": {
                "description": "public, max-age=5",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "minimum": 0
                },
                "example": 3
              }
            }
          },
          "400": {
            "description": "Not a valid station id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "bad channel"
                }
              }
            }
          },
          "502": {
            "description": "The count couldn't be read just now. Try again later."
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "ETag": {
        "description": "Send it back as If-None-Match to get a 304 when nothing changed.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Counts": {
        "type": "object",
        "required": [
          "counts"
        ],
        "properties": {
          "counts": {
            "type": "object",
            "description": "Listeners by station id.",
            "additionalProperties": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0
            }
          }
        }
      }
    }
  }
}
