# US River Water Level API - Official USGS Streamflow Gauges (`neverempty/us-river-water-levels`) Actor

Live river water level, streamflow and water temperature from official USGS gauges - no API key. Every reading carries its USGS quality codes in plain English, so you can tell an approved measurement from a provisional one, a frozen gauge or a dry channel. -999999 is never returned as a level.

- **URL**: https://apify.com/neverempty/us-river-water-levels.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** News, Developer tools, MCP servers
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 gauge reading returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## US River Water Level API - Official USGS Streamflow Gauges

**Live river level, streamflow and water temperature from the government's own gauges - with the three facts you need to know whether a number is usable: how old it is, whether the sensor was working, and whether it will be revised.**

This reads **USGS Water Services** (`waterservices.usgs.gov`), the official public API of the US Geological Survey. It is a work of the US government: public domain, no API key, no rate-limit contract, no terms that forbid redistribution. Nothing here parses a website. No proxies required.

### Why not just call the API yourself

Four traps. Each one turns into a wrong number in your system without anything looking broken. All four were measured on live data across **55 US states and territories, 31,752 time series** on 2026-08-30.

#### 1. `-999999` is the missing marker - and it is not the only bad number

`-999999` appeared in **2.64%** of readings. A naive client reports a river at minus 999,999 feet.

But filtering only for that is not enough: real responses also contain values like `-100000` and a water temperature of **-120 °C**. This Actor checks each measurement against a physical range as well, so `waterTemperature` outside roughly -3 to 70 °C is reported as missing rather than as a reading.

**Careful:** large *negative streamflow* is real - it is tidal backflow (Savannah River at Fort Pulaski runs to -218,000 ft³/s). A client that simply discards big negative numbers destroys real data. The range is set per measurement, not per sign.

#### 2. Freshness is a separate question from quality - and it bites hard here

**26.6% of live readings across the country are more than a year old.** The service returns them with no warning. The oldest gauge in the Arizona sample was last read **13,847 days ago** and still comes back in a request for current conditions.

Every measurement therefore carries `...AgeHours`, and every row carries `freshestAgeHours`. If you are monitoring for flooding, filter on those.

> **A warning worth spelling out.** USGS quality code `A` means "approved for publication". It is tempting to treat `A` as "trustworthy". **Do not.** In the instantaneous-value service, approval only happens after review, so `A` is attached to *historical* records: of 2,207 `A` readings measured, **100% were more than 24 hours old** and the oldest was 45 years old. Almost every current reading is `P` (provisional). Filtering for "approved" data leaves you with no current data at all. This Actor therefore does **not** publish a single "trusted" flag - it gives you age, suspect and provisional separately, and lets you decide.

#### 3. Quality codes decide whether the number means anything

A gauge can be iced over, dry, under maintenance, or broken. `...IsSuspect` is `true` when any code says the measurement itself was affected, and `...QualifierMeanings` spells out why in English.

| Code | Meaning | Suspect |
|---|---|---|
| `A` Approved for publication | (see the warning above - approved usually means old) | no |
| `P` Provisional - subject to revision | the normal state of current data | no |
| `R` Revised record | | no |
| `Dry` The channel is dry at this site | | no |
| `ZFL` Zero flow | | no |
| `e` Estimated | | **yes** |
| `Ice` Value affected by ice at the site | | **yes** |
| `Eqp` Value affected by equipment malfunction | | **yes** |
| `Mnt` Site under maintenance | | **yes** |
| `Fld` Value affected by flood damage | | **yes** |
| `Bkw` Value affected by backwater | | **yes** |
| `Rat` Rating being developed or revised | | **yes** |
| `Ssn` Monitored seasonally - not measured now | | **yes** |
| `Dis` Record discontinued at this site | | **yes** |
| `***` Temporarily unavailable | | **yes** |
| `<` Actual value is less than the value shown | | **yes** |

Any code not in this list is reported as `Unknown USGS qualifier '<code>'` **and treated as suspect**, so a new code can never quietly pass as a clean reading.

#### 4. One gauge can have several sensors, and the API does not put the good one first

USGS returns a separate block per measurement method - Primary, Secondary, Bubbler, Radar, Upper Stage Sensor. Measured nationwide: **720 series carry more than one block, 612 of them disagree on the value, and in 11 the first block is missing while another sensor has a real reading.** One gauge reported no water level at all while its radar sensor was reading 11.11 ft.

This Actor picks the freshest block that actually has a reading, and tells you which one it used in `...Sensor`.

### What you get

One row per gauge site, with the same columns every time - a site that does not measure something gets `null`, never a zero.

| Field | Example | Notes |
|---|---|---|
| `siteCode` / `siteName` | `09379025` / `CHINLE CREEK AT CHINLE, AZ` | As USGS names it |
| `latitude` / `longitude` | `36.155` / `-109.5375` | |
| `stateFips` / `countyFips` | `04` / `04001` | **FIPS numbers, not `AZ`** - named for what they actually are |
| `timeZone` | `MST` | |
| **`streamflow`** / `gageHeight` / `waterTemperature` + `...Unit` | `0` / `2.2` / `null` | ft³/s, ft, °C |
| **`...AgeHours`** | **`1.6`** | How old this measurement is, in hours |
| `...ObservedAt` | `2026-08-30T03:00:00.000-07:00` | With offset |
| `...IsMissing` | `false` | `-999999` or physically impossible |
| **`...IsSuspect`** | **`false`** | The measurement itself was affected |
| **`...IsProvisional`** | **`true`** | Will be revised later |
| `...Qualifiers` / `...QualifierMeanings` | `P` / `Provisional - subject to revision` | Raw codes and plain English |
| `...Sensor` | `Radar` | Which sensor block was used, when the gauge has several |
| `freshestAgeHours` | `1.6` | Age of the newest measurement on this row |
| `parametersObserved` / `parametersTotal` | `2` / `3` | How much this gauge actually reported |
| `source` | `USGS Water Services NWIS (official, public domain)` | |

#### A real row

```json
{
  "siteCode": "09379025",
  "siteName": "CHINLE CREEK AT CHINLE, AZ",
  "latitude": 36.155,
  "longitude": -109.5375,
  "stateFips": "04",
  "timeZone": "MST",
  "streamflow": 0,
  "streamflowUnit": "ft3/s",
  "streamflowObservedAt": "2026-08-30T03:00:00.000-07:00",
  "streamflowAgeHours": 1.6,
  "streamflowIsMissing": false,
  "streamflowQualifiers": "P",
  "streamflowQualifierMeanings": "Provisional - subject to revision",
  "streamflowIsProvisional": true,
  "streamflowIsSuspect": false,
  "gageHeight": 2.2,
  "gageHeightUnit": "ft",
  "gageHeightAgeHours": 1.6,
  "freshestAgeHours": 1.6,
  "parametersObserved": 2,
  "parametersTotal": 3,
  "source": "USGS Water Services NWIS (official, public domain)"
}
```

### Input

```json
{
  "sites": ["01646500", "01463500", "07010000", "05331000", "06934500",
            "03294500", "02358000", "08279500", "14105700", "12510500"],
  "states": [],
  "parameterCodes": ["00060", "00065", "00010"],
  "maxRetries": 4
}
```

- **`sites`** - USGS gauge site numbers, 8 to 15 digits **including the leading zero**. Find them at `https://waterdata.usgs.gov/nwis/rt`. Defaults to ten major US river gauges.
- **`states`** - two letter state codes such as `vt`, `tx`. Returns every active gauge in those states, which can be several hundred rows per state. **Takes priority over `sites`.**
- **`parameterCodes`** - `00060` streamflow, `00065` gage height, `00010` water temperature. Anything else is ignored so the columns stay identical on every row.
- **`maxRetries`** - attempts when USGS returns a server error.

### What it does when something is wrong

It tells you, instead of returning a clean-looking empty result.

- **If none of the site numbers or state codes you gave are usable, the run fails and says so.** It does **not** quietly fall back to the default gauges and charge you for rivers you did not ask about. (`1646500` with the leading zero dropped is a realistic typo, and it is rejected rather than substituted.)
- **A `4xx` from USGS is reported as a rejected request, not as an outage**, and is not retried - retrying a bad site number just wastes your time.
- **USGS Water Services returns HTTP 503 often** (2 of the first 3 calls on the morning this was built). That is retried, and if it still fails the run **fails with the reason** in `RUN_ISSUES` rather than delivering an empty dataset.
- A response whose shape is not what USGS documents **throws** instead of returning zero rows.
- If you set a maximum total charge and the run hits it, the run **stops and says the result is incomplete**.

### Pricing

Pay per result: one charge per gauge site row delivered. Failed runs deliver nothing and charge nothing.

### Source and licence

All data comes from the **US Geological Survey Water Services** (`waterservices.usgs.gov`), a work of the US federal government and therefore in the public domain. This Actor adds the flattening, the missing-value and range handling, the sensor selection, the freshness figures and the quality-code translation described above.

# Actor input Schema

## `sites` (type: `array`):

USGS gauge site numbers, for example "01646500" (Potomac River near Washington DC). Find them at https://waterdata.usgs.gov/nwis/rt . Leave the default to get ten major US river gauges. Ignored if you fill in States.

## `states` (type: `array`):

Two letter state codes such as "vt", "tx". Returns every active gauge in those states, which can be several hundred rows per state. Takes priority over Site numbers.

## `parameterCodes` (type: `array`):

USGS parameter codes. 00060 = streamflow (cubic feet per second), 00065 = gage height (feet), 00010 = water temperature (Celsius). Anything else is ignored so the columns stay the same on every row.

## `maxRetries` (type: `integer`):

How many times to retry when USGS Water Services returns an error. It returns HTTP 503 fairly often, so more than one attempt is worth it.

## Actor input object example

```json
{
  "sites": [
    "01646500",
    "01463500",
    "07010000",
    "05331000",
    "06934500",
    "03294500",
    "02358000",
    "08279500",
    "14105700",
    "12510500"
  ],
  "states": [],
  "parameterCodes": [
    "00060",
    "00065",
    "00010"
  ],
  "maxRetries": 4
}
```

# Actor output Schema

## `results` (type: `string`):

One row per USGS gauge site produced by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/us-river-water-levels").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/us-river-water-levels").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call neverempty/us-river-water-levels --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/us-river-water-levels"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/DV6l2IzbLLCa9dfnt/builds/kbhX2pFgAcTfJ0L9j/openapi.json
