USGS NWIS Streamflow & Gage Height Monitor API by Site/State avatar

USGS NWIS Streamflow & Gage Height Monitor API by Site/State

Pricing

from $3.30 / 1,000 results

Go to Apify Store
USGS NWIS Streamflow & Gage Height Monitor API by Site/State

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

Kyle Maloney

Maintained by Community

Actor 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, site 09035820, 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 adds reading_age_hours, reading_age_days, is_stale and data_status, plus a siteStatus input and a maxReadingAgeHours filter.
  • 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" and above_threshold: null.
  • above_threshold said false when no threshold had been applied — asserting a negative it never evaluated. It is now null when nothing was checked, with threshold_applied telling you which happened. A real evaluation still returns a real true/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 -999999 sentinel, distinguishable only by a qualifier code. v1.0 dropped them all silently. data_status now 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

  1. Accepts a list of USGS site numbers OR a state code (every gaging site in the state).
  2. Pulls the latest instantaneous (iv) or daily (dv) value for one or more parameters (00060 discharge, 00065 gage height).
  3. Optionally widens the window (period, e.g. PT2H) so it can compute a change delta + direction (rising / falling / steady).
  4. Applies an optional global threshold or per-site thresholds and sets above_threshold + amount_over_threshold.
  5. onlyAlerts mode 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.

FieldDescription
site_noUSGS NWIS station (site) number
site_nameUSGS station name
agencyReporting agency code (USGS)
stateUSPS state code derived from the site's FIPS state code
state_fipsNumeric FIPS state code
county_fipsNumeric FIPS county code (state+county)
hucHydrologic Unit Code
lat / lonSite coordinates (EPSG:4326)
parameter_codeUSGS parameter code (00060 discharge, 00065 gage height)
parameter_nameHuman-readable parameter description
unitUnit of the value (ft3/s, ft)
valueLatest observed value. null when USGS returned its no-data sentinel
datetimeTimestamp of the latest reading, site local time
qualifierQualifier code(s) on the latest reading, comma-joined
provisionalTrue when the reading carries P (subject to revision)
prior_value / prior_datetimeThe reading immediately before the latest (needs a period)
changeLatest value minus prior value
change_directionrising / falling / steady
threshold_valueThe threshold applied to this site, if any
above_thresholdTrue/false when a threshold was genuinely evaluated; null when nothing was checked
amount_over_thresholdLatest value minus threshold (negative when below)
threshold_appliedWhether a threshold was actually evaluated against a real reading
reading_age_hours / reading_age_daysHow old the latest reading is at run time
is_staleTrue when older than stale_threshold_hours; null when not judged
stale_threshold_hoursThe staleness cut-off used on this run
data_statusWhy the row is or is not a usable current measurement (table below)
data_status_reasonPlain-language explanation, decoded from the USGS qualifier definitions
raw_valueThe value exactly as USGS returned it, including -999999
no_data_sentinelThe noDataValue USGS declared for this variable on this run
qualifier_codesQualifier codes as an array
qualifier_descriptionsDecoded qualifier meanings
unknown_qualifier_codesQualifier codes not in the known vocabulary — surfaced, not dropped
approvedReading carries A (approved for publication)
estimatedReading carries e (value estimated)
zero_flowReading carries ZFL — a genuinely dry channel
ice_affectedReading affected by ice at the gage
discontinuedUSGS flagged the series Dis — site discontinued
censoredReading carries < or > (true value only bounded)
statistic_code / statistic_name00000 for instantaneous; 00003 = daily Mean for dv
variable_nameUSGS short variable name, HTML entities decoded
variable_oidUSGS internal variable object id
series_idUSGS time-series id, agency:site:parameter:statistic
method_id / method_descriptionThe sensor/method that produced the reading
readings_in_windowHow many readings the requested period returned
value_block_countHow many sensor/method blocks the series carries (the first is flattened)
site_type_codeUSGS site type, e.g. ST = stream
time_zone / time_zone_offsetSite standard-time zone and UTC offset — datetime is site-local
daylight_savings_zone / site_uses_dstSite DST zone and whether it observes DST
site_returnedTrue when USGS returned a series for this site; false on a requested site it did not answer
source_statusok when USGS answered for this site, unavailable when it did not
source_errorWhy the source did not answer, when it did not

data_status values

ValueMeaning
okA current, usable reading
staleUsable, but older than stale_threshold_hours
estimatedCarries the e qualifier
zero_flowZFL — channel is dry. USGS reports this as -999999, not 0
ice_affectedIce — value affected by ice
equipment_malfunctionEqp
site_maintenanceMnt
discontinuedDis — site discontinued
rating_developmentRat — rating being developed or revised
out_of_seasonSsn — parameter monitored seasonally
temporarily_unavailable***
no_dataSentinel returned with no explanatory qualifier
no_readingSeries returned with no values at all
not_returnedYou 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 + declaredType must still be WaterML TimeSeriesResponseType.
  • 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 noDataValue USGS 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 99999999 must 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=99999 returns 0 series), which v1.0 reported as a green run with an empty dataset.
  • onlyAlerts with 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.

  • 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.