{
  "openapi": "3.1.1",
  "info": {
    "version": "1.0.0",
    "title": "Rainlog API",
    "description": "Rainlog API for accessing data collected on\n[Rainlog.org](https://rainlog.org)\n\nThe read-only endpoints require no authentication.\n\nThe personal weather station upload endpoints are write-only, and\nauthenticate with credentials Rainlog issues per gauge.\n\nLast updated 2026-09-01.\n",
    "contact": {
      "name": "help@rainlog.org",
      "email": "help@rainlog.org"
    },
    "x-logo": {
      "url": "/assets/logo8.png",
      "altText": "Rainlog.org",
      "href": "https://rainlog.org/"
    },
    "x-lastModified": "2026-09-01"
  },
  "servers": [
    {
      "url": "https://rainlog.org/api/1.0"
    }
  ],
  "tags": [
    {
      "name": "Reading",
      "description": "A reading for a single day"
    },
    {
      "name": "MonthReading",
      "description": "A reading for an entire month. There are very few of these readings and this type was never properly supported. It is not recommended for use in any data studies."
    },
    {
      "name": "Gauge",
      "description": "A rain gauge. The actual properties of the gauge are stored in its GaugeRevision. A gauge may be modified over its life, but may not be moved more than a quarter mile from it's original location."
    },
    {
      "name": "GaugeRevision",
      "description": "Type and location of a gauge. Revisions exist because a gauge can be updated by a user, but it should not retroactively affect old readings."
    },
    {
      "name": "Data Export",
      "description": "Bulk download of Rainlog's data as zipped CSVs. Everything Rainlog holds is\npublic, so these need no authentication: the global export is the whole\ndataset, and the per user export is the same data narrowed to one account.\nStation credentials are redacted from the raw upload payloads either way.\n"
    },
    {
      "name": "PWS Upload",
      "description": "Observation upload for automatic gauges (personal weather stations). These are\nthe endpoints a station console, a bridge, or your own uploader posts to; the\nRainlog apps themselves never use them. Everything below applies to both\nprotocols; each operation then documents only what is specific to it.\n\n**Credentials are per gauge, and are not your Rainlog account login.** Rainlog\nissues every automatic gauge its own: a station id and key for the Weather\nUnderground protocol, a path token for Ecowitt. Those are the only things the\nstation ever sends, your Rainlog username and password will not authenticate an\nupload, and a station may upload only to the gauge its credentials were issued\nfor. Find them in Rainlog under **Data > View/Edit Gauges**, then the **key\nicon** on the gauge, which opens a window called **Automated Upload\nCredentials**. The gauge's Report Automation Mode must be set to Automatic\nfirst (its newest revision must have `reportAutomationMode` `automatic`): a\nmanual gauge has no credentials at all, and an upload for one is rejected.\nStation-side setup, with screenshots: https://rainlog.org/help/pws-setup\n\nBoth protocols accept GET and POST. The documented method for each is the one\nreal stations use: WU consoles send a GET with query parameters, Ecowitt\nconsoles send a form encoded POST. Either endpoint accepts the other form with\nthe same field names, so a hand-rolled uploader can pick whichever is easier.\n\n**How often to upload.** There is a ceiling and a floor, and they are set by\ndifferent things.\n\nThe ceiling is the rate limit: one accepted observation per gauge per 295\nseconds, about one every five minutes, on both protocols. Going faster is not\nan error, just wasted: the extra upload is rejected for rate, and still\naudited. Nothing is gained by trying, since the daily rollup works in hourly\nbuckets.\n\nThe floor is what the daily rollup needs to see. It works in hourly buckets,\nand a bucket counts as covered when it holds at least one observation, so\n**roughly one upload per hour is the practical minimum**. A day produces a\ndaily reading only if all three hold: at least 12 covered hours, at least 50%\nof that day's hours covered, and no gap longer than 12 consecutive hours. Miss\nany of them and the day generates nothing at all, which is why a station that\nonly reports when it rains can go missing from the map entirely.\n\nCoverage also decides quality. A day with every one of its hours covered is\nGood; anything less that still clears the gate is Poor, which counts for less\nin the interpolated map.\n\n**Target one upload every five minutes, with a minimum of one per hour.**\nHourly is the floor, not a goal, and it leaves no margin at all, because one\nupload that fails to send loses that hour outright. Enough lost hours and the\nday drops from Good to Poor, or out of the rollup entirely. At five-minute\nintervals a given hour has a dozen chances to be covered, so an outage has to\nlast a full hour before it costs anything. Upload on that cadence whether or\nnot it is raining: a dry hour still has to be covered to count.\n\n**What happens to an upload.** Only the rain fields become observation data.\nAny other field a station sends is accepted rather than rejected, but has no\neffect on rainfall totals. Nothing is thrown away: every accepted upload is\nkept verbatim, exactly as the station submitted it, with only the credential\nredacted, so the original reading remains available even for fields Rainlog\ndoes not currently interpret.\n\nA rain value above 30 inches is still recorded, but the observation is flagged\nPoor quality so it does not poison daily totals or the interpolated map.\nNegative values and `-9999` style \"no reading\" sentinels are discarded.\nObservations more than 2 hours in the future or more than 7 days in the past\nare rejected outright, and the gauge owner is notified.\n"
    }
  ],
  "paths": {
    "/Reading/getFiltered": {
      "post": {
        "security": [],
        "summary": "Search daily readings",
        "tags": [
          "Reading"
        ],
        "description": "Filter readings and return a list. This will return Readings whose\n  - readingId are in the list of readingIds\n  - AND readingDate intersects the date range\n  - AND position intersects the specified region\n  - AND gaugeType matches the list of types specified\n  - AND gaugeIds match the list of gaugeIds specified\n  - AND userIds match the list of userIds specified\n  - AND gaugeStatus matches the list of statuses specified\n  - AND quality matches the list of qualities specified\n  - AND timeWindowHour matches the list of hours specified\nOmitted or null fields will disable filtering on those fields. An explicitly empty gaugeIds list still means \"match nothing\".\nThis endpoint returns BOTH hand-entered readings and the daily totals derived from automatic (personal weather station) gauges. Where a gauge has both for the same day and window, the hand-entered reading wins. Automatic rows report readingId 0, which is how you tell the two apart. Automation is a property of the DAY, not of the gauge: a gauge that switched between manual and automatic keeps each day under whichever mechanism was active then, so one gauge's history routinely contains both kinds of row.\nResults are ordered by readingDate ascending. Pagination is applied after filtering.\n",
        "requestBody": {
          "description": "Criteria by which to filter the data",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReadingFilter"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation, returning a list of readings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Reading"
                  }
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/MonthReading/getFiltered": {
      "post": {
        "security": [],
        "summary": "Search monthly readings",
        "tags": [
          "MonthReading"
        ],
        "description": "Filter month readings and return a list. This will return GaugeRevisions whose\n  - monthReadingId are in the list of monthReadingIds\n  - AND readingDate intersects the date range\n  - AND position intersects the specified region\n  - AND gaugeType matches the list of types specified\n  - AND gaugeIds match the list of gaugeIds specified\n  - AND userIds match the list of userIds specified\n\nOmitted or null fields will disable filtering on those fields.\n",
        "requestBody": {
          "description": "Criteria by which to filter the data",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonthReadingFilter"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation, returning a list of MonthReadings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MonthReading"
                  }
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/GaugeRevision/getFiltered": {
      "post": {
        "security": [],
        "summary": "Search gauge revisions",
        "tags": [
          "GaugeRevision"
        ],
        "description": "Filter gauge revisions and return a list. A revision is the type and location of a gauge at a point in time, so this is where a reading's position, gauge type and timezone come from.\n",
        "requestBody": {
          "description": "Filter gauge revisions and return a list. This will return GaugeRevisions whose\n  - gaugeRevisionId matches the list of gaugeRevisionIds\n  - AND existence intersects the date range\n  - AND position intersects the specified region\n  - AND gaugeType matches the list of types specified\n  - AND gaugeIds match the list of gaugeIds specified\n  - AND userIds match the list of userIds specified\n\n  gaugeRevisionIds, region, gaugeType, gaugeIds, and userIds may be null or omitted to disable filtering on those fields\n",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GaugeRevisionFilter"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation, returning a list of GaugeRevisions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GaugeRevision"
                  }
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/Gauge/getFiltered": {
      "post": {
        "security": [],
        "summary": "Search gauges",
        "tags": [
          "Gauge"
        ],
        "description": "Filter gauges and return a list. A gauge is an identity that ties its revisions together; its properties live on the GaugeRevision.\n",
        "requestBody": {
          "description": "Filter Gauges and return a list. This will return Gauges whose\n  - gaugeIds match the list of gaugeIds specified\n  - AND userIds match the list of userIds specified\n  - AND gaugeStatus matches the list of statuses specified\n\n  gaugeIds, userIds, and gaugeStatus may be null or omitted to disable filtering on those fields\n",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GaugeFilter"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation, returning a list of Gauges",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Gauge"
                  }
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/PwsPayload/getLatest": {
      "get": {
        "security": [],
        "summary": "Read a gauge's latest upload",
        "tags": [
          "PWS Upload"
        ],
        "description": "The newest automatic observation for one automatic gauge.\n\nEvery other read endpoint here searches daily or monthly aggregates. This one answers what a gauge is reading now, which is what an integration needs and what nothing else returned.\n\nRain and snow come from Rainlog's own columns. Every other sensor is read out of the upload payload exactly as the station submitted it, which Rainlog keeps permanently and already publishes in the global data export.\n",
        "parameters": [
          {
            "name": "gaugeId",
            "in": "query",
            "required": true,
            "description": "The gauge to read. Must be an automatic gauge.",
            "schema": {
              "$ref": "#/components/schemas/DatabaseId"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "The shape to answer in.\n\n`raw` (the default) returns the stored payload verbatim, redacted exactly as the global data export redacts it. `json` returns the named fields plus that same payload. `wu` and `ecowitt` return the payload untouched when the observation arrived on that protocol, and otherwise render it under the other protocol's parameter names.\n\nA rendered record carries only the fields Rainlog models, so it can lose things `raw` would keep. The piezo rain family exists only in the Ecowitt protocol, so rendering as `wu` drops it. A piezo gauge still reports its headline rain as `rainin` (Rainlog folds the piezo hourly value into the observation's rain when there is no tipping bucket value), but its piezo daily, weekly, monthly, yearly, event and rate counters have no WU names and are lost. Ask for `ecowitt` or `raw` to keep them.\n",
            "schema": {
              "type": "string",
              "enum": [
                "raw",
                "json",
                "wu",
                "ecowitt"
              ],
              "default": "raw"
            }
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "Read the newest upload at or before this time instead of the newest overall. An ISO-8601 instant (`2026-09-01T12:00:00Z`) or the `yyyy-MM-dd HH:mm:ss` UTC form the upload endpoints accept.\n\nAsking for a point in time is deliberate, so the staleness rule below does not apply when `at` is given.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeStale",
            "in": "query",
            "required": false,
            "description": "Return the newest upload even when it is too old to be a current reading. See the 404 below.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The upload. `application/json` for format `json`, otherwise `text/plain` holding one form encoded record.\n",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "ID=Rainlog4821&dateutc=2026-09-01%2003%3A08%3A41&rainin=0.12&tempf=78.4"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PwsPayload"
                }
              }
            }
          },
          "404": {
            "description": "No current reading. Either the gauge has never reported, or nothing exists at or before `at`, or (for a text format) the newest upload has no stored payload to return verbatim.\n\nAlso returned when the gauge last reported more than `rainlog.pws.stale-after-hours` ago (24 by default, the same threshold that marks a gauge as stopped reporting elsewhere in Rainlog). A gauge that fell silent months ago has no current reading, and answering with its last one invites a caller to treat it as now. Pass `includeStale=true` for the last known reading regardless of age, judging it from `observedAt` yourself, or `at` to read a point in time.\n"
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "security": [],
        "summary": "Read the latest upload for several gauges",
        "tags": [
          "PWS Upload"
        ],
        "description": "The batch form of the GET. A gauge with no current reading is absent from the response rather than being an error, which covers a gauge that never reported and one that has gone stale.\n\nIn a text format the body is one record per line, each beginning with its `ID=Rainlog<gaugeId>` so records can be matched to gauges without relying on their order.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PwsPayloadFilter"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per gauge that has an observation. `application/json` for format `json`, otherwise `text/plain` with one record per line.\n",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PwsPayload"
                  }
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/DataDump/globalData": {
      "get": {
        "security": [],
        "summary": "Download the whole public dataset",
        "tags": [
          "Data Export"
        ],
        "description": "The nightly export of every gauge, reading and observation in Rainlog, as a zip of CSVs. Regenerated once a day, so the same URL gives that day's artifact to everyone. The rollover is just after midnight America/Phoenix; before that instant the request resolves to the previous day's artifact, because the current day's has not been written yet.\n\nWith `includePwsPayloads=true` you get the superset artifact instead: the same CSVs plus `automatic_observation_payloads.csv`, the raw per-upload payload for every accepted PWS upload, exactly as the station submitted it. Station credentials are redacted in that file: `PASSWORD`, `PASSKEY` and `mac`, plus `ID` / `stationid`, which are redundant with its own `gauge_id` column. This is the same redaction `PwsPayload/getLatest` applies to its `raw` format.\n",
        "parameters": [
          {
            "name": "includePwsPayloads",
            "in": "query",
            "required": false,
            "description": "Include the raw per-upload PWS payload CSV. Adds roughly 150 MB uncompressed, so ask for it only if you want the submitted values rather than Rainlog's parsed readings.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The export archive.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "The day's artifact has not been generated yet. It is written by a nightly task, so this clears on its own.\n"
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/DataDump/forUser": {
      "get": {
        "security": [],
        "summary": "Download one user's data",
        "tags": [
          "Data Export"
        ],
        "description": "The same export as `DataDump/globalData`, narrowed to one user's gauges, readings, monthly readings, observations and notification subscriptions. Built on request rather than nightly, so it reflects the data as of the call.\n\n`includePwsPayloads=true` adds that user's raw upload payloads, one CSV per gauge, under the same redaction as the global artifact.\n",
        "parameters": [
          {
            "name": "userId",
            "in": "query",
            "required": true,
            "description": "The user whose data to export.",
            "schema": {
              "$ref": "#/components/schemas/DatabaseId"
            }
          },
          {
            "name": "includePwsPayloads",
            "in": "query",
            "required": false,
            "description": "Include this user's raw per-upload PWS payload CSVs.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The export archive.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/AutomaticObservation/createFromWu": {
      "get": {
        "security": [],
        "tags": [
          "PWS Upload"
        ],
        "summary": "Upload an observation using the Weather Underground protocol",
        "description": "Accepts a Weather Underground style observation for one automatic gauge.\n\nAlso reachable at `https://rainlog.org/weatherstation/updateweatherstation.php`,\nwhich is the literal path WU-compatible firmware uses, so a station already\nconfigured for Weather Underground can be pointed at Rainlog by changing only\nthe host name.\n\nThe gauge's credentials travel as ordinary request fields: `ID` is the station\nid `Rainlog<gaugeId>` (for example `Rainlog4821`) and `PASSWORD` is that\ngauge's station key. See PWS Upload above for where to find them.\n\nRate limit: one accepted observation per station per 295 seconds by default,\nso about one upload every five minutes.\n\nThe rain value stored for the observation is `rainin`, the WU rolling one hour\ntotal. `hourlyrainin` is accepted for uploaders that send the Ecowitt\nspelling, but it feeds only the separate hourly figure: it is not a\nfallback for the observation's rain, so an upload carrying `hourlyrainin`\nand no `rainin` records no rain. Send `rainin`.\n\nOnly precipitation becomes a Rainlog reading. The other sensors below are\naccepted and recorded with the upload, and the whole payload is retained\nverbatim, but they do not change the reading. Parameters not listed here\nare not rejected either: they are kept in the stored payload as sent.\n\nHow daily totals are derived, and which fields to send: Rainlog builds a\nreading day by differencing a cumulative counter between consecutive\nuploads, not by adding up the hourly values. Differencing is immune to\nupload phase, missed uploads and a varying upload rate, because the\nchange between two samples is exactly the rain that fell in between.\n\nThe counter is chosen per gauge per reading day, in this order of\npreference:\n\n1. `totalrainin`, the all time counter. Best: it only resets on a\n   factory reset.\n2. `yearlyrainin`, which resets on 1 January local time.\n3. `dailyrainin`, which resets at the station's local midnight. Last\n   resort: every reset is a point where a little rain can be lost, and\n   this one resets nightly.\n\n(An Ecowitt upload can also supply a piezo year counter, which ranks\nbetween `yearlyrainin` and `dailyrainin`. There is no WU parameter for\nit.)\n\nWhere several are present, the one with the most readings in the day\nwins, ties going to the order above. A counter going backwards (a reset)\nor jumping by more than 30 inches contributes nothing and simply\nrebaselines.\n\nIf no counter at all is sent, Rainlog falls back to adding the rolling\nhour values, which is materially less accurate. **Send `totalrainin` if\nyour station has it**, and send the counters even when they look\nredundant next to the hourly figure.\n",
        "parameters": [
          {
            "name": "ID",
            "in": "query",
            "required": true,
            "description": "Station id, in the form `Rainlog<gaugeId>`. Case insensitive on the `Rainlog` prefix. `station`, `si`, and `stationId` are accepted as aliases for Windy-compatible uploaders.\n",
            "schema": {
              "type": "string",
              "example": "Rainlog4821"
            }
          },
          {
            "name": "PASSWORD",
            "in": "query",
            "required": true,
            "description": "The gauge's station key, issued by Rainlog per gauge (not your Rainlog account password). Compared case sensitively.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateutc",
            "in": "query",
            "description": "Time of the observation, as UTC wall clock: `YYYY-MM-DD HH:MM:SS`, `YYYY-MM-DD HH:MM`, RFC3339, or the literal `now`. If absent or unparsable the server falls back to `time`, then `ts`, then its own clock.\n",
            "schema": {
              "type": "string",
              "example": "2026-08-17 21:05:00"
            }
          },
          {
            "name": "time",
            "in": "query",
            "description": "Fallback observation time when dateutc is absent. RFC3339, or `now`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ts",
            "in": "query",
            "description": "Fallback observation time when dateutc and time are absent. Unix epoch seconds.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "rainin",
            "in": "query",
            "description": "Rain in the past hour, in inches. The WU rolling one hour total, and the rain value stored for the observation.\n",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "rainratein",
            "in": "query",
            "description": "Instantaneous rain rate, inches per hour.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "eventrainin",
            "in": "query",
            "description": "Rain so far in the current event, in inches.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "hourlyrainin",
            "in": "query",
            "description": "Ecowitt-flavored rolling one hour total, in inches. Used only when rainin is absent.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "dailyrainin",
            "in": "query",
            "description": "Rain so far today, in inches. Resets at the station's local midnight. Used as a last-resort daily counter for stations that emit nothing better.\n",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "weeklyrainin",
            "in": "query",
            "description": "Rain so far this week, in inches.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "monthlyrainin",
            "in": "query",
            "description": "Rain so far this month, in inches.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "yearlyrainin",
            "in": "query",
            "description": "Rain so far this year, in inches. A monotonic counter, and the preferred source for daily totals after totalrainin.\n",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "totalrainin",
            "in": "query",
            "description": "All time rain counter, in inches. The most preferred source for deriving daily totals, because it only ever increases.\n",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "tempf",
            "in": "query",
            "description": "Outdoor temperature, degrees Fahrenheit.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "humidity",
            "in": "query",
            "description": "Outdoor relative humidity, percent.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "dewptf",
            "in": "query",
            "description": "Dew point, degrees Fahrenheit.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "baromin",
            "in": "query",
            "description": "Barometric pressure at sea level, inches of mercury.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "windspeedmph",
            "in": "query",
            "description": "Wind speed, miles per hour.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "windgustmph",
            "in": "query",
            "description": "Wind gust, miles per hour.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "maxdailygust",
            "in": "query",
            "description": "Highest gust so far today, miles per hour.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "winddir",
            "in": "query",
            "description": "Wind direction, degrees clockwise from north (0 to 359).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "solarradiation",
            "in": "query",
            "description": "Solar irradiance, watts per square metre.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "UV",
            "in": "query",
            "description": "UV index. `uv` is accepted as well, since firmwares disagree on the case.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "indoortempf",
            "in": "query",
            "description": "Indoor temperature, degrees Fahrenheit.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "indoorhumidity",
            "in": "query",
            "description": "Indoor relative humidity, percent.",
            "schema": {
              "type": "number",
              "format": "double"
            }
          },
          {
            "name": "softwaretype",
            "in": "query",
            "description": "Free text naming the uploading software. Recorded with the upload.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "query",
            "description": "WU protocol mechanic, always `updateraw` in practice. Accepted and recorded with the upload; it does not affect the reading.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "realtime",
            "in": "query",
            "description": "WU protocol mechanic marking a realtime feed, `1` in practice. Accepted and recorded with the upload.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rtfreq",
            "in": "query",
            "description": "WU protocol mechanic giving the realtime interval in seconds. Accepted and recorded with the upload. It does not change the rate limit, which is fixed per gauge.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Always returned for a well formed request, whether or not the observation was stored. The body is `success` when the observation was accepted, otherwise one of the error codes below. Check the body, not the status code.\n",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "success",
                    "INVALIDREQUEST",
                    "INVALIDPASSWORDID",
                    "RATELIMIT",
                    "FUTURE_TIMESTAMP",
                    "BACKFILL_TOO_OLD"
                  ],
                  "example": "success"
                }
              }
            }
          },
          "500": {
            "description": "The upload authenticated but the observation could not be written. The attempt is recorded in the audit log as INSERT_FAILED. Safe to retry.\n",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/AutomaticObservation/createFromEcowitt/{token}": {
      "post": {
        "security": [],
        "tags": [
          "PWS Upload"
        ],
        "summary": "Upload an observation using the Ecowitt protocol",
        "description": "Accepts an Ecowitt style observation for one automatic gauge.\n\nAlso reachable at `https://rainlog.org/data/report/{token}`, which is the\nliteral path Ecowitt consoles use for a custom server, so a station can be\npointed at Rainlog through its normal \"custom weather server\" settings.\n\nAuthentication has two parts. The path `token` is the gauge's credential,\nissued by Rainlog (see PWS Upload above for where to find it). `PASSKEY` is\nthe station's own identifier rather than anything Rainlog issues: the first\nsuccessful upload for a token binds whatever PASSKEY it presents to that\ngauge, and every later upload must present the same one. Regenerating the\ntoken clears the binding.\n\nRate limit: one accepted observation per gauge per 295 seconds by default,\nso about one upload every five minutes.\n\nOnly precipitation becomes a Rainlog reading. The other sensors below are\naccepted and recorded with the upload, and the whole payload is retained\nverbatim, but they do not change the reading. Parameters not listed here\nare not rejected either: they are kept in the stored payload as sent,\nwhich is where a console's extra channels, battery levels and air\nquality fields end up.\n\nHow daily totals are derived, and which fields to send: Rainlog builds a\nreading day by differencing a cumulative counter between consecutive\nuploads, not by adding up the hourly values. Differencing is immune to\nupload phase, missed uploads and a varying upload rate, because the\nchange between two samples is exactly the rain that fell in between.\n\nThe counter is chosen per gauge per reading day, in this order of\npreference:\n\n1. `totalrainin`, the all time counter. Best: it only resets on a\n   factory reset.\n2. `yearlyrainin`, which resets on 1 January local time.\n3. `yrain_piezo`, the piezo year counter, used when the tipping bucket\n   fields are absent.\n4. `dailyrainin`, which resets at the station's local midnight. Last\n   resort: every reset is a point where a little rain can be lost, and\n   this one resets nightly.\n\nWhere several are present, the one with the most readings in the day\nwins, ties going to the order above. A counter going backwards (a reset)\nor jumping by more than 30 inches contributes nothing and simply\nrebaselines.\n\nIf no counter at all is sent, Rainlog falls back to adding the rolling\nhour values, which is materially less accurate. **Send `totalrainin` if\nyour station has it**, and send the counters even when they look\nredundant next to the hourly figure.\n\nBoth rain families are understood. The stored hourly rain value is the first\nof `hourlyrainin`, `hrain_piezo`, `eventrainin`, `erain_piezo` that is\npresent, so a console reporting both a tipping bucket and a piezo gauge\nprefers the tipping bucket.\n",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The gauge's Ecowitt path token, issued by Rainlog per gauge (not your Rainlog account password). Compared case sensitively.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The observation fields, form encoded. This is what an Ecowitt console sends. The same fields are also accepted as query parameters on a GET, which is useful for a hand-rolled uploader or a quick curl test.\n",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "PASSKEY": {
                    "description": "The station's own passkey, as emitted by the console. Bound to the gauge on first use and required to match thereafter.\n",
                    "type": "string"
                  },
                  "dateutc": {
                    "description": "Time of the observation, as UTC wall clock: `YYYY-MM-DD HH:MM:SS`, `YYYY-MM-DD HH:MM`, RFC3339, or the literal `now`. If absent or unparsable the server uses its own clock.\n",
                    "type": "string",
                    "example": "2026-08-17 21:05:00"
                  },
                  "rainratein": {
                    "description": "Tipping bucket rain rate, inches per hour.",
                    "type": "number",
                    "format": "double"
                  },
                  "eventrainin": {
                    "description": "Tipping bucket rain so far in the current event, in inches.",
                    "type": "number",
                    "format": "double"
                  },
                  "hourlyrainin": {
                    "description": "Tipping bucket rain in the past hour, in inches. First choice for the stored rain value.",
                    "type": "number",
                    "format": "double"
                  },
                  "dailyrainin": {
                    "description": "Tipping bucket rain so far today, in inches. Resets at the station's local midnight.",
                    "type": "number",
                    "format": "double"
                  },
                  "weeklyrainin": {
                    "description": "Tipping bucket rain so far this week, in inches.",
                    "type": "number",
                    "format": "double"
                  },
                  "monthlyrainin": {
                    "description": "Tipping bucket rain so far this month, in inches.",
                    "type": "number",
                    "format": "double"
                  },
                  "yearlyrainin": {
                    "description": "Tipping bucket rain so far this year, in inches. A monotonic counter that resets on 1 January local time, and the second choice for deriving daily totals after totalrainin.\n",
                    "type": "number",
                    "format": "double"
                  },
                  "totalrainin": {
                    "description": "Tipping bucket all time rain counter, in inches. The most preferred source for deriving daily totals, because it only ever increases. Send it if the station has it.\n",
                    "type": "number",
                    "format": "double"
                  },
                  "rrain_piezo": {
                    "description": "Piezo (WS90/WS85) rain rate, inches per hour.",
                    "type": "number",
                    "format": "double"
                  },
                  "erain_piezo": {
                    "description": "Piezo rain so far in the current event, in inches.",
                    "type": "number",
                    "format": "double"
                  },
                  "hrain_piezo": {
                    "description": "Piezo rain in the past hour, in inches. Used for the stored rain value when hourlyrainin is absent.",
                    "type": "number",
                    "format": "double"
                  },
                  "drain_piezo": {
                    "description": "Piezo rain so far today, in inches. Resets at the station's local midnight.\n",
                    "type": "number",
                    "format": "double"
                  },
                  "wrain_piezo": {
                    "description": "Piezo rain so far this week, in inches.",
                    "type": "number",
                    "format": "double"
                  },
                  "mrain_piezo": {
                    "description": "Piezo rain so far this month, in inches.",
                    "type": "number",
                    "format": "double"
                  },
                  "yrain_piezo": {
                    "description": "Piezo rain so far this year, in inches. Third choice for deriving daily totals, used when the tipping bucket counters are absent.\n",
                    "type": "number",
                    "format": "double"
                  },
                  "srain_piezo": {
                    "description": "Piezo rain-start indicator, 1 or 0. Not an accumulation.",
                    "type": "integer",
                    "enum": [
                      0,
                      1
                    ]
                  },
                  "tempf": {
                    "description": "Outdoor temperature, degrees Fahrenheit.",
                    "type": "number",
                    "format": "double"
                  },
                  "humidity": {
                    "description": "Outdoor relative humidity, percent.",
                    "type": "number",
                    "format": "double"
                  },
                  "dewptf": {
                    "description": "Dew point, degrees Fahrenheit.",
                    "type": "number",
                    "format": "double"
                  },
                  "baromrelin": {
                    "description": "Barometric pressure at sea level, inches of mercury.",
                    "type": "number",
                    "format": "double"
                  },
                  "baromabsin": {
                    "description": "Absolute (station) pressure, inches of mercury.",
                    "type": "number",
                    "format": "double"
                  },
                  "windspeedmph": {
                    "description": "Wind speed, miles per hour.",
                    "type": "number",
                    "format": "double"
                  },
                  "windgustmph": {
                    "description": "Wind gust, miles per hour.",
                    "type": "number",
                    "format": "double"
                  },
                  "maxdailygust": {
                    "description": "Highest gust so far today, miles per hour.",
                    "type": "number",
                    "format": "double"
                  },
                  "winddir": {
                    "description": "Wind direction, degrees clockwise from north (0 to 359).",
                    "type": "integer"
                  },
                  "solarradiation": {
                    "description": "Solar irradiance, watts per square metre.",
                    "type": "number",
                    "format": "double"
                  },
                  "uv": {
                    "description": "UV index.",
                    "type": "number",
                    "format": "double"
                  },
                  "tempinf": {
                    "description": "Indoor temperature, degrees Fahrenheit. This is the Ecowitt spelling of the WU protocol indoortempf.",
                    "type": "number",
                    "format": "double"
                  },
                  "humidityin": {
                    "description": "Indoor relative humidity, percent. This is the Ecowitt spelling of the WU protocol indoorhumidity.",
                    "type": "number",
                    "format": "double"
                  },
                  "stationtype": {
                    "description": "Console firmware string, for example GW1100B_V2.4.5. Recorded with the upload.",
                    "type": "string"
                  },
                  "model": {
                    "description": "Console model, for example GW1100B. Recorded with the upload.",
                    "type": "string"
                  },
                  "freq": {
                    "description": "Sensor radio frequency, for example 915M. Recorded with the upload.",
                    "type": "string"
                  },
                  "runtime": {
                    "description": "Console uptime in seconds. Recorded with the upload.",
                    "type": "integer",
                    "format": "int64"
                  },
                  "heap": {
                    "description": "Console free heap in bytes. Recorded with the upload.",
                    "type": "integer",
                    "format": "int64"
                  }
                },
                "required": [
                  "PASSKEY"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Always returned for a well formed request, whether or not the observation was stored. The body is `success` when the observation was accepted, otherwise one of the error codes below. Check the body, not the status code.\n",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "success",
                    "INVALIDREQUEST",
                    "INVALIDTOKEN",
                    "INVALIDPASSKEY",
                    "RATELIMIT",
                    "FUTURE_TIMESTAMP",
                    "BACKFILL_TOO_OLD"
                  ],
                  "example": "success"
                }
              }
            }
          },
          "500": {
            "description": "The upload authenticated but the observation could not be written. The attempt is recorded in the audit log as INSERT_FAILED. Safe to retry.\n",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Reading": {
        "description": "A reading for a specific day. The period it covers is given by timeWindowHour: 7 means 7am-7am (the common case), 0 means midnight-to-midnight. Note that readingDate is the date the reading was TAKEN, not the date the rain fell: under a 7am window, rain that fell on Jan 10 is reported with readingDate Jan 11.\n",
        "properties": {
          "readingId": {
            "description": "Identifier of the hand-entered reading. Readings derived from an automatic (personal weather station) gauge are not rows in the manual reading table and report 0 here.\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/DatabaseId"
              }
            ]
          },
          "gaugeId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "gaugeRevisionId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "remarks": {
            "type": [
              "string",
              "null"
            ]
          },
          "readingDate": {
            "$ref": "#/components/schemas/ISO8601Date"
          },
          "readingHour": {
            "description": "Hour the reading was taken, time local to gauge GPS coordinate.",
            "type": "integer",
            "format": "int32",
            "example": 7
          },
          "readingMinute": {
            "description": "Minute the reading was taken, time local to gauge GPS coordinate.",
            "type": "integer",
            "format": "int32"
          },
          "quality": {
            "$ref": "#/components/schemas/ReadingQuality"
          },
          "rainAmount": {
            "description": "Liquid precipitation, in inches. The nullability of this field changes with quality.\n\n- \"Good\", \"Trace\", or \"Poor\"\n   - This value must not be null\n- \"Snow\"\n   - This value _may_ be null\n- \"Lost\", or \"Absent\"\n   - This value must be null\n",
            "type": [
              "number",
              "null"
            ],
            "format": "double"
          },
          "snowDepth": {
            "description": "Snow depth, in inches. The nullability of this field changes with quality.\n- \"Good\", \"Trace\", \"Poor\", \"Lost\" or \"Absent\"\n   - This value must be null\n- \"Snow\"\n   - This value _may_ be null\n",
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "example": null
          },
          "snowAccumulation": {
            "description": "Snow accumulation, in inches. The nullability of this field changes with quality.\n- \"Good\", \"Trace\", \"Poor\", \"Lost\" or \"Absent\"\n   - This value must be null\n- \"Snow\"\n   - This value _may_ be null\n",
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "example": null
          },
          "timeWindowHour": {
            "description": "Hour at which the reading day ends, local to the gauge. 7 means the reading covers 7am-7am, 0 means midnight-to-midnight. These are the only two legal values. An automatic gauge produces a row for BOTH windows for the same day from the same observations, so if you do not filter on timeWindowHour you will get two rows per automatic gauge per day.\n",
            "type": "integer",
            "format": "int32",
            "enum": [
              0,
              7
            ],
            "example": 7
          }
        }
      },
      "MonthReading": {
        "description": "A reading for an entire month",
        "properties": {
          "monthReadingId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "gaugeId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "gaugeRevisionId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "readingDate": {
            "description": "The month this reading covers, as the first day of that month (`2026-08-01` means August 2026). There are no separate year and month fields.\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/ISO8601Date"
              }
            ]
          },
          "remarks": {
            "type": [
              "string",
              "null"
            ]
          },
          "rainAmount": {
            "description": "Liquid precipitation for the whole month, in inches.",
            "type": [
              "number",
              "null"
            ],
            "format": "double"
          }
        }
      },
      "Gauge": {
        "properties": {
          "gaugeId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "userId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "name": {
            "type": "string",
            "example": "Bob's gauge"
          },
          "status": {
            "$ref": "#/components/schemas/GaugeStatus"
          },
          "sortOrder": {
            "description": "Display order of this gauge within its owner's gauge list.",
            "type": "integer",
            "format": "int32",
            "example": 0
          },
          "pwsWuKey": {
            "description": "The gauge's Weather Underground upload key, used as `PASSWORD` when posting observations to /AutomaticObservation/createFromWu.\nThis is a credential, so it is returned ONLY to the gauge's owner or to an admin. It is null for an unauthenticated caller and for any other user, including on gauges that do have a key. Manual gauges have no key and report null for everyone.\n",
            "type": [
              "string",
              "null"
            ],
            "example": null
          }
        }
      },
      "GaugeRevision": {
        "properties": {
          "gaugeRevisionId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "gaugeId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "createdDate": {
            "$ref": "#/components/schemas/ISO8601Date"
          },
          "brand": {
            "type": [
              "string",
              "null"
            ],
            "example": "AccuGauge"
          },
          "model": {
            "type": [
              "string",
              "null"
            ],
            "example": "AccuGauge 2000"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "example": "On South side of house"
          },
          "position": {
            "$ref": "#/components/schemas/GpsCoordinate"
          },
          "gaugeType": {
            "$ref": "#/components/schemas/GaugeType"
          },
          "gaugeTypeOther": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "reportAutomationMode": {
            "description": "How this revision reports. \"manual\" means a person enters readings; \"automatic\" means the gauge is a personal weather station whose daily totals are machine-derived from its uploads. The mode that applies to any given day is the one on the revision that was active THAT DAY, not the newest revision.\n",
            "type": "string",
            "enum": [
              "manual",
              "automatic"
            ],
            "example": "manual"
          },
          "timeWindowHour": {
            "description": "Hour at which this revision's reading day ends: 7 for 7am-7am, 0 for midnight-to-midnight. Null when reportAutomationMode is \"automatic\", because an automatic gauge generates both windows rather than picking one.\n",
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "enum": [
              0,
              7,
              null
            ],
            "example": 7
          },
          "timezone": {
            "description": "IANA timezone identifier for gauge location",
            "type": [
              "string",
              "null"
            ],
            "example": "America/Phoenix"
          }
        }
      },
      "DatabaseId": {
        "type": "integer",
        "format": "int64"
      },
      "ISO8601Date": {
        "description": "Short ISO 8601 formatted date, formatted __YYYY-MM-DD__",
        "type": "string",
        "example": "1999-02-25"
      },
      "GpsCoordinate": {
        "properties": {
          "lat": {
            "description": "Latitude",
            "type": "number",
            "format": "double"
          },
          "lng": {
            "description": "Longitude",
            "type": "number",
            "format": "double"
          }
        },
        "required": [
          "lat",
          "lng"
        ]
      },
      "GeoRegion": {
        "description": "A generic geospacial region. Polygons are not currently supported.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/GeoCircle"
          },
          {
            "$ref": "#/components/schemas/GeoRectangle"
          }
        ],
        "example": {
          "type": "Circle",
          "center": {
            "lat": 20.1,
            "lng": 11.4
          },
          "radius": 1.2
        }
      },
      "GeoCircle": {
        "description": "A geospacial region in the shape of a circle",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "Circle"
            ]
          },
          "center": {
            "$ref": "#/components/schemas/GpsCoordinate"
          },
          "radius": {
            "description": "The radius of the circle, in miles.",
            "type": "number",
            "format": "double"
          }
        },
        "required": [
          "type",
          "center",
          "radius"
        ],
        "example": {
          "type": "Circle",
          "center": {
            "lat": 20.1,
            "lng": 11.4
          },
          "radius": 1.2
        }
      },
      "GeoRectangle": {
        "description": "A geospacial region in the shape of rectangle coordinates. The rectangle is defined by its four edges - north west east south.\n",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "Rectangle"
            ]
          },
          "westLng": {
            "description": "West-most longitude",
            "type": "number",
            "format": "double"
          },
          "eastLng": {
            "description": "East-most longitude",
            "type": "number",
            "format": "double"
          },
          "northLat": {
            "description": "North-most latitude",
            "type": "number",
            "format": "double"
          },
          "southLat": {
            "description": "South-most latitude",
            "type": "number",
            "format": "double"
          }
        },
        "required": [
          "type",
          "westLng",
          "eastLng",
          "northLat",
          "southLat"
        ],
        "example": {
          "type": "Rectangle",
          "westLng": -120.1,
          "eastLng": -100.4,
          "northLat": 25.1,
          "southLat": 35.8
        }
      },
      "ReadingQuality": {
        "description": "Describes the quality and type of reading",
        "type": "string",
        "enum": [
          "Good",
          "Trace",
          "Poor",
          "Snow",
          "Lost",
          "Absent"
        ]
      },
      "ReadingFilter": {
        "description": "Criteria for a /Reading/getFiltered request",
        "allOf": [
          {
            "properties": {
              "readingIds": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DatabaseId"
                }
              },
              "quality": {
                "description": "Filter by reading quality. Omitted or null disables the filter, which is equivalent to listing all six qualities.\n",
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "$ref": "#/components/schemas/ReadingQuality"
                }
              },
              "timeWindowHour": {
                "description": "Filter by the hour the reading day ends: 7 for 7am-7am, 0 for midnight-to-midnight. Omitted or null disables the filter and returns BOTH windows, which for an automatic gauge means two rows per day. Pass [7] to get the classic 7am-7am series.\n",
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "integer",
                  "format": "int32",
                  "enum": [
                    0,
                    7
                  ]
                },
                "example": [
                  7
                ]
              },
              "pagination": {
                "$ref": "#/components/schemas/Pagination"
              }
            }
          },
          {
            "$ref": "#/components/schemas/GaugeRevisionFilter"
          }
        ]
      },
      "MonthReadingFilter": {
        "description": "Criteria for a /MonthReading/getFiltered request",
        "allOf": [
          {
            "properties": {
              "monthReadingIds": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DatabaseId"
                }
              },
              "pagination": {
                "$ref": "#/components/schemas/Pagination"
              }
            }
          },
          {
            "$ref": "#/components/schemas/GaugeRevisionFilter"
          }
        ]
      },
      "GaugeRevisionFilter": {
        "description": "Criteria for a /GaugeRevision/getFiltered request",
        "allOf": [
          {
            "properties": {
              "gaugeRevisionIds": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DatabaseId"
                }
              },
              "dateRangeStart": {
                "$ref": "#/components/schemas/ISO8601Date"
              },
              "dateRangeEnd": {
                "$ref": "#/components/schemas/ISO8601Date"
              },
              "region": {
                "$ref": "#/components/schemas/GeoRegion"
              },
              "gaugeType": {
                "description": "Filter by gauge type. Omitted or null disables the filter, which is equivalent to listing every type.\n",
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "$ref": "#/components/schemas/GaugeType"
                }
              },
              "currentOnly": {
                "description": "When true, return only the newest revision of each gauge.\n",
                "type": "boolean",
                "default": false
              },
              "newestInDateRangeOnly": {
                "description": "When true, and a date range is supplied, return only the newest revision of each gauge that was already in effect at dateRangeStart, rather than every revision created in the range. Ignored when both dateRangeStart and dateRangeEnd are omitted.\n",
                "type": "boolean",
                "default": false
              },
              "pagination": {
                "$ref": "#/components/schemas/Pagination"
              }
            }
          },
          {
            "$ref": "#/components/schemas/GaugeFilter"
          }
        ]
      },
      "GaugeFilter": {
        "description": "Criteria for a /Gauge/getFiltered request",
        "properties": {
          "gaugeIds": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DatabaseId"
            }
          },
          "userIds": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DatabaseId"
            }
          },
          "gaugeStatus": {
            "description": "Filter by gauge status. Omitted or null disables the filter and returns gauges of every status, including Inactive and InactiveHidden ones.\n",
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/GaugeStatus"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "PwsPayloadFilter": {
        "description": "Criteria for a /PwsPayload/getLatest request",
        "properties": {
          "gaugeIds": {
            "description": "The gauges to read. Each must be an automatic gauge.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DatabaseId"
            }
          },
          "format": {
            "description": "See the format parameter on the GET form.",
            "type": "string",
            "enum": [
              "raw",
              "json",
              "wu",
              "ecowitt"
            ],
            "default": "raw"
          },
          "at": {
            "description": "See the at parameter on the GET form.",
            "type": "string"
          },
          "includeStale": {
            "description": "See the includeStale parameter on the GET form.",
            "type": "boolean",
            "default": false
          }
        },
        "required": [
          "gaugeIds"
        ]
      },
      "PwsPayload": {
        "description": "A gauge's newest automatic observation. Fields the station did not report are omitted rather than sent as null, so a rain only station returns a small object. Units are as the station submitted them, which the field names state.\n",
        "properties": {
          "gaugeId": {
            "$ref": "#/components/schemas/DatabaseId"
          },
          "observedAt": {
            "description": "When the station says it made the observation, UTC.",
            "type": "string",
            "format": "date-time"
          },
          "quality": {
            "description": "Rainlog's quality verdict for the observation.",
            "type": "string",
            "enum": [
              "Good",
              "Poor",
              "Trace"
            ]
          },
          "fidelity": {
            "description": "Whether the stored payload was captured verbatim at upload time (`submitted`) or reconstructed during a backfill (`synthetic`). Absent when the observation has no payload row at all.\n",
            "type": "string",
            "enum": [
              "submitted",
              "synthetic"
            ]
          },
          "protocol": {
            "description": "Which upload protocol the observation arrived on. Absent when it cannot be determined, in which case `wu` and `ecowitt` are refused.\n",
            "type": "string",
            "enum": [
              "wu",
              "ecowitt"
            ]
          },
          "rainAmountIn": {
            "description": "The rain Rainlog records for this observation, inches.",
            "type": "number"
          },
          "rainIsTrace": {
            "type": "boolean"
          },
          "rainRateInPerHr": {
            "type": "number"
          },
          "rainEventIn": {
            "type": "number"
          },
          "rainHourlyIn": {
            "type": "number"
          },
          "rainDailyIn": {
            "type": "number"
          },
          "rainWeeklyIn": {
            "type": "number"
          },
          "rainMonthlyIn": {
            "type": "number"
          },
          "rainYearlyIn": {
            "type": "number"
          },
          "rainTotalIn": {
            "type": "number"
          },
          "rainPiezoRateInPerHr": {
            "description": "Piezo (WS85 / WS90) rain. Only an Ecowitt upload carries these.",
            "type": "number"
          },
          "rainPiezoEventIn": {
            "type": "number"
          },
          "rainPiezoHourlyIn": {
            "type": "number"
          },
          "rainPiezoDailyIn": {
            "type": "number"
          },
          "rainPiezoWeeklyIn": {
            "type": "number"
          },
          "rainPiezoMonthlyIn": {
            "type": "number"
          },
          "rainPiezoYearlyIn": {
            "type": "number"
          },
          "rainPiezoStart": {
            "type": "boolean"
          },
          "snowAccumulation": {
            "type": "number"
          },
          "snowIsTrace": {
            "type": "boolean"
          },
          "snowDepth": {
            "type": "number"
          },
          "tempF": {
            "type": "number"
          },
          "dewpointF": {
            "type": "number"
          },
          "humidity": {
            "type": "number"
          },
          "pressureInHg": {
            "description": "Sea level pressure. Ecowitt sends this as `baromrelin`.",
            "type": "number"
          },
          "pressureAbsInHg": {
            "description": "Station (absolute) pressure. The protocols reverse the word order: WU sends `absbaromin`, Ecowitt sends `baromabsin`.\n",
            "type": "number"
          },
          "rain24hIn": {
            "description": "Rain in the last 24 hours, a rolling window rather than the midnight-reset daily total. Ecowitt spells the parameter `last24hrainin`, the CWOP-derived convention uses `last24hrrainin`.\n",
            "type": "number"
          },
          "windChillF": {
            "type": "number"
          },
          "windSpeedMph": {
            "type": "number"
          },
          "windGustMph": {
            "type": "number"
          },
          "maxDailyGustMph": {
            "type": "number"
          },
          "windDir": {
            "description": "Degrees, 0 to 359.",
            "type": "integer"
          },
          "solarRadiation": {
            "type": "number"
          },
          "uvIndex": {
            "type": "number"
          },
          "indoorTempF": {
            "type": "number"
          },
          "indoorHumidity": {
            "type": "number"
          },
          "softwareType": {
            "description": "What the station calls its uploading software.",
            "type": "string"
          },
          "rawPayload": {
            "description": "The stored upload payload, redacted exactly as the global data export redacts it. The named fields above are a convenience over this, not a filter on it: anything Rainlog does not model is still here.\n",
            "type": "string"
          }
        },
        "required": [
          "gaugeId",
          "observedAt"
        ]
      },
      "Pagination": {
        "description": "Describes how to limit the result size",
        "properties": {
          "offset": {
            "description": "First element to return",
            "type": "integer",
            "default": 0
          },
          "limit": {
            "description": "Max number of elements to return. This value may not exceed 1000.",
            "type": "integer",
            "default": 1000,
            "example": 1000
          }
        }
      },
      "GaugeType": {
        "description": "Currently allowed types of gauges",
        "type": "string",
        "enum": [
          "TruChek",
          "SimpleCatch",
          "FunnelCatch",
          "FunnelCatchWithOverflow",
          "TippingBucketOrElectronic",
          "Other"
        ]
      },
      "GaugeStatus": {
        "description": "User specified activity state of gauge",
        "type": "string",
        "enum": [
          "Active",
          "Inactive",
          "InactiveHidden"
        ]
      },
      "Error": {
        "properties": {
          "code": {
            "description": "- ProtocolError: There was a problem with receiving or understanding the request\n- ServerError: The server had a general problem servicing the request. May also mean BadInput.\n- BadInput: Something in the input can not be processed or converted to the neccessary type. Double check the values submitted.\n",
            "type": "string",
            "enum": [
              "ProtocolError",
              "ServerError",
              "BadInput"
            ]
          },
          "message": {
            "description": "A plain English error message for the developer",
            "type": "string"
          }
        }
      }
    }
  }
}
