{
 "openapi": "3.1.0",
 "info": {
  "title": "Community Radio API",
  "version": "v1",
  "summary": "The public read API of the Tahoe Backcountry Radio program and its peers",
  "description": "The Community Radio API's read side: public, no key, one program per address, the website one client of it like any other. Every answer carries Access-Control-Allow-Origin: *, so a static site or a map app can read it from any origin.\n\nThe Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.\n\nThe write addresses under /api/v1/programs/<program>/ (installs, usage, page-views, check-ins, subscribe) are what the site's own pages and the public website post, not public reads, and are not listed here.",
  "license": {
   "name": "CC BY 4.0",
   "identifier": "CC-BY-4.0"
  }
 },
 "externalDocs": {
  "description": "docs/api.md, the prose",
  "url": "https://github.com/snowyogi/tbr-site/blob/main/docs/api.md"
 },
 "servers": [
  {
   "url": "https://tbradio.org"
  }
 ],
 "tags": [
  {
   "name": "Read API"
  },
  {
   "name": "Publishing feeds"
  }
 ],
 "paths": {
  "/api/v1/programs.json": {
   "get": {
    "summary": "Programs",
    "description": "The directory of programs on this server, the one address that names no program: every program with its slug, name, the organization that runs it, its region in words, its licence (CC BY 4.0) and the attribution a client credits it with (<name>, via the Community Radio API, CC BY 4.0), whether it is a sample (demo) and, for one, the sentence on where its data comes from (source; null for a program with none), its latest stand-up where one is recorded (the date in PT, whether the files were a spreadsheet or a CalTopo export, and the stand-up clock's minutes), and its addresses, absolute on this server: zones, sign locations, channels, events and the three publishing feeds. The programs people use come first, the samples after. No changed_since: the list is small.",
    "operationId": "programs",
    "tags": [
     "Read API"
    ],
    "parameters": [],
    "responses": {
     "200": {
      "description": "generated_at and programs; each program: slug, name, organization, region, licence, attribution, demo, source (null for a program with none), stood_up (date, source, minutes; null until one is recorded), addresses (zones, sign_locations, channels, events, zones_feed, zones_gpx, sign_locations_feed). Content-Type application/json, cached for five minutes.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=300"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Programs"
        },
        "examples": {
         "programs": {
          "summary": "A real answer for this server: the TBR program",
          "value": {
           "generated_at": "2026-10-17T05:27:32.891729Z",
           "programs": [
            {
             "slug": "tbr",
             "name": "Tahoe Backcountry Radio",
             "organization": "Tahoe Backcountry Ski Patrol",
             "region": "Lake Tahoe, California and Nevada",
             "licence": "CC BY 4.0",
             "attribution": "Tahoe Backcountry Radio, via the Community Radio API, CC BY 4.0",
             "demo": false,
             "source": null,
             "stood_up": {
              "date": "2026-10-16",
              "source": "spreadsheet",
              "minutes": 12
             },
             "addresses": {
              "zones": "https://tbradio.org/api/v1/programs/tbr/zones.geojson",
              "sign_locations": "https://tbradio.org/api/v1/programs/tbr/sign-locations.geojson",
              "channels": "https://tbradio.org/api/v1/programs/tbr/channels.json",
              "conditions": "https://tbradio.org/api/v1/programs/tbr/conditions.json",
              "events": "https://tbradio.org/api/v1/programs/tbr/events.json",
              "zones_feed": "https://tbradio.org/feeds/tbr/zones.geojson",
              "zones_gpx": "https://tbradio.org/feeds/tbr/zones.gpx",
              "sign_locations_feed": "https://tbradio.org/feeds/tbr/sign-locations.geojson"
             }
            }
           ]
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/programs/{program}/sources/{file}": {
   "get": {
    "summary": "Source files",
    "description": "The files the program was stood up from, as committed in its folder on this server: program.json, the program's facts; zones.csv and trailheads.csv, the two sheet tabs; zones.json, the CalTopo export; redirection.csv, the Redirection export of its short links; stand-up.json, the stand-up clock's records. Those six names and no other: any other name is a 404, as is a program with no folder and a file the folder lacks (the last two are optional). Nothing in them is a person. Tahoe Backcountry Radio's set is the worked example on the Bring your program panel: what a program sends, file by file.",
    "operationId": "sources",
    "tags": [
     "Read API"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "file",
      "in": "path",
      "required": true,
      "description": "Which of the program's committed files: one of the six names the address serves, no other. A file the program's folder lacks (redirection.csv, stand-up.json are optional) is a 404.",
      "schema": {
       "type": "string",
       "enum": [
        "program.json",
        "zones.csv",
        "trailheads.csv",
        "zones.json",
        "redirection.csv",
        "stand-up.json"
       ]
      },
      "example": "program.json"
     }
    ],
    "responses": {
     "200": {
      "description": "The file as committed, byte for byte. Content-Type application/json for program.json and stand-up.json; text/csv for zones.csv, trailheads.csv and redirection.csv; application/geo+json for zones.json; cached for five minutes.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=300"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "description": "The committed JSON file: the facts, or the stand-up records"
        }
       },
       "text/csv": {
        "schema": {
         "type": "string",
         "description": "The committed sheet tab or export as CSV"
        }
       },
       "application/geo+json": {
        "schema": {
         "description": "The committed CalTopo export, a GeoJSON FeatureCollection"
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug, no folder of files for it, or a file its folder lacks",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/programs/{program}/zones.geojson": {
   "get": {
    "summary": "Zones",
    "description": "The program's zones: a GeoJSON FeatureCollection, one feature per zone (a Polygon, or a MultiPolygon for a zone drawn in two parts; null geometry for a zone whose outline is not yet drawn) with its slug, name, colour, community channel and privacy code, the boundary in words, and its area (slug, name, channel and privacy code) or null for a zone in no area. An area is a named group of zones with a channel and privacy code shared inside it, as the Front Range program draws its map; Tahoe Backcountry Radio's zones belong to none. Beside the features, areas: the program's areas, each with its slug, name, channel, privacy code and outline (Polygon, MultiPolygon or null), every one whatever changed_since says. After the zones, the program's sign locations as Point features, one per trailhead or hut that has a position, with its slug, name, kind, zone (null outside every zone) and whether the trailhead is established, and no sign states: those are the Sign locations address's. So the collection holds both, and one file is the whole import for a map app. With changed_since, only the zones changed after that time (a zone counts as changed when its area changed) and the sign locations that changed (a sign's state is no change here), and a new generated_at to send next time; nothing changed is an empty features list, not an error. A zone removed is never reported as a change: the import never removes a row, and a client that needs to notice removals reads the whole collection now and then. Every changed-since read looks two minutes back of the time sent, because a row's time is stamped before it becomes visible; a feature served twice costs nothing, since a client keeping a copy replaces a feature by its slug. In the Zones answer a zone and a sign location can share a slug (Rubicon is both), so the copy keeps the Points apart from the zones.",
    "operationId": "zones",
    "tags": [
     "Read API"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "changed_since",
      "in": "query",
      "required": false,
      "description": "An ISO 8601 UTC time, the generated_at of an earlier answer sent back to get only what changed after it. Every read looks two minutes back of the time sent, because a row's time is stamped before it becomes visible; a feature may therefore be served again, and a client keeping a copy replaces a feature by its slug (in the Zones answer a zone and a sign location can share a slug, so it keeps the Points apart). Send the Z form, the one generated_at uses (2026-10-15T00:30:15.250000Z): an offset such as +00:00 must be URL-encoded, since a bare + in a query string is read as a space; a time with no Z and no offset is read as UTC. Anything that is not a time is a 400.",
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "example": "2026-10-15T00:30:15.250000Z"
     }
    ],
    "responses": {
     "200": {
      "description": "generated_at, features and areas; each feature's properties: slug, name, colour, colour_hex, channel, privacy_code, boundary, area, updated_at; each sign location's point: slug, name, kind, zone, established, updated_at; each area: slug, name, channel, privacy_code, outline. Content-Type application/geo+json, cached for five minutes.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=300"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      },
      "content": {
       "application/geo+json": {
        "schema": {
         "$ref": "#/components/schemas/ZonesCollection"
        },
        "examples": {
         "zones": {
          "summary": "A real answer for the TBR program, cut to one zone and one sign location",
          "value": {
           "type": "FeatureCollection",
           "generated_at": "2026-10-17T04:17:13.153486Z",
           "features": [
            {
             "type": "Feature",
             "geometry": {
              "type": "Polygon",
              "coordinates": [
               [
                [
                 -120.23880600929262,
                 39.24498357143696
                ],
                [
                 -120.24142384529115,
                 39.24342147595056
                ],
                [
                 -120.2451252937317,
                 39.242839835764286
                ],
                [
                 -120.2465844154358,
                 39.24207538704161
                ],
                [
                 -120.24933099746706,
                 39.24215847969763
                ],
                [
                 -120.2537512779236,
                 39.240646177952094
                ],
                [
                 -120.25590777397157,
                 39.23980691951004
                ],
                [
                 -120.25923371315004,
                 39.23889286423981
                ],
                [
                 -120.26134729385377,
                 39.23938313172922
                ],
                [
                 -120.26350378990175,
                 39.238602025974444
                ],
                [
                 -120.26527404785158,
                 39.237106267278556
                ],
                [
                 -120.2663040161133,
                 39.23685697106219
                ],
                [
                 -120.26943683624269,
                 39.23443044161825
                ],
                [
                 -120.2722477912903,
                 39.236707392907185
                ],
                [
                 -120.27387857437135,
                 39.237272464264045
                ],
                [
                 -120.27563810348512,
                 39.238203160106984
                ],
                [
                 -120.27714014053346,
                 39.23986508626054
                ],
                [
                 -120.27804136276247,
                 39.241377404845096
                ],
                [
                 -120.27883529663087,
                 39.243089110720405
                ],
                [
                 -120.28048753738405,
                 39.24471768530033
                ],
                [
                 -120.28275936841966,
                 39.244603437033504
                ],
                [
                 -120.28498291969301,
                 39.244784156928965
                ],
                [
                 -120.27989208698274,
                 39.247526056715564
                ],
                [
                 -120.27832031250001,
                 39.251032208742814
                ],
                [
                 -120.2739429473877,
                 39.253773864256814
                ],
                [
                 -120.27175426483156,
                 39.25423910425051
                ],
                [
                 -120.27081012725831,
                 39.25521942127452
                ],
                [
                 -120.26490926742555,
                 39.25543542148791
                ],
                [
                 -120.26297807693483,
                 39.25651541257324
                ],
                [
                 -120.25808572769166,
                 39.25993804364213
                ],
                [
                 -120.2448892593384,
                 39.26223078070044
                ],
                [
                 -120.23617744445802,
                 39.26111764961037
                ],
                [
                 -120.23208975791933,
                 39.259265160924514
                ],
                [
                 -120.22997617721559,
                 39.259871586377216
                ],
                [
                 -120.22849023342134,
                 39.25868365213873
                ],
                [
                 -120.22640347480775,
                 39.258160290107426
                ],
                [
                 -120.22248744964601,
                 39.25791106871928
                ],
                [
                 -120.21819591522218,
                 39.25821013427874
                ],
                [
                 -120.21516501903535,
                 39.257645231595426
                ],
                [
                 -120.21170496940614,
                 39.257645231595426
                ],
                [
                 -120.21161645650865,
                 39.25561195962971
                ],
                [
                 -120.21581411361696,
                 39.25528588294952
                ],
                [
                 -120.21775603294374,
                 39.25393171388641
                ],
                [
                 -120.21935462951662,
                 39.25226181295798
                ],
                [
                 -120.22171497344972,
                 39.25109867438677
                ],
                [
                 -120.22242307662965,
                 39.250085066471485
                ],
                [
                 -120.22800207138063,
                 39.2482239777278
                ],
                [
                 -120.23057699203493,
                 39.24624651682127
                ],
                [
                 -120.23362398147584,
                 39.24634622206725
                ],
                [
                 -120.23703575134279,
                 39.24544886975068
                ],
                [
                 -120.23880600929262,
                 39.24498357143696
                ]
               ]
              ]
             },
             "properties": {
              "slug": "deep-creek",
              "name": "Deep Creek",
              "colour": "red",
              "colour_hex": "#FF0000",
              "channel": 16,
              "privacy_code": 30,
              "boundary": "",
              "area": null,
              "updated_at": "2026-10-17T04:14:51.441440Z"
             }
            },
            {
             "type": "Feature",
             "geometry": {
              "type": "Point",
              "coordinates": [
               -120.21192,
               39.25682
              ]
             },
             "properties": {
              "slug": "deep-creek",
              "name": "Deep Creek",
              "kind": "trailhead",
              "zone": "deep-creek",
              "established": true,
              "updated_at": "2026-10-23T06:10:50.324778Z"
             }
            }
           ],
           "areas": []
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "changed_since is not a time",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/programs/{program}/sign-locations.geojson": {
   "get": {
    "summary": "Sign locations",
    "description": "The program's sign locations: Point features (null geometry for one not yet placed) with kind (trailhead or hut), zone, whether the trailhead is established, the checkpoint's sign state (the state of the trailhead sign bolted to it, support CP; null where no checkpoint stands, a planned one included), and the trailhead signs: their count by sign state, and each with its colour code, support (POLE, POST or CP), number and sign state. With changed_since, only the sign locations changed after that time: a sign location counts as changed when it changed or when any of its trailhead signs did (a sign put up, damaged, missing, or down for the summer), and each feature's updated_at is the later of those. A sign location removed is never reported as a change. Every changed-since read looks two minutes back of the time sent, because a row's time is stamped before it becomes visible; a feature served twice costs nothing, since a client keeping a copy replaces a feature by its slug. In the Zones answer a zone and a sign location can share a slug (Rubicon is both), so the copy keeps the Points apart from the zones.",
    "operationId": "sign-locations",
    "tags": [
     "Read API"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "changed_since",
      "in": "query",
      "required": false,
      "description": "An ISO 8601 UTC time, the generated_at of an earlier answer sent back to get only what changed after it. Every read looks two minutes back of the time sent, because a row's time is stamped before it becomes visible; a feature may therefore be served again, and a client keeping a copy replaces a feature by its slug (in the Zones answer a zone and a sign location can share a slug, so it keeps the Points apart). Send the Z form, the one generated_at uses (2026-10-15T00:30:15.250000Z): an offset such as +00:00 must be URL-encoded, since a bare + in a query string is read as a space; a time with no Z and no offset is read as UTC. Anything that is not a time is a 400.",
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "example": "2026-10-15T00:30:15.250000Z"
     }
    ],
    "responses": {
     "200": {
      "description": "generated_at and features; each feature's properties: slug, name, kind, zone, established, checkpoint, signs (count, states, and list: each sign's sign_type, support, number and state), updated_at. Content-Type application/geo+json, cached for five minutes.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=300"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      },
      "content": {
       "application/geo+json": {
        "schema": {
         "$ref": "#/components/schemas/SignLocationsCollection"
        },
        "examples": {
         "sign-locations": {
          "summary": "A real answer for the TBR program, cut to one feature",
          "value": {
           "type": "FeatureCollection",
           "generated_at": "2026-10-17T04:17:13.170388Z",
           "features": [
            {
             "type": "Feature",
             "geometry": {
              "type": "Point",
              "coordinates": [
               -120.34825,
               39.34176
              ]
             },
             "properties": {
              "slug": "castle-peak",
              "name": "Castle Peak",
              "kind": "trailhead",
              "zone": "donner-summit-north",
              "established": true,
              "updated_at": "2026-10-17T04:14:51.585694Z",
              "checkpoint": "installed",
              "signs": {
               "count": 1,
               "states": {
                "installed": 1
               },
               "list": [
                {
                 "sign_type": "RD",
                 "support": "CP",
                 "number": 1,
                 "state": "installed"
                }
               ]
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "changed_since is not a time",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/programs/{program}/channels.json": {
   "get": {
    "summary": "Channels",
    "description": "The sign colours with the community channel each fixes, and the out-of-zone channel for anywhere outside every zone (null when the program has none). A program with no signs, as one stood up from its map alone, publishes no colours (sign_colours is empty): its map's colours fix no channel, and each zone's own channel, in the zones answer, is the one to read. With changed_since after the program's channels last changed (less the same two minutes), the answer is generated_at alone.",
    "operationId": "channels",
    "tags": [
     "Read API"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "changed_since",
      "in": "query",
      "required": false,
      "description": "An ISO 8601 UTC time, the generated_at of an earlier answer sent back to get only what changed after it. Every read looks two minutes back of the time sent, because a row's time is stamped before it becomes visible; a feature may therefore be served again, and a client keeping a copy replaces a feature by its slug (in the Zones answer a zone and a sign location can share a slug, so it keeps the Points apart). Send the Z form, the one generated_at uses (2026-10-15T00:30:15.250000Z): an offset such as +00:00 must be URL-encoded, since a bare + in a query string is read as a space; a time with no Z and no offset is read as UTC. Anything that is not a time is a 400.",
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "example": "2026-10-15T00:30:15.250000Z"
     }
    ],
    "responses": {
     "200": {
      "description": "sign_colours (code, name, hex, channel, privacy_code), out_of_zone_channel (channel, privacy_code, or null) and generated_at. Content-Type application/json, cached for five minutes.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=300"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "$ref": "#/components/schemas/Channels"
          },
          {
           "$ref": "#/components/schemas/ChannelsUnchanged"
          }
         ]
        },
        "examples": {
         "channels": {
          "summary": "A real answer for the TBR program, cut to one feature",
          "value": {
           "sign_colours": [
            {
             "code": "RD",
             "name": "red",
             "hex": "#FF0000",
             "channel": 16,
             "privacy_code": 30
            },
            {
             "code": "GN",
             "name": "green",
             "hex": "#008000",
             "channel": 18,
             "privacy_code": 30
            },
            {
             "code": "BL",
             "name": "blue",
             "hex": "#009AFF",
             "channel": 20,
             "privacy_code": 30
            },
            {
             "code": "VT",
             "name": "violet",
             "hex": "#800080",
             "channel": 22,
             "privacy_code": 30
            }
           ],
           "out_of_zone_channel": {
            "channel": 5,
            "privacy_code": 30
           },
           "generated_at": "2026-10-17T04:17:13.188921Z"
          }
         },
         "channels-unchanged": {
          "summary": "Nothing changed since the time sent",
          "value": {
           "generated_at": "2026-10-17T04:17:13.188921Z"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "changed_since is not a time",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/programs/{program}/conditions.json": {
   "get": {
    "summary": "Trailhead conditions",
    "description": "Every trailhead (sign location) that names its weather stations or its avalanche forecast zone, with what each shows: up high, crest wind and temperature and snow depth; down low, snow; each reading with its station, its source and its time, stale when older than three hours, and the station's backup standing in when it has no recent reading. Then each forecast zone's forecast from the avalanche center, with its danger by elevation band, bottom line and avalanche problems, each problem's location the center's own list. ?demo=1 gives every zone the demo forecast, the center's real forecast of Feb 21, 2026, and every station its reading from that morning, as committed (none where the source keeps none). No changed_since: readings change by the hour. Nothing here is a person.",
    "operationId": "conditions",
    "tags": [
     "Read API"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     }
    ],
    "responses": {
     "200": {
      "description": "generated_at, demo, out_of_zone, trailheads and forecasts; each trailhead: slug, name, latitude, longitude, zone, forecast, up_high (wind, snow) and down_low. Content-Type application/json, cached for five minutes.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=300"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Conditions"
        },
        "examples": {
         "conditions": {
          "summary": "A real answer for the TBR program, cut to one feature",
          "value": {
           "generated_at": "2026-10-24T06:13:16.985006Z",
           "demo": true,
           "out_of_zone": {
            "channel": 5,
            "privacy_code": 30
           },
           "trailheads": [
            {
             "slug": "rubicon",
             "name": "Rubicon",
             "latitude": 39.00958,
             "longitude": -120.12403,
             "zone": {
              "slug": "rubicon",
              "name": "Rubicon",
              "colour": "BL",
              "channel": 20,
              "privacy_code": 30
             },
             "forecast": "central-sierra-nevada",
             "up_high": {
              "wind": {
               "station": {
                "code": "SIBSV",
                "name": "OV-Siberia",
                "elevation_ft": 8700,
                "link": "https://www.sierraavalanchecenter.org/weather/stations/map#/station-table/SIBSV"
               },
               "backup_for": null,
               "state": "stale",
               "source": "NOAA (weather.gov)",
               "read_at": "2026-10-22T13:15:00+00:00",
               "read": "6:15 AM PT Thu 22 Oct",
               "temperature_f": null,
               "wind_mph": null,
               "gust_mph": 18.7,
               "wind_from_deg": null,
               "depth_in": null
              },
              "snow": {
               "station": {
                "code": "BMTSV",
                "name": "OV-Belmont",
                "elevation_ft": 8000,
                "link": "https://www.sierraavalanchecenter.org/weather/stations/map#/station-table/BMTSV"
               },
               "backup_for": null,
               "state": "stale",
               "source": "NOAA (weather.gov)",
               "read_at": "2026-10-22T13:15:00+00:00",
               "read": "6:15 AM PT Thu 22 Oct",
               "temperature_f": null,
               "wind_mph": null,
               "gust_mph": 16.4,
               "wind_from_deg": null,
               "depth_in": null
              }
             },
             "down_low": {
              "station": {
               "code": "724",
               "name": "Rubicon #2",
               "elevation_ft": 7560,
               "link": "https://wcc.sc.egov.usda.gov/nwcc/site?sitenum=724"
              },
              "backup_for": null,
              "state": "recent",
              "source": "NRCS SNOTEL",
              "read_at": "2026-10-24T05:00:00+00:00",
              "read": "10:00 PM PT Fri 23 Oct",
              "temperature_f": 34.5,
              "wind_mph": null,
              "gust_mph": null,
              "wind_from_deg": null,
              "depth_in": 0.0
             }
            }
           ],
           "forecasts": {
            "central-sierra-nevada": {
             "state": "current",
             "demo": true,
             "zone": "Central Sierra Nevada",
             "center": "Sierra Avalanche Center",
             "center_link": "https://www.sierraavalanchecenter.org/",
             "link": "https://www.sierraavalanchecenter.org/forecasts/avalanche/central-sierra-nevada",
             "issued_at": "2026-02-21T14:50:00+00:00",
             "expires_at": "2026-02-22T12:00:00+00:00",
             "danger": {
              "upper": 3,
              "middle": 3,
              "lower": 3
             },
             "bottom_line": [
              "Ridgetop winds are blowing and drifting snow, forming new wind slabs in the areas that fill in with drifted snow. Weak layers deep in the snowpack have continued to show reactivity in some areas. Consider avoiding avalanche terrain in areas where new wind slabs exist and/or near areas where you have triggered any snowpack collapses or audible whumpfing."
             ],
             "problems": [
              {
               "rank": 1,
               "name": "Wind Slab",
               "likelihood": "likely",
               "size": [
                "1",
                "2"
               ],
               "location": [
                "northwest middle",
                "northwest upper",
                "north upper",
                "north middle",
                "northeast middle",
                "northeast upper",
                "east upper",
                "east middle",
                "southeast middle",
                "southeast upper"
               ],
               "icon": "https://api.avalanche.org/img/avalanche_problems/WindSlab.png"
              },
              {
               "rank": 2,
               "name": "Persistent Slab",
               "likelihood": "possible",
               "size": [
                "2",
                "3"
               ],
               "location": [
                "north upper",
                "northeast upper",
                "east upper",
                "southeast upper",
                "south upper",
                "southwest upper",
                "west upper",
                "northwest upper",
                "north middle",
                "northeast middle",
                "east middle",
                "southeast middle",
                "south middle",
                "southwest middle",
                "west middle",
                "northwest middle",
                "north lower",
                "northeast lower",
                "east lower",
                "southeast lower",
                "south lower",
                "southwest lower",
                "west lower",
                "northwest lower"
               ],
               "icon": "https://api.avalanche.org/img/avalanche_problems/PersistentSlab.png"
              }
             ]
            }
           }
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/programs/{program}/events.json": {
   "get": {
    "summary": "Events",
    "description": "The program's published events not over yet, soonest first, or with from every one not over by that day, past ones included: each with its slug, title, activity (slug and name), start and end times, whether it is all day, its status (scheduled or cancelled; a cancelled event stays on the list), the event organizer's name and link, the place in words and a description. No people: not the organizing contact, not the number of places, not who signed up. With changed_since, only the events changed after that time (an event counts as changed when it changed or when its activity did, renamed say, and each event's updated_at is the later of those), then each event unpublished or deleted since as its slug, updated_at and published: false, so a client keeping a copy drops it. Times are the event's start and end in UTC; a client shows them in Pacific time (PT). An all-day event runs from midnight to midnight PT of its days.",
    "operationId": "events",
    "tags": [
     "Read API"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "changed_since",
      "in": "query",
      "required": false,
      "description": "An ISO 8601 UTC time, the generated_at of an earlier answer sent back to get only what changed after it. Every read looks two minutes back of the time sent, because a row's time is stamped before it becomes visible; a feature may therefore be served again, and a client keeping a copy replaces a feature by its slug (in the Zones answer a zone and a sign location can share a slug, so it keeps the Points apart). Send the Z form, the one generated_at uses (2026-10-15T00:30:15.250000Z): an offset such as +00:00 must be URL-encoded, since a bare + in a query string is read as a space; a time with no Z and no offset is read as UTC. Anything that is not a time is a 400.",
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "example": "2026-10-15T00:30:15.250000Z"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "A day in Pacific time (PT), such as 2026-08-01, to get every event not over by that day's start, past ones included: the season so far and to come. Without it, the events not over yet. Anything that is not a date is a 400.",
      "schema": {
       "type": "string",
       "format": "date"
      },
      "example": "2026-08-01"
     }
    ],
    "responses": {
     "200": {
      "description": "generated_at and events; each event: slug, title, activity, activity_name, starts_at, ends_at, all_day, status, organizer_name, organizer_url, place, description, published, updated_at; each removal: slug, updated_at, published. Content-Type application/json, cached for five minutes.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=300"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Events"
        },
        "examples": {
         "events": {
          "summary": "A real answer for the TBR program, cut to one feature",
          "value": {
           "generated_at": "2026-10-17T04:17:13.211827Z",
           "events": [
            {
             "slug": "sugar-bowl-info-table-2026-12-19",
             "title": "Info table at the Sugar Bowl trailhead",
             "activity": "info-table",
             "activity_name": "Info table",
             "starts_at": "2026-12-19T17:00:00Z",
             "ends_at": "2026-12-19T21:00:00Z",
             "all_day": false,
             "status": "scheduled",
             "organizer_name": "",
             "organizer_url": null,
             "place": "Donner Summit, the Sugar Bowl lot",
             "description": "Trade hazard reports, pick up a sticker, learn the community channel.",
             "published": true,
             "updated_at": "2026-10-17T04:17:13.113969Z"
            }
           ]
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "changed_since is not a time, or from is not a date",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "/feeds/{program}/zones.geojson": {
   "get": {
    "summary": "Zones feed",
    "description": "The publishing feed of the zones: a static GeoJSON file, byte for byte what the Zones address answered when the feed was last regenerated, with the same generated_at inside, for a map app that wants a file and no API at all. It is regenerated on the next request after the program's data changed, never on a clock, so it is never older than the data. Cached for an hour, with an ETag; a request with If-None-Match is answered 304 while the file stands. The feed does not take changed_since; a client wanting only changes reads the live address. A program's feed is rendered on its first request; a program that does not exist is a 404.",
    "operationId": "zones-feed",
    "tags": [
     "Publishing feeds"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "If-None-Match",
      "in": "header",
      "required": false,
      "description": "The ETag of the feed as last received; while the file stands the answer is 304 with no body, and the ETag again.",
      "schema": {
       "type": "string"
      },
      "example": "\"21515e88c8d162426a4ec3607fe728f3abf95038d8d85e1e2762bb22d0b76d27\""
     }
    ],
    "responses": {
     "200": {
      "description": "The Zones answer as a file: generated_at, features (the zones, then the sign locations' points) and areas. Content-Type application/geo+json, ETag, cached for an hour.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=3600"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       },
       "ETag": {
        "schema": {
         "type": "string"
        },
        "description": "Strong, the SHA-256 of the body; send it back as If-None-Match"
       }
      },
      "content": {
       "application/geo+json": {
        "schema": {
         "$ref": "#/components/schemas/ZonesCollection"
        },
        "examples": {
         "zones": {
          "summary": "A real answer for the TBR program, cut to one zone and one sign location",
          "value": {
           "type": "FeatureCollection",
           "generated_at": "2026-10-17T04:17:13.153486Z",
           "features": [
            {
             "type": "Feature",
             "geometry": {
              "type": "Polygon",
              "coordinates": [
               [
                [
                 -120.23880600929262,
                 39.24498357143696
                ],
                [
                 -120.24142384529115,
                 39.24342147595056
                ],
                [
                 -120.2451252937317,
                 39.242839835764286
                ],
                [
                 -120.2465844154358,
                 39.24207538704161
                ],
                [
                 -120.24933099746706,
                 39.24215847969763
                ],
                [
                 -120.2537512779236,
                 39.240646177952094
                ],
                [
                 -120.25590777397157,
                 39.23980691951004
                ],
                [
                 -120.25923371315004,
                 39.23889286423981
                ],
                [
                 -120.26134729385377,
                 39.23938313172922
                ],
                [
                 -120.26350378990175,
                 39.238602025974444
                ],
                [
                 -120.26527404785158,
                 39.237106267278556
                ],
                [
                 -120.2663040161133,
                 39.23685697106219
                ],
                [
                 -120.26943683624269,
                 39.23443044161825
                ],
                [
                 -120.2722477912903,
                 39.236707392907185
                ],
                [
                 -120.27387857437135,
                 39.237272464264045
                ],
                [
                 -120.27563810348512,
                 39.238203160106984
                ],
                [
                 -120.27714014053346,
                 39.23986508626054
                ],
                [
                 -120.27804136276247,
                 39.241377404845096
                ],
                [
                 -120.27883529663087,
                 39.243089110720405
                ],
                [
                 -120.28048753738405,
                 39.24471768530033
                ],
                [
                 -120.28275936841966,
                 39.244603437033504
                ],
                [
                 -120.28498291969301,
                 39.244784156928965
                ],
                [
                 -120.27989208698274,
                 39.247526056715564
                ],
                [
                 -120.27832031250001,
                 39.251032208742814
                ],
                [
                 -120.2739429473877,
                 39.253773864256814
                ],
                [
                 -120.27175426483156,
                 39.25423910425051
                ],
                [
                 -120.27081012725831,
                 39.25521942127452
                ],
                [
                 -120.26490926742555,
                 39.25543542148791
                ],
                [
                 -120.26297807693483,
                 39.25651541257324
                ],
                [
                 -120.25808572769166,
                 39.25993804364213
                ],
                [
                 -120.2448892593384,
                 39.26223078070044
                ],
                [
                 -120.23617744445802,
                 39.26111764961037
                ],
                [
                 -120.23208975791933,
                 39.259265160924514
                ],
                [
                 -120.22997617721559,
                 39.259871586377216
                ],
                [
                 -120.22849023342134,
                 39.25868365213873
                ],
                [
                 -120.22640347480775,
                 39.258160290107426
                ],
                [
                 -120.22248744964601,
                 39.25791106871928
                ],
                [
                 -120.21819591522218,
                 39.25821013427874
                ],
                [
                 -120.21516501903535,
                 39.257645231595426
                ],
                [
                 -120.21170496940614,
                 39.257645231595426
                ],
                [
                 -120.21161645650865,
                 39.25561195962971
                ],
                [
                 -120.21581411361696,
                 39.25528588294952
                ],
                [
                 -120.21775603294374,
                 39.25393171388641
                ],
                [
                 -120.21935462951662,
                 39.25226181295798
                ],
                [
                 -120.22171497344972,
                 39.25109867438677
                ],
                [
                 -120.22242307662965,
                 39.250085066471485
                ],
                [
                 -120.22800207138063,
                 39.2482239777278
                ],
                [
                 -120.23057699203493,
                 39.24624651682127
                ],
                [
                 -120.23362398147584,
                 39.24634622206725
                ],
                [
                 -120.23703575134279,
                 39.24544886975068
                ],
                [
                 -120.23880600929262,
                 39.24498357143696
                ]
               ]
              ]
             },
             "properties": {
              "slug": "deep-creek",
              "name": "Deep Creek",
              "colour": "red",
              "colour_hex": "#FF0000",
              "channel": 16,
              "privacy_code": 30,
              "boundary": "",
              "area": null,
              "updated_at": "2026-10-17T04:14:51.441440Z"
             }
            },
            {
             "type": "Feature",
             "geometry": {
              "type": "Point",
              "coordinates": [
               -120.21192,
               39.25682
              ]
             },
             "properties": {
              "slug": "deep-creek",
              "name": "Deep Creek",
              "kind": "trailhead",
              "zone": "deep-creek",
              "established": true,
              "updated_at": "2026-10-23T06:10:50.324778Z"
             }
            }
           ],
           "areas": []
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "304": {
      "description": "Unchanged since the ETag sent as If-None-Match; no body",
      "headers": {
       "ETag": {
        "schema": {
         "type": "string"
        }
       },
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=3600"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      }
     }
    }
   }
  },
  "/feeds/{program}/zones.gpx": {
   "get": {
    "summary": "Zones GPX feed",
    "description": "The publishing feed of the zones as GPX 1.1, for a GPS unit or a phone map app that imports GPX without converting: one track per zone, named with the zone and its community channel and privacy code (Rubicon 20-30), the zone's outline as the track's points, and the file's time the generated_at of the Zones answer it was rendered from. A zone whose outline is not yet drawn has no track. Before the tracks, one waypoint per trailhead and hut that has a position, named with the sign location and its zone's channel and privacy code (Rubicon 20-30), or with the program's out-of-zone channel for one outside every zone (5-30), its type trailhead or hut. Kept and regenerated as the GeoJSON feeds are: on the next request after the program's zones or sign locations changed, never on a clock. Cached for an hour, with an ETag; a request with If-None-Match is answered 304 while the file stands. A program's feed is rendered on its first request; a program that does not exist is a 404.",
    "operationId": "zones-gpx",
    "tags": [
     "Publishing feeds"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "If-None-Match",
      "in": "header",
      "required": false,
      "description": "The ETag of the feed as last received; while the file stands the answer is 304 with no body, and the ETag again.",
      "schema": {
       "type": "string"
      },
      "example": "\"21515e88c8d162426a4ec3607fe728f3abf95038d8d85e1e2762bb22d0b76d27\""
     }
    ],
    "responses": {
     "200": {
      "description": "The zones as a GPX file: metadata (the program's name, the attribution, the time), one wpt per sign location, its name and type, and one trk per zone, its name and its trkpts. Content-Type application/gpx+xml, ETag, cached for an hour.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=3600"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       },
       "ETag": {
        "schema": {
         "type": "string"
        },
        "description": "Strong, the SHA-256 of the body; send it back as If-None-Match"
       }
      },
      "content": {
       "application/gpx+xml": {
        "schema": {
         "$ref": "#/components/schemas/ZonesGpx"
        },
        "examples": {
         "zones-gpx": {
          "summary": "A real answer for the TBR program, cut to one waypoint and one track",
          "value": "<?xml version='1.0' encoding='UTF-8'?>\n<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" version=\"1.1\" creator=\"Community Radio API\">\n  <metadata>\n    <name>Tahoe Backcountry Radio zones</name>\n    <desc>One track per zone, named with the zone and its community channel and privacy code (channel-code); one waypoint per trailhead and hut, named with its zone's channel and code. Tahoe Backcountry Radio, via the Community Radio API, CC BY 4.0</desc>\n    <time>2026-10-20T23:21:11.969362Z</time>\n  </metadata>\n  <wpt lat=\"39.113280\" lon=\"-120.159240\">\n    <name>Blackwood Canyon SNO-Park 16-30</name>\n    <type>trailhead</type>\n  </wpt>\n  <trk>\n    <name>Blackwood Canyon 16-30</name>\n    <trkseg>\n      <trkpt lat=\"39.113494\" lon=\"-120.158041\" />\n      <trkpt lat=\"39.113066\" lon=\"-120.160208\" />\n      <trkpt lat=\"39.112200\" lon=\"-120.162225\" />\n      <trkpt lat=\"39.111467\" lon=\"-120.164628\" />\n      <trkpt lat=\"39.111326\" lon=\"-120.171548\" />\n      <trkpt lat=\"39.089553\" lon=\"-120.171409\" />\n      <trkpt lat=\"39.087888\" lon=\"-120.171237\" />\n      <trkpt lat=\"39.087895\" lon=\"-120.171240\" />\n      <trkpt lat=\"39.087823\" lon=\"-120.171498\" />\n      <trkpt lat=\"39.087835\" lon=\"-120.171501\" />\n      <trkpt lat=\"39.087810\" lon=\"-120.171640\" />\n      <trkpt lat=\"39.087810\" lon=\"-120.171640\" />\n      <trkpt lat=\"39.087690\" lon=\"-120.172190\" />\n      <trkpt lat=\"39.087610\" lon=\"-120.172710\" />\n      <trkpt lat=\"39.087610\" lon=\"-120.172710\" />\n      <trkpt lat=\"39.087500\" lon=\"-120.173350\" />\n      <trkpt lat=\"39.087240\" lon=\"-120.174420\" />\n      <trkpt lat=\"39.087090\" lon=\"-120.174860\" />\n      <trkpt lat=\"39.087090\" lon=\"-120.174860\" />\n      <trkpt lat=\"39.086880\" lon=\"-120.175580\" />\n      <trkpt lat=\"39.086880\" lon=\"-120.175580\" />\n      <trkpt lat=\"39.086520\" lon=\"-120.176600\" />\n      <trkpt lat=\"39.086460\" lon=\"-120.176880\" />\n      <trkpt lat=\"39.086460\" lon=\"-120.176880\" />\n      <trkpt lat=\"39.086350\" lon=\"-120.177340\" />\n      <trkpt lat=\"39.086290\" lon=\"-120.177480\" />\n      <trkpt lat=\"39.086090\" lon=\"-120.177810\" />\n      <trkpt lat=\"39.085830\" lon=\"-120.178710\" />\n      <trkpt lat=\"39.085830\" lon=\"-120.178710\" />\n      <trkpt lat=\"39.085790\" lon=\"-120.178850\" />\n      <trkpt lat=\"39.085740\" lon=\"-120.179280\" />\n      <trkpt lat=\"39.085640\" lon=\"-120.179580\" />\n      <trkpt lat=\"39.085390\" lon=\"-120.180240\" />\n      <trkpt lat=\"39.085190\" lon=\"-120.180930\" />\n      <trkpt lat=\"39.085190\" lon=\"-120.180930\" />\n      <trkpt lat=\"39.084580\" lon=\"-120.182530\" />\n      <trkpt lat=\"39.084580\" lon=\"-120.182530\" />\n      <trkpt lat=\"39.084370\" lon=\"-120.183050\" />\n      <trkpt lat=\"39.084180\" lon=\"-120.183790\" />\n      <trkpt lat=\"39.084180\" lon=\"-120.183790\" />\n      <trkpt lat=\"39.083970\" lon=\"-120.184790\" />\n      <trkpt lat=\"39.083800\" lon=\"-120.185290\" />\n      <trkpt lat=\"39.083650\" lon=\"-120.185570\" />\n      <trkpt lat=\"39.083590\" lon=\"-120.185760\" />\n      <trkpt lat=\"39.083480\" lon=\"-120.186320\" />\n      <trkpt lat=\"39.083300\" lon=\"-120.186830\" />\n      <trkpt lat=\"39.082860\" lon=\"-120.187570\" />\n      <trkpt lat=\"39.082560\" lon=\"-120.188210\" />\n      <trkpt lat=\"39.082560\" lon=\"-120.188210\" />\n      <trkpt lat=\"39.082380\" lon=\"-120.188330\" />\n      <trkpt lat=\"39.082200\" lon=\"-120.188340\" />\n      <trkpt lat=\"39.081530\" lon=\"-120.188310\" />\n      <trkpt lat=\"39.081420\" lon=\"-120.188350\" />\n      <trkpt lat=\"39.081120\" lon=\"-120.188290\" />\n      <trkpt lat=\"39.080850\" lon=\"-120.188290\" />\n      <trkpt lat=\"39.079700\" lon=\"-120.188500\" />\n      <trkpt lat=\"39.079560\" lon=\"-120.188510\" />\n      <trkpt lat=\"39.079170\" lon=\"-120.188410\" />\n      <trkpt lat=\"39.078970\" lon=\"-120.188380\" />\n      <trkpt lat=\"39.078970\" lon=\"-120.188380\" />\n      <trkpt lat=\"39.078910\" lon=\"-120.188370\" />\n      <trkpt lat=\"39.078910\" lon=\"-120.188370\" />\n      <trkpt lat=\"39.077970\" lon=\"-120.188660\" />\n      <trkpt lat=\"39.077670\" lon=\"-120.188660\" />\n      <trkpt lat=\"39.077670\" lon=\"-120.188660\" />\n      <trkpt lat=\"39.077300\" lon=\"-120.188650\" />\n      <trkpt lat=\"39.077090\" lon=\"-120.188590\" />\n      <trkpt lat=\"39.076720\" lon=\"-120.188540\" />\n      <trkpt lat=\"39.076490\" lon=\"-120.188430\" />\n      <trkpt lat=\"39.076170\" lon=\"-120.188210\" />\n      <trkpt lat=\"39.075800\" lon=\"-120.188080\" />\n      <trkpt lat=\"39.075310\" lon=\"-120.187690\" />\n      <trkpt lat=\"39.075310\" lon=\"-120.187690\" />\n      <trkpt lat=\"39.074362\" lon=\"-120.189712\" />\n      <trkpt lat=\"39.070464\" lon=\"-120.192523\" />\n      <trkpt lat=\"39.069244\" lon=\"-120.195795\" />\n      <trkpt lat=\"39.068524\" lon=\"-120.198123\" />\n      <trkpt lat=\"39.068203\" lon=\"-120.197941\" />\n      <trkpt lat=\"39.066799\" lon=\"-120.197673\" />\n      <trkpt lat=\"39.065900\" lon=\"-120.198606\" />\n      <trkpt lat=\"39.064720\" lon=\"-120.200147\" />\n      <trkpt lat=\"39.064406\" lon=\"-120.200786\" />\n      <trkpt lat=\"39.064029\" lon=\"-120.201720\" />\n      <trkpt lat=\"39.063759\" lon=\"-120.202646\" />\n      <trkpt lat=\"39.063101\" lon=\"-120.204024\" />\n      <trkpt lat=\"39.061501\" lon=\"-120.206041\" />\n      <trkpt lat=\"39.061135\" lon=\"-120.207243\" />\n      <trkpt lat=\"39.061035\" lon=\"-120.209732\" />\n      <trkpt lat=\"39.061301\" lon=\"-120.211277\" />\n      <trkpt lat=\"39.061901\" lon=\"-120.213637\" />\n      <trkpt lat=\"39.063667\" lon=\"-120.217671\" />\n      <trkpt lat=\"39.063001\" lon=\"-120.219989\" />\n      <trkpt lat=\"39.063800\" lon=\"-120.222607\" />\n      <trkpt lat=\"39.063867\" lon=\"-120.225267\" />\n      <trkpt lat=\"39.066533\" lon=\"-120.231276\" />\n      <trkpt lat=\"39.067816\" lon=\"-120.230803\" />\n      <trkpt lat=\"39.070364\" lon=\"-120.231404\" />\n      <trkpt lat=\"39.072030\" lon=\"-120.230846\" />\n      <trkpt lat=\"39.074754\" lon=\"-120.231276\" />\n      <trkpt lat=\"39.076320\" lon=\"-120.232391\" />\n      <trkpt lat=\"39.077203\" lon=\"-120.235074\" />\n      <trkpt lat=\"39.079501\" lon=\"-120.239000\" />\n      <trkpt lat=\"39.079918\" lon=\"-120.241790\" />\n      <trkpt lat=\"39.083099\" lon=\"-120.247498\" />\n      <trkpt lat=\"39.082716\" lon=\"-120.250158\" />\n      <trkpt lat=\"39.083182\" lon=\"-120.251703\" />\n      <trkpt lat=\"39.084382\" lon=\"-120.252283\" />\n      <trkpt lat=\"39.087863\" lon=\"-120.251210\" />\n      <trkpt lat=\"39.088196\" lon=\"-120.250716\" />\n      <trkpt lat=\"39.090577\" lon=\"-120.250051\" />\n      <trkpt lat=\"39.093991\" lon=\"-120.247605\" />\n      <trkpt lat=\"39.095740\" lon=\"-120.248399\" />\n      <trkpt lat=\"39.097405\" lon=\"-120.248506\" />\n      <trkpt lat=\"39.099104\" lon=\"-120.248356\" />\n      <trkpt lat=\"39.099720\" lon=\"-120.247519\" />\n      <trkpt lat=\"39.100786\" lon=\"-120.244043\" />\n      <trkpt lat=\"39.101185\" lon=\"-120.241597\" />\n      <trkpt lat=\"39.101352\" lon=\"-120.240910\" />\n      <trkpt lat=\"39.103483\" lon=\"-120.237606\" />\n      <trkpt lat=\"39.104915\" lon=\"-120.236254\" />\n      <trkpt lat=\"39.105815\" lon=\"-120.236619\" />\n      <trkpt lat=\"39.107346\" lon=\"-120.236940\" />\n      <trkpt lat=\"39.109294\" lon=\"-120.236232\" />\n      <trkpt lat=\"39.110726\" lon=\"-120.235181\" />\n      <trkpt lat=\"39.112275\" lon=\"-120.233207\" />\n      <trkpt lat=\"39.112375\" lon=\"-120.231662\" />\n      <trkpt lat=\"39.111700\" lon=\"-120.228776\" />\n      <trkpt lat=\"39.113890\" lon=\"-120.221083\" />\n      <trkpt lat=\"39.115438\" lon=\"-120.217671\" />\n      <trkpt lat=\"39.117203\" lon=\"-120.215676\" />\n      <trkpt lat=\"39.118834\" lon=\"-120.214775\" />\n      <trkpt lat=\"39.118734\" lon=\"-120.213165\" />\n      <trkpt lat=\"39.119084\" lon=\"-120.211062\" />\n      <trkpt lat=\"39.119966\" lon=\"-120.209260\" />\n      <trkpt lat=\"39.122247\" lon=\"-120.205151\" />\n      <trkpt lat=\"39.123013\" lon=\"-120.203466\" />\n      <trkpt lat=\"39.123113\" lon=\"-120.200505\" />\n      <trkpt lat=\"39.125044\" lon=\"-120.198231\" />\n      <trkpt lat=\"39.127033\" lon=\"-120.195114\" />\n      <trkpt lat=\"39.122093\" lon=\"-120.190463\" />\n      <trkpt lat=\"39.121059\" lon=\"-120.187738\" />\n      <trkpt lat=\"39.120524\" lon=\"-120.187244\" />\n      <trkpt lat=\"39.120757\" lon=\"-120.181708\" />\n      <trkpt lat=\"39.119342\" lon=\"-120.171537\" />\n      <trkpt lat=\"39.118890\" lon=\"-120.171505\" />\n      <trkpt lat=\"39.118938\" lon=\"-120.163673\" />\n      <trkpt lat=\"39.116433\" lon=\"-120.162702\" />\n      <trkpt lat=\"39.116198\" lon=\"-120.163075\" />\n      <trkpt lat=\"39.115888\" lon=\"-120.163778\" />\n      <trkpt lat=\"39.115565\" lon=\"-120.164733\" />\n      <trkpt lat=\"39.115140\" lon=\"-120.164603\" />\n      <trkpt lat=\"39.115013\" lon=\"-120.165237\" />\n      <trkpt lat=\"39.114934\" lon=\"-120.165209\" />\n      <trkpt lat=\"39.115102\" lon=\"-120.164357\" />\n      <trkpt lat=\"39.114737\" lon=\"-120.164234\" />\n      <trkpt lat=\"39.114750\" lon=\"-120.158297\" />\n      <trkpt lat=\"39.114164\" lon=\"-120.158067\" />\n      <trkpt lat=\"39.113494\" lon=\"-120.158041\" />\n    </trkseg>\n  </trk>\n</gpx>"
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "304": {
      "description": "Unchanged since the ETag sent as If-None-Match; no body",
      "headers": {
       "ETag": {
        "schema": {
         "type": "string"
        }
       },
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=3600"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      }
     }
    }
   }
  },
  "/feeds/{program}/sign-locations.geojson": {
   "get": {
    "summary": "Sign locations feed",
    "description": "The publishing feed of the sign locations: a static GeoJSON file, byte for byte what the Sign locations address answered when the feed was last regenerated, with the same generated_at inside. Regenerated on the next request after the program's data changed (a trailhead sign's state included), never on a clock. Cached for an hour, with an ETag; a request with If-None-Match is answered 304 while the file stands. No changed_since here; a client wanting only changes reads the live address. A program's feed is rendered on its first request; a program that does not exist is a 404.",
    "operationId": "sign-locations-feed",
    "tags": [
     "Publishing feeds"
    ],
    "parameters": [
     {
      "name": "program",
      "in": "path",
      "required": true,
      "description": "The program's slug: tbr for Tahoe Backcountry Radio. Every address names the program; a program that does not exist is a 404. The Community Radio API serves many programs, and every address names the program: /api/v1/programs/<program>/... where <program> is the program's slug, tbr for Tahoe Backcountry Radio. There are no plain addresses: every address names the program. A program that does not exist is a 404. Each program's data is its own: zones, channels, sign locations, signs, events. A second program appears as its own slug with no change to a client. The directory at /api/v1/programs.json lists every program on this server with its addresses and the attribution a client credits it with.",
      "schema": {
       "type": "string"
      },
      "example": "tbr"
     },
     {
      "name": "If-None-Match",
      "in": "header",
      "required": false,
      "description": "The ETag of the feed as last received; while the file stands the answer is 304 with no body, and the ETag again.",
      "schema": {
       "type": "string"
      },
      "example": "\"21515e88c8d162426a4ec3607fe728f3abf95038d8d85e1e2762bb22d0b76d27\""
     }
    ],
    "responses": {
     "200": {
      "description": "The Sign locations answer as a file: generated_at and features. Content-Type application/geo+json, ETag, cached for an hour.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=3600"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       },
       "ETag": {
        "schema": {
         "type": "string"
        },
        "description": "Strong, the SHA-256 of the body; send it back as If-None-Match"
       }
      },
      "content": {
       "application/geo+json": {
        "schema": {
         "$ref": "#/components/schemas/SignLocationsCollection"
        },
        "examples": {
         "sign-locations": {
          "summary": "A real answer for the TBR program, cut to one feature",
          "value": {
           "type": "FeatureCollection",
           "generated_at": "2026-10-17T04:17:13.170388Z",
           "features": [
            {
             "type": "Feature",
             "geometry": {
              "type": "Point",
              "coordinates": [
               -120.34825,
               39.34176
              ]
             },
             "properties": {
              "slug": "castle-peak",
              "name": "Castle Peak",
              "kind": "trailhead",
              "zone": "donner-summit-north",
              "established": true,
              "updated_at": "2026-10-17T04:14:51.585694Z",
              "checkpoint": "installed",
              "signs": {
               "count": 1,
               "states": {
                "installed": 1
               },
               "list": [
                {
                 "sign_type": "RD",
                 "support": "CP",
                 "number": 1,
                 "state": "installed"
                }
               ]
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "No program of that slug",
      "content": {
       "text/plain": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "304": {
      "description": "Unchanged since the ETag sent as If-None-Match; no body",
      "headers": {
       "ETag": {
        "schema": {
         "type": "string"
        }
       },
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=3600"
       },
       "Access-Control-Allow-Origin": {
        "schema": {
         "type": "string"
        },
        "example": "*"
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "ZoneProperties": {
    "type": "object",
    "required": [
     "slug",
     "name",
     "colour",
     "colour_hex",
     "channel",
     "privacy_code",
     "boundary",
     "area",
     "updated_at"
    ],
    "properties": {
     "slug": {
      "type": "string",
      "description": "The zone's slug, stable across reads"
     },
     "name": {
      "type": "string"
     },
     "colour": {
      "type": "string",
      "description": "The sign colour's name (blue)"
     },
     "colour_hex": {
      "type": "string",
      "description": "The sign colour as #RRGGBB"
     },
     "channel": {
      "type": "integer",
      "description": "The zone's community channel"
     },
     "privacy_code": {
      "type": "integer"
     },
     "boundary": {
      "type": "string",
      "description": "The boundary in words"
     },
     "area": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/AreaOfZone"
       },
       {
        "type": "null"
       }
      ],
      "description": "The area the zone belongs to; null for a zone in no area"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "The later of the zone's change and its area's"
     }
    }
   },
   "AreaOfZone": {
    "type": "object",
    "description": "An area, as a zone names it: a named group of zones with a shared channel and privacy code",
    "required": [
     "slug",
     "name",
     "channel",
     "privacy_code"
    ],
    "properties": {
     "slug": {
      "type": "string",
      "description": "The area's slug, stable across reads"
     },
     "name": {
      "type": "string"
     },
     "channel": {
      "type": "integer",
      "description": "The channel shared inside the area"
     },
     "privacy_code": {
      "type": "integer"
     }
    }
   },
   "Area": {
    "description": "A named group of zones with a shared channel and privacy code, optionally an outline; a zone belongs to one area or none. The four properties a zone names (AreaOfZone) and the outline",
    "allOf": [
     {
      "$ref": "#/components/schemas/AreaOfZone"
     },
     {
      "type": "object",
      "required": [
       "outline"
      ],
      "properties": {
       "outline": {
        "oneOf": [
         {
          "$ref": "#/components/schemas/Polygon"
         },
         {
          "$ref": "#/components/schemas/MultiPolygon"
         },
         {
          "type": "null"
         }
        ],
        "description": "The area's outline, Polygon or MultiPolygon; null for none"
       }
      }
     }
    ]
   },
   "ZoneFeature": {
    "type": "object",
    "required": [
     "type",
     "geometry",
     "properties"
    ],
    "properties": {
     "type": {
      "const": "Feature"
     },
     "geometry": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Polygon"
       },
       {
        "$ref": "#/components/schemas/MultiPolygon"
       },
       {
        "type": "null"
       }
      ],
      "description": "Polygon, or MultiPolygon for a zone drawn in two parts; null for a zone whose outline is not yet drawn"
     },
     "properties": {
      "$ref": "#/components/schemas/ZoneProperties"
     }
    }
   },
   "ZonesCollection": {
    "type": "object",
    "required": [
     "type",
     "generated_at",
     "features",
     "areas"
    ],
    "properties": {
     "type": {
      "const": "FeatureCollection"
     },
     "generated_at": {
      "type": "string",
      "format": "date-time"
     },
     "features": {
      "type": "array",
      "description": "The zones, then a Point per sign location with a position (#165), without its signs",
      "items": {
       "oneOf": [
        {
         "$ref": "#/components/schemas/ZoneFeature"
        },
        {
         "$ref": "#/components/schemas/SignLocationPointFeature"
        }
       ]
      }
     },
     "areas": {
      "type": "array",
      "description": "The program's areas, every one whatever changed_since says; empty for a program with none",
      "items": {
       "$ref": "#/components/schemas/Area"
      }
     }
    }
   },
   "SignCounts": {
    "type": "object",
    "required": [
     "count",
     "states",
     "list"
    ],
    "properties": {
     "count": {
      "type": "integer",
      "description": "Trailhead signs at the location"
     },
     "states": {
      "type": "object",
      "description": "How many trailhead signs stand in each sign state",
      "additionalProperties": {
       "type": "integer"
      }
     },
     "list": {
      "type": "array",
      "description": "Each trailhead sign at the location with its sign type (#100)",
      "items": {
       "$ref": "#/components/schemas/TrailheadSign"
      }
     }
    }
   },
   "TrailheadSign": {
    "type": "object",
    "required": [
     "sign_type",
     "support",
     "number",
     "state"
    ],
    "properties": {
     "sign_type": {
      "type": "string",
      "description": "The sign's colour code (BL)"
     },
     "support": {
      "type": "string",
      "enum": [
       "POLE",
       "POST",
       "CP"
      ],
      "description": "How the sign is held: POLE on the program's pole, POST alone for a post already standing, CP bolted to a checkpoint"
     },
     "number": {
      "type": "integer",
      "description": "Which of several signs of its colour at the location; 1 unless more"
     },
     "state": {
      "type": "string",
      "enum": [
       "no sign yet",
       "installed",
       "damaged",
       "missing",
       "down for the summer"
      ]
     }
    }
   },
   "SignLocationProperties": {
    "type": "object",
    "required": [
     "slug",
     "name",
     "kind",
     "zone",
     "established",
     "updated_at",
     "checkpoint",
     "signs"
    ],
    "properties": {
     "slug": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "kind": {
      "type": "string",
      "enum": [
       "trailhead",
       "hut"
      ]
     },
     "zone": {
      "type": [
       "string",
       "null"
      ],
      "description": "The zone's slug; null outside every zone (on the out-of-zone channel, if the program has one)"
     },
     "established": {
      "type": "boolean",
      "description": "Legal, plowed winter parking; for reference only"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "The later of the location's change and any of its signs'"
     },
     "checkpoint": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "no sign yet",
       "installed",
       "damaged",
       "missing",
       "down for the summer",
       null
      ],
      "description": "The checkpoint's sign state where one stands: the state of the trailhead sign bolted to it (support CP); null elsewhere"
     },
     "signs": {
      "$ref": "#/components/schemas/SignCounts"
     }
    }
   },
   "SignLocationFeature": {
    "type": "object",
    "required": [
     "type",
     "geometry",
     "properties"
    ],
    "properties": {
     "type": {
      "const": "Feature"
     },
     "geometry": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Point"
       },
       {
        "type": "null"
       }
      ],
      "description": "null for a sign location not yet placed"
     },
     "properties": {
      "$ref": "#/components/schemas/SignLocationProperties"
     }
    }
   },
   "SignLocationsCollection": {
    "type": "object",
    "required": [
     "type",
     "generated_at",
     "features"
    ],
    "properties": {
     "type": {
      "const": "FeatureCollection"
     },
     "generated_at": {
      "type": "string",
      "format": "date-time"
     },
     "features": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/SignLocationFeature"
      }
     }
    }
   },
   "SignLocationPointProperties": {
    "type": "object",
    "description": "A sign location as the Zones answer carries it: the Sign locations answer's properties less checkpoint and signs, so no sign states",
    "required": [
     "slug",
     "name",
     "kind",
     "zone",
     "established",
     "updated_at"
    ],
    "properties": {
     "slug": {
      "type": "string",
      "description": "The sign location's slug"
     },
     "name": {
      "type": "string"
     },
     "kind": {
      "type": "string",
      "enum": [
       "trailhead",
       "hut"
      ]
     },
     "zone": {
      "type": [
       "string",
       "null"
      ],
      "description": "The zone's slug; null outside every zone (on the out-of-zone channel, if the program has one)"
     },
     "established": {
      "type": "boolean",
      "description": "Legal, plowed winter parking; for reference only"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the sign location last changed; a sign's state is no change"
     }
    }
   },
   "SignLocationPointFeature": {
    "type": "object",
    "required": [
     "type",
     "geometry",
     "properties"
    ],
    "properties": {
     "type": {
      "const": "Feature"
     },
     "geometry": {
      "$ref": "#/components/schemas/Point",
      "description": "Where the sign location is; one not yet placed is left out"
     },
     "properties": {
      "$ref": "#/components/schemas/SignLocationPointProperties"
     }
    }
   },
   "ZonesGpx": {
    "type": "string",
    "format": "xml",
    "contentMediaType": "application/gpx+xml",
    "description": "GPX 1.1 (xmlns http://www.topografix.com/GPX/1/1), text not JSON: metadata with the program's name, a desc ending in the attribution, and time, the generated_at of the Zones answer it renders; then one wpt per sign location the Zones answer carries, its name <sign location> <channel>-<privacy code> with its zone's (Rubicon 20-30), or the program's out-of-zone channel outside every zone (5-30; <sign location> see the sign in a program with none) and its type trailhead or hut; then one trk per zone whose outline is drawn, its name <zone> <channel>-<privacy code> (Rubicon 20-30), one trkseg per ring of the outline, each trkpt with lat and lon in decimal degrees"
   },
   "Point": {
    "type": "object",
    "required": [
     "type",
     "coordinates"
    ],
    "properties": {
     "type": {
      "const": "Point"
     },
     "coordinates": {
      "type": "array",
      "description": "[longitude, latitude]",
      "items": {
       "type": "number"
      },
      "minItems": 2,
      "maxItems": 2
     }
    }
   },
   "Polygon": {
    "type": "object",
    "required": [
     "type",
     "coordinates"
    ],
    "properties": {
     "type": {
      "const": "Polygon"
     },
     "coordinates": {
      "type": "array",
      "description": "Rings of [longitude, latitude] positions, the outer ring first",
      "items": {
       "type": "array",
       "items": {
        "type": "array",
        "items": {
         "type": "number"
        },
        "minItems": 2,
        "maxItems": 2
       }
      }
     }
    }
   },
   "MultiPolygon": {
    "type": "object",
    "required": [
     "type",
     "coordinates"
    ],
    "properties": {
     "type": {
      "const": "MultiPolygon"
     },
     "coordinates": {
      "type": "array",
      "description": "One list of rings per part, each as a Polygon's coordinates",
      "items": {
       "type": "array",
       "items": {
        "type": "array",
        "items": {
         "type": "array",
         "items": {
          "type": "number"
         },
         "minItems": 2,
         "maxItems": 2
        }
       }
      }
     }
    }
   },
   "SignColour": {
    "type": "object",
    "required": [
     "code",
     "name",
     "hex",
     "channel",
     "privacy_code"
    ],
    "properties": {
     "code": {
      "type": "string",
      "description": "The sign type's code (BL)"
     },
     "name": {
      "type": "string",
      "description": "The colour's name (blue)"
     },
     "hex": {
      "type": "string"
     },
     "channel": {
      "type": "integer",
      "description": "The community channel it fixes"
     },
     "privacy_code": {
      "type": "integer"
     }
    }
   },
   "Channels": {
    "type": "object",
    "required": [
     "sign_colours",
     "out_of_zone_channel",
     "generated_at"
    ],
    "properties": {
     "sign_colours": {
      "type": "array",
      "description": "Empty for a program with no signs, as one stood up from its map alone: read each zone's own channel",
      "items": {
       "$ref": "#/components/schemas/SignColour"
      }
     },
     "out_of_zone_channel": {
      "type": [
       "object",
       "null"
      ],
      "description": "The channel for anywhere outside every zone; null when the program has none",
      "required": [
       "channel",
       "privacy_code"
      ],
      "properties": {
       "channel": {
        "type": "integer"
       },
       "privacy_code": {
        "type": "integer"
       }
      }
     },
     "generated_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "ChannelsUnchanged": {
    "type": "object",
    "description": "The answer when the channels have not changed since the time sent",
    "required": [
     "generated_at"
    ],
    "properties": {
     "generated_at": {
      "type": "string",
      "format": "date-time"
     }
    },
    "additionalProperties": false
   },
   "Event": {
    "type": "object",
    "required": [
     "slug",
     "title",
     "activity",
     "activity_name",
     "starts_at",
     "ends_at",
     "all_day",
     "status",
     "organizer_name",
     "organizer_url",
     "place",
     "description",
     "published",
     "updated_at"
    ],
    "properties": {
     "slug": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "activity": {
      "type": "string",
      "description": "The activity's slug"
     },
     "activity_name": {
      "type": "string"
     },
     "starts_at": {
      "type": "string",
      "format": "date-time",
      "description": "UTC; a client shows Pacific time (PT)"
     },
     "ends_at": {
      "type": "string",
      "format": "date-time",
      "description": "UTC; a client shows Pacific time (PT)"
     },
     "all_day": {
      "type": "boolean",
      "description": "true for a date with no times: starts_at and ends_at are then midnight PT of its first day and of the day after its last"
     },
     "status": {
      "type": "string",
      "enum": [
       "scheduled",
       "cancelled"
      ],
      "description": "A cancelled event stays on the list"
     },
     "organizer_name": {
      "type": "string",
      "description": "The event organizer, the outside contact; empty when none"
     },
     "organizer_url": {
      "type": [
       "string",
       "null"
      ],
      "format": "uri",
      "description": "A link to the event organizer's page; null when none"
     },
     "place": {
      "type": "string",
      "description": "Where, in words"
     },
     "description": {
      "type": "string"
     },
     "published": {
      "const": true,
      "description": "Always true on a listed event"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "The later of the event's change and its activity's"
     }
    }
   },
   "RemovedEvent": {
    "type": "object",
    "description": "On a changed_since answer only: an event unpublished or deleted since, for a client keeping a copy to drop",
    "required": [
     "slug",
     "updated_at",
     "published"
    ],
    "properties": {
     "slug": {
      "type": "string"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "When it was unpublished or deleted"
     },
     "published": {
      "const": false
     }
    },
    "additionalProperties": false
   },
   "StandUp": {
    "type": "object",
    "description": "The latest stand-up of the program from its files",
    "required": [
     "date",
     "source",
     "minutes"
    ],
    "properties": {
     "date": {
      "type": "string",
      "format": "date",
      "description": "The day, in PT"
     },
     "source": {
      "type": "string",
      "enum": [
       "spreadsheet",
       "caltopo-export"
      ],
      "description": "What the files were"
     },
     "minutes": {
      "type": "integer",
      "description": "The stand-up clock's reading"
     }
    }
   },
   "Addresses": {
    "type": "object",
    "description": "The program's public addresses, absolute on this server",
    "required": [
     "zones",
     "sign_locations",
     "channels",
     "conditions",
     "events",
     "zones_feed",
     "zones_gpx",
     "sign_locations_feed"
    ],
    "properties": {
     "zones": {
      "type": "string",
      "format": "uri"
     },
     "sign_locations": {
      "type": "string",
      "format": "uri"
     },
     "channels": {
      "type": "string",
      "format": "uri"
     },
     "conditions": {
      "type": "string",
      "format": "uri"
     },
     "events": {
      "type": "string",
      "format": "uri"
     },
     "zones_feed": {
      "type": "string",
      "format": "uri"
     },
     "zones_gpx": {
      "type": "string",
      "format": "uri"
     },
     "sign_locations_feed": {
      "type": "string",
      "format": "uri"
     }
    }
   },
   "Program": {
    "type": "object",
    "required": [
     "slug",
     "name",
     "organization",
     "region",
     "licence",
     "attribution",
     "demo",
     "source",
     "stood_up",
     "addresses"
    ],
    "properties": {
     "slug": {
      "type": "string",
      "description": "The program's slug, in every address"
     },
     "name": {
      "type": "string"
     },
     "organization": {
      "type": "string",
      "description": "Who runs the program"
     },
     "region": {
      "type": "string",
      "description": "Where, in words"
     },
     "licence": {
      "type": "string",
      "description": "The licence the data is under"
     },
     "attribution": {
      "type": "string",
      "description": "What a client credits the program with, as a line of text"
     },
     "demo": {
      "type": "boolean",
      "description": "true for a sample program"
     },
     "source": {
      "type": [
       "string",
       "null"
      ],
      "description": "One sentence on where a sample's data comes from; null for a program with none"
     },
     "stood_up": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/StandUp"
       },
       {
        "type": "null"
       }
      ],
      "description": "null until a stand-up is recorded"
     },
     "addresses": {
      "$ref": "#/components/schemas/Addresses"
     }
    }
   },
   "Programs": {
    "type": "object",
    "required": [
     "generated_at",
     "programs"
    ],
    "properties": {
     "generated_at": {
      "type": "string",
      "format": "date-time"
     },
     "programs": {
      "type": "array",
      "description": "The programs people use first, then the samples",
      "items": {
       "$ref": "#/components/schemas/Program"
      }
     }
    }
   },
   "Events": {
    "type": "object",
    "required": [
     "generated_at",
     "events"
    ],
    "properties": {
     "generated_at": {
      "type": "string",
      "format": "date-time"
     },
     "events": {
      "type": "array",
      "description": "Published events not over yet (or not over by the from day), soonest first; on a changed_since answer, then the events gone since",
      "items": {
       "oneOf": [
        {
         "$ref": "#/components/schemas/Event"
        },
        {
         "$ref": "#/components/schemas/RemovedEvent"
        }
       ]
      }
     }
    }
   },
   "Station": {
    "type": "object",
    "required": [
     "code",
     "name",
     "elevation_ft",
     "link"
    ],
    "properties": {
     "code": {
      "type": "string",
      "description": "The station's code in the station table"
     },
     "name": {
      "type": "string"
     },
     "elevation_ft": {
      "type": "integer"
     },
     "link": {
      "type": "string",
      "description": "The station's own page at its source"
     }
    }
   },
   "Reading": {
    "type": "object",
    "required": [
     "station",
     "backup_for",
     "state",
     "source",
     "read_at",
     "read"
    ],
    "properties": {
     "station": {
      "$ref": "#/components/schemas/Station"
     },
     "backup_for": {
      "oneOf": [
       {
        "type": "object"
       },
       {
        "type": "null"
       }
      ],
      "description": "The station named, when its backup stands in for it"
     },
     "state": {
      "type": "string",
      "enum": [
       "recent",
       "stale",
       "none",
       "unread"
      ],
      "description": "unread: the station has not been read yet; ask again shortly"
     },
     "source": {
      "type": "string",
      "description": "Who gave the numbers"
     },
     "read_at": {
      "oneOf": [
       {
        "type": "string",
        "format": "date-time"
       },
       {
        "type": "null"
       }
      ]
     },
     "read": {
      "type": "string",
      "description": "The reading's time in Pacific time (PT)"
     },
     "temperature_f": {
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "null"
       }
      ]
     },
     "wind_mph": {
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "null"
       }
      ]
     },
     "gust_mph": {
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "null"
       }
      ]
     },
     "wind_from_deg": {
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "null"
       }
      ]
     },
     "depth_in": {
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "null"
       }
      ]
     }
    }
   },
   "ConditionsTrailhead": {
    "type": "object",
    "required": [
     "slug",
     "name",
     "zone",
     "forecast",
     "up_high",
     "down_low"
    ],
    "properties": {
     "slug": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "latitude": {
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "null"
       }
      ]
     },
     "longitude": {
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "null"
       }
      ]
     },
     "zone": {
      "oneOf": [
       {
        "type": "object"
       },
       {
        "type": "null"
       }
      ]
     },
     "forecast": {
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "description": "The avalanche forecast zone's slug, a key of forecasts"
     },
     "up_high": {
      "type": "object",
      "description": "wind and snow, each a Reading or null"
     },
     "down_low": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Reading"
       },
       {
        "type": "null"
       }
      ]
     }
    }
   },
   "Conditions": {
    "type": "object",
    "required": [
     "generated_at",
     "demo",
     "out_of_zone",
     "trailheads",
     "forecasts"
    ],
    "properties": {
     "generated_at": {
      "type": "string",
      "format": "date-time"
     },
     "demo": {
      "type": "boolean",
      "description": "Every forecast is the demo forecast"
     },
     "out_of_zone": {
      "oneOf": [
       {
        "type": "object"
       },
       {
        "type": "null"
       }
      ]
     },
     "trailheads": {
      "type": "array",
      "description": "Every trailhead that names its stations or forecast zone",
      "items": {
       "$ref": "#/components/schemas/ConditionsTrailhead"
      }
     },
     "forecasts": {
      "type": "object",
      "description": "Each avalanche forecast zone's forecast, by slug, with its problems"
     }
    }
   }
  }
 }
}