USGS NWIS Streamflow & Gage Height Monitor API by Site/State
Pricing
from $3.30 / 1,000 results
USGS NWIS Streamflow & Gage Height Monitor API by Site/State
USGS NWIS streamflow API: pull real-time river discharge (cfs) and gage height (ft) by site number or state. Flattens nested USGS WaterML-JSON into clean rows with change deltas and flood-stage threshold alerts. Keyless USGS Water Services data for irrigation, utilities, flood risk & hydrology.
Pricing
from $3.30 / 1,000 results
Rating
0.0
(0)
Developer
Kyle Maloney
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
6 days ago
Last modified
Share
USGS NWIS Streamflow & Gage Height Monitor
Real-time streamflow (discharge, cfs) and gage height (ft) from the U.S. Geological Survey National Water Information System (NWIS), delivered as clean, flat, one-row-per-site records. The actor pulls the deeply nested WaterML-in-JSON response, flattens it, computes the change vs the prior reading, and flags any site over a flood-stage / alert threshold you set. Keyless, public-domain USGS data.
This is a screening and monitoring tool, not a determination. It reports what USGS published; it does not certify flood risk.
What v1.1 fixes (read this if you used v1.0)
Every change is append-only — no field was removed or renamed. Four defects meant v1.0 could bill you for a confident wrong answer while the run reported SUCCESS:
- Decades-old readings were presented as current. USGS
siteStatus=all(the default) returns discontinued gages, and their most recent reading can be decades old. Measured live 2026-08-01: a Colorado statewide discharge pull returns 615 sites, of which 268 (43.6%) last reported over a year ago — the oldest, site09035820, on 1987-09-28. Alaska is 61.1%, Florida 38.0%. v1.0 emitted those as the current flow with no age signal. v1.1 addsreading_age_hours,reading_age_days,is_staleanddata_status, plus asiteStatusinput and amaxReadingAgeHoursfilter. - A requested gage USGS never answered for simply vanished. Asking for three sites where one is unknown returns two rows, HTTP 200, no error — so silence read as "no flood alert". v1.1 emits an explicit row with
data_status: "not_returned"andabove_threshold: null. above_thresholdsaidfalsewhen no threshold had been applied — asserting a negative it never evaluated. It is nownullwhen nothing was checked, withthreshold_appliedtelling you which happened. A real evaluation still returns a realtrue/false.- Six different things looked identical. USGS collapses zero flow, ice, equipment malfunction, site maintenance, discontinuation, rating development, seasonal shutdown and "temporarily unavailable" onto the same
-999999sentinel, distinguishable only by a qualifier code. v1.0 dropped them all silently.data_statusnow names each one.
Who it's for
- Agriculture & irrigation districts — watch intake streams and canals for low/high flow.
- Water utilities & dam operators — track river stage and discharge at supply and downstream points.
- Flood / P&C insurers & risk teams — threshold alerts on rising gages near insured exposure.
- Environmental consultants & hydrologists — normalized, time-stamped, qualifier-decoded readings.
- Researchers & data engineers — skip the WaterML parsing; get tidy rows and deltas.
What it does
- Accepts a list of USGS site numbers OR a state code (every gaging site in the state).
- Pulls the latest instantaneous (
iv) or daily (dv) value for one or more parameters (00060 discharge, 00065 gage height). - Optionally widens the window (
period, e.g.PT2H) so it can compute a change delta + direction (rising / falling / steady). - Applies an optional global threshold or per-site thresholds and sets
above_threshold+amount_over_threshold. onlyAlertsmode returns only sites at or above their threshold — plus any requested site USGS did not answer for, so silence is never mistaken for an all-clear.
Example input
{"sites": ["01646500", "09380000", "06752260"],"parameterCd": "00060","service": "iv","period": "PT2H","thresholdValue": "5000","onlyAlerts": false}
Statewide flood watch on gages USGS still operates, last 24 hours only:
{ "stateCd": "VA", "parameterCd": "00065", "thresholdValue": "10", "siteStatus": "active", "maxReadingAgeHours": 24, "onlyAlerts": true }
Output fields
Every field the actor declares, in the order it appears on the row.
| Field | Description |
|---|---|
site_no | USGS NWIS station (site) number |
site_name | USGS station name |
agency | Reporting agency code (USGS) |
state | USPS state code derived from the site's FIPS state code |
state_fips | Numeric FIPS state code |
county_fips | Numeric FIPS county code (state+county) |
huc | Hydrologic Unit Code |
lat / lon | Site coordinates (EPSG:4326) |
parameter_code | USGS parameter code (00060 discharge, 00065 gage height) |
parameter_name | Human-readable parameter description |
unit | Unit of the value (ft3/s, ft) |
value | Latest observed value. null when USGS returned its no-data sentinel |
datetime | Timestamp of the latest reading, site local time |
qualifier | Qualifier code(s) on the latest reading, comma-joined |
provisional | True when the reading carries P (subject to revision) |
prior_value / prior_datetime | The reading immediately before the latest (needs a period) |
change | Latest value minus prior value |
change_direction | rising / falling / steady |
threshold_value | The threshold applied to this site, if any |
above_threshold | True/false when a threshold was genuinely evaluated; null when nothing was checked |
amount_over_threshold | Latest value minus threshold (negative when below) |
threshold_applied | Whether a threshold was actually evaluated against a real reading |
reading_age_hours / reading_age_days | How old the latest reading is at run time |
is_stale | True when older than stale_threshold_hours; null when not judged |
stale_threshold_hours | The staleness cut-off used on this run |
data_status | Why the row is or is not a usable current measurement (table below) |
data_status_reason | Plain-language explanation, decoded from the USGS qualifier definitions |
raw_value | The value exactly as USGS returned it, including -999999 |
no_data_sentinel | The noDataValue USGS declared for this variable on this run |
qualifier_codes | Qualifier codes as an array |
qualifier_descriptions | Decoded qualifier meanings |
unknown_qualifier_codes | Qualifier codes not in the known vocabulary — surfaced, not dropped |
approved | Reading carries A (approved for publication) |
estimated | Reading carries e (value estimated) |
zero_flow | Reading carries ZFL — a genuinely dry channel |
ice_affected | Reading affected by ice at the gage |
discontinued | USGS flagged the series Dis — site discontinued |
censored | Reading carries < or > (true value only bounded) |
statistic_code / statistic_name | 00000 for instantaneous; 00003 = daily Mean for dv |
variable_name | USGS short variable name, HTML entities decoded |
variable_oid | USGS internal variable object id |
series_id | USGS time-series id, agency:site:parameter:statistic |
method_id / method_description | The sensor/method that produced the reading |
readings_in_window | How many readings the requested period returned |
value_block_count | How many sensor/method blocks the series carries (the first is flattened) |
site_type_code | USGS site type, e.g. ST = stream |
time_zone / time_zone_offset | Site standard-time zone and UTC offset — datetime is site-local |
daylight_savings_zone / site_uses_dst | Site DST zone and whether it observes DST |
site_returned | True when USGS returned a series for this site; false on a requested site it did not answer |
source_status | ok when USGS answered for this site, unavailable when it did not |
source_error | Why the source did not answer, when it did not |
data_status values
| Value | Meaning |
|---|---|
ok | A current, usable reading |
stale | Usable, but older than stale_threshold_hours |
estimated | Carries the e qualifier |
zero_flow | ZFL — channel is dry. USGS reports this as -999999, not 0 |
ice_affected | Ice — value affected by ice |
equipment_malfunction | Eqp |
site_maintenance | Mnt |
discontinued | Dis — site discontinued |
rating_development | Rat — rating being developed or revised |
out_of_season | Ssn — parameter monitored seasonally |
temporarily_unavailable | *** |
no_data | Sentinel returned with no explanatory qualifier |
no_reading | Series returned with no values at all |
not_returned | You asked about this site and USGS returned nothing. Not a verified all-clear |
Fields that are legitimately null on a healthy run, with an input that populates them: prior_value / prior_datetime / change / change_direction need a period (e.g. "PT2H"); threshold_value / above_threshold / amount_over_threshold need thresholdValue; zero_flow / discontinued / raw_value = "-999999" need includeNoDataSites: true on a state pull (e.g. stateCd: "CO"); source_error populates only when a requested site is not returned; method_description is published sparsely by USGS (populated on 4 of 1,236 live rows measured).
Reliability: what makes this fail loudly instead of answering wrongly
A crash is cheap — you see red and retry. A confident wrong answer is expensive. Every run checks the live upstream before any billable row is written:
- Envelope +
declaredTypemust still be WaterMLTimeSeriesResponseType. - Echoed-filter check — USGS echoes the site and parameter filters back in
queryInfo.criteria. If a filter is not echoed, or a series comes back for a site you did not ask about, the run fails rather than answering about the wrong gage. - Sentinel drift — the
noDataValueUSGS declares per variable must still be-999999. If USGS changes it, an unguarded actor would publish the sentinel as a real discharge measurement. - Positive canary — site
01646500(Potomac at Little Falls), a permanent gage, must return a finite reading no more than 30 days old. - Negative control — the non-existent site
99999999must return zero series. If it ever returns data, the USGS site filter is not being applied and every result would be about the wrong gages: the run fails immediately. - No silent zero — a query that returns nothing at all fails with an explanation. A typo'd parameter code returns HTTP 200 with an empty result set (verified:
parameterCd=99999returns 0 series), which v1.0 reported as a green run with an empty dataset. onlyAlertswith no threshold is rejected up front, because nothing could ever be over threshold and the empty dataset would read as "no flood alerts".
Every measured value is logged on every run, so the bands can be tightened on evidence.
Upstream sunset
USGS states that WaterServices will be decommissioned in early 2027, with applications migrating to https://api.waterdata.usgs.gov. The legacy endpoint is fully functional today and is what this actor uses. The envelope check above means a format change surfaces as a loud failure, not as wrong numbers.
Use as an MCP tool
This actor is callable by AI agents (Claude, Cursor, etc.) via mcp.apify.com. Its input and output schemas carry field-level descriptions, so an agent can chain it: "get the latest Potomac River streamflow and tell me if it is above 25,000 cfs" maps directly to sites + thresholdValue. Agents should read data_status and source_status before treating a row as an answer — above_threshold: null means not checked, never clear.
Pricing
Pay per result: $6 per 1,000 rows, one row per site and parameter. Graduated discounts apply on paid Apify plans. There are no platform-usage charges beyond Apify's own compute. A run that fails a reliability gate emits nothing and bills nothing.
FAQ
Where does the data come from? USGS Water Services (waterservices.usgs.gov/nwis/iv and /dv), public domain, no API key.
How do I find a site number? Use the USGS site map or NWIS. Examples: 01646500 = Potomac River near Washington DC (Little Falls); 09380000 = Colorado River at Lees Ferry; 06752260 = Cache la Poudre at Fort Collins.
Why did a site come back with data_status: "not_returned"? USGS returned no series for it. Usually the site number is wrong, or that gage does not measure the parameter you asked for, or it reported nothing in your period. It is deliberately not treated as "no exceedance".
Why is value null when raw_value is -999999? That is the USGS no-data sentinel, not a measurement. data_status tells you which of the eight reasons applies — a dry channel (zero_flow) is a very different fact from a broken sensor (equipment_malfunction).
Why is my statewide pull full of old readings? siteStatus defaults to all, which includes discontinued gages. Set siteStatus: "active", or filter with maxReadingAgeHours. Check is_stale on every row.
Why is above_threshold null? No threshold was applied, or there was no reading to compare. null means not checked; false means checked and below.
Is a dv value the same as an iv value? No. dv returns a daily statistic — statistic_name is normally Mean. iv is a spot reading.
Related actors
- USACE Levee Flood Risk Screener — leveed-area intersection for a site.
- FEMA NRI County Risk Profile — county-level riverine flood and other peril risk.
- NFIP Flood Loss Risk Screener — historical paid flood-claim exposure near a coordinate.
- NOAA Storm Events Peril Climatology — county-level flood and storm event history.