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

12 days ago

Last modified

Share

USGS Streamflow & Gage Height Monitor

Real-time streamflow (discharge, cfs) and gage height (ft) from the U.S. Geological Survey, delivered as clean, flat, one-row-per-site records. The actor reads the USGS OGC API at api.waterdata.usgs.gov, flattens it, computes the change vs the prior reading, and flags any gage 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.

v1.1 — migrated off the retiring USGS endpoint (2026-08-20)

USGS is decommissioning the legacy NWIS Water Services host. Its gwlevels sibling was switched off on 2026-06-01, and /nwis/iv and /nwis/dv are announced for retirement in late 2026 or early 2027. This actor now reads the successor API and no longer touches the legacy host at all.

The migration was done deliberately while both endpoints still worked, so the old and new output could be compared directly — which is impossible once the old one is gone.

What the comparison showed

Measured 2026-08-20, Colorado statewide discharge, every gage:

Legacy /nwis/ivSuccessor OGC API
Gages returned615615 (set difference zero, both directions)
Identical values599 of 615
Readings older than 1 year268268
Gages with no usable reading1313
Future-dated (corrupt) timestamps10

Of the 16 values that differed, 13 were encoding only — the legacy feed marked "no data" with the numeric sentinel -999999, the successor uses JSON null. Of the 3 genuine differences, two were the successor being one refresh cycle fresher, and one was a legacy data defect: site 09078141 was published with a timestamp of 2027-02-10, 175 days in the future.

A 20-gage spot check across the country returned 17 of 17 identical values with zero timestamp offset.

What changed on your rows

Nothing was removed or renamed — every one of the 58 v1.0 fields is still there, and 26 new ones were added. Three differences are worth knowing about:

  • datetime is now UTC (2026-08-20T05:50:00+00:00) where the legacy feed rendered the gage's local time with an offset (2026-08-20T01:50:00.000-04:00). Both are ISO 8601 and parse to the identical instant — verified on 615 of 615 Colorado gages. Only the rendering changed.
  • statistic_code reads 00011 for instantaneous values where the legacy feed said 00000. Same measurement, different USGS code. Daily mean is 00003 on both.
  • no_data_sentinel now reads null because the successor API has no numeric sentinel. no_data_representation carries the truth, and the column was kept rather than deleted.

qualifier_codes still uses the short legacy spellings (ZFL, Dis, e, Ice…) so filters written against v1.0 keep working; qualifier_codes_source carries the successor's own uppercase tokens (ZEROFLOW, DISCONTINUED, ESTIMATED, ICE…).

One behaviour change to be aware of

A reading that cannot describe conditions now no longer gets a threshold verdict. When a reading is stale, future-dated, or flagged untrustworthy by USGS itself (TEST, EQUIP, MAINT, UNAVAIL, RATINGDEV, FORCEINTERPOLATION), above_threshold is null, not false, and threshold_verdict_suppressed_reason explains why in plain language.

This matters at real volume: 268 of 615 Colorado gages carry a "latest" reading over a year old, the oldest from 1987. Publishing above_threshold: false on those asserts "checked and clear" about a gage nobody has read in decades. Publishing true would be a false alarm off a 38-year-old number.

Nothing is hidden. The value, its timestamp, its age, and the comparison that would have been made all still ship — the last as above_threshold_ignoring_staleness. If you were relying on the old behaviour, that field reproduces it exactly. Only the word "verdict" is withheld, and only where it would have been a guess.

No fallback to the legacy endpoint was kept, deliberately. A fallback that returns a stale or partial answer while looking healthy is worse than none — and during this migration the legacy host returned HTTP 503 on 6 of ~14 requests (4 consecutive on one statewide pull) while the successor answered 100%. Where the two disagreed, the legacy host was the one that was wrong or staler. If the successor API is unreachable, this actor fails loudly and bills nothing.

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 GeoJSON and the metadata join; 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 continuous (iv) or daily (dv) value for one or more parameters (00060 discharge, 00065 gage height).
  3. Joins each gage to its USGS site description — name, county, HUC, time zone, drainage area, altitude.
  4. Optionally widens the window (period, e.g. PT2H) so it can compute a change delta + direction (rising / falling / steady).
  5. Applies an optional global threshold or per-site thresholds and sets above_threshold + amount_over_threshold.
  6. onlyAlerts mode returns only gages at or above their threshold — plus any requested gage 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 that are still reporting, 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.
stateUSPS state code derived from the site's FIPS state code.
state_fipsNumeric FIPS state code from NWIS siteProperty.
county_fipsFive-digit state+county FIPS code, reconstructed from the successor API's separate state_code and three-digit county_code so the v1.0 format is preserved. Null when the site-metadata lookup was unavailable.
hucHydrologic Unit Code for the site.
latSite latitude (EPSG:4326).
lonSite longitude (EPSG:4326).
parameter_codeUSGS parameter code (00060=discharge, 00065=gage height).
parameter_nameHuman-readable parameter description.
unitUnit of the value.
valueLatest observed value for the parameter.
datetimeTimestamp of the reading, ISO 8601 UTC (e.g. 2026-08-20T05:50:00+00:00). The retired legacy feed rendered the same instant in the gage's local time with an offset; both parse to the identical instant, verified on 615 of 615 Colorado gages.
qualifierData qualifier code(s) on the latest reading (e.g. P=provisional).
provisionalTrue when the latest reading is provisional (subject to revision).
prior_valueThe reading immediately before the latest (only when a look-back period is set).
prior_datetimeTimestamp of the prior reading.
changeLatest value minus prior value.
change_directionrising, falling, or steady versus the prior reading.
threshold_valueThe flood-stage / alert threshold applied to this site, if any.
above_thresholdTrue when the latest reading is at or above the threshold; false when it is below. NULL when no threshold was supplied, when there is no reading, or when the reading cannot support a current verdict (stale, future-dated, or flagged untrustworthy by USGS) - null means NOT CHECKED, never checked-and-clear. See threshold_verdict_suppressed_reason and above_threshold_ignoring_staleness.
amount_over_thresholdLatest value minus threshold (negative when below).
threshold_appliedTrue when a threshold was actually evaluated against a real reading. When false, above_threshold is null (not checked) rather than false.
reading_age_hoursHow old the latest reading is, in hours, at run time.
reading_age_daysHow old the latest reading is, in days, at run time.
is_staleTrue when the latest reading is older than the stale threshold. null when there is no timestamp to judge.
stale_threshold_hoursThe staleness cut-off applied on this run.
data_statusWhy this row is or is not a usable current measurement: ok, stale, estimated, zero_flow, ice_affected, equipment_malfunction, site_maintenance, discontinued, rating_development, out_of_season, temporarily_unavailable, no_data, no_reading, not_returned.
data_status_reasonPlain-language explanation of data_status, decoded from the USGS qualifier definitions.
raw_valueThe value exactly as USGS returned it, including the -999999 no-data sentinel.
no_data_sentinelLegacy numeric no-data sentinel (-999999). The successor API does not use one - it publishes JSON null - so this column now reads null on every row. Kept so no field was removed. See no_data_representation.
qualifier_codesUSGS qualifier codes for the reading, mapped to the short legacy spellings (ZFL, Dis, e, Ice, Eqp, Mnt, Rat, Ssn, Bkw, <, ***) so downstream filters written against v1.0 keep working. qualifier_codes_source carries the successor API's own uppercase tokens.
qualifier_descriptionsDecoded qualifier meanings from the USGS qualifier definition block.
unknown_qualifier_codesQualifier codes not in the known USGS vocabulary. Surfaced rather than silently ignored.
approvedTrue when the reading carries the A qualifier (approved for publication).
estimatedTrue when the reading carries the e qualifier (value has been estimated).
zero_flowTrue when USGS flagged the reading ZFL - a genuinely dry channel, reported as the -999999 sentinel.
ice_affectedTrue when the reading is affected by ice at the gage.
discontinuedTrue when USGS flagged the series Dis - site discontinued.
censoredTrue when the reading carries a < or > qualifier (actual value known only to be below/above the reported one).
statistic_codeUSGS statistic code for the series. 00011 = instantaneous (continuous collections), 00003 = daily mean. Note the retired legacy feed reported instantaneous values as 00000; the successor API reports the same measurement as 00011.
statistic_nameHuman-readable statistic, e.g. Mean.
variable_nameUSGS short variable name with HTML entities decoded.
variable_oidLegacy WaterML internal variable id. Not published by the successor API, so this reads null; time_series_id is the successor identifier.
series_idUSGS time-series identifier, agency:site:parameter:statistic.
method_idLegacy WaterML method id. Not published by the successor API's values collections, so this reads null.
method_descriptionLegacy WaterML method description. Not published by the successor API's values collections, so this reads null.
readings_in_windowHow many readings the requested period returned for this series.
value_block_countHow many value blocks (sensor methods) the series carries. Only the first is flattened.
site_type_codeUSGS site type, e.g. ST = stream.
time_zoneSite standard-time zone abbreviation. datetime is site-local.
time_zone_offsetSite standard-time UTC offset.
daylight_savings_zoneSite daylight-savings zone abbreviation.
site_uses_dstWhether the site observes daylight savings time.
site_returnedTrue when USGS returned a time series for this site. False on a requested site it did not answer for.
source_statusPer-source outcome: ok when USGS answered for this site, unavailable when it did not.
source_errorWhy the source did not answer, when it did not.
api_sourceWhich USGS API produced this row. Always "usgs_ogcapi_v0" since v1.1 (2026-08-20), when this actor migrated off the legacy NWIS Water Services host ahead of its decommissioning.
api_hostThe host this row was read from.
api_collectionThe OGC API collection queried: latest-continuous (instantaneous), latest-daily, continuous or daily (the windowed variants, used when a look-back period is supplied).
retired_api_hostThe legacy USGS host this actor no longer reads, published for provenance. waterservices.usgs.gov is being decommissioned; its gwlevels sibling was already switched off on 2026-06-01.
monitoring_location_idThe USGS OGC API identifier for the gage, agency-prefixed (e.g. USGS-01646500). site_no carries the bare station number.
time_series_idStable USGS identifier for this gage-and-parameter time series.
feature_idThe GeoJSON feature id for this specific reading.
approval_statusUSGS review state of the reading: "Provisional" (subject to revision) or "Approved" (review complete). On the legacy feed this was carried as qualifier codes P and A; the successor API gives it its own field. The provisional and approved booleans are derived from this.
last_modifiedWhen USGS last modified this reading record (UTC). Distinct from datetime, which is when the measurement was taken.
unit_sourceThe unit string exactly as the USGS values collection publishes it ("ft^3/s"). The unit column carries the normalised spelling ("ft3/s"), which is what the legacy feed and the USGS parameter-codes reference table both use.
qualifier_codes_sourceThe qualifier tokens exactly as the USGS OGC API publishes them (ZEROFLOW, DISCONTINUED, ESTIMATED, ICE, EQUIP, MAINT, UNAVAIL, RATINGDEV, SEASONAL, BACKWATER, DRY, BLWMIN, LESSTHAN, FORCEINTERPOLATION, TEST). qualifier_codes carries the same information mapped to the short legacy codes for backward compatibility.
no_data_representationHow the source marks a missing reading. The successor API uses JSON null ("json_null"); the retired legacy feed used the numeric sentinel -999999. This is why no_data_sentinel now reads null.
reading_is_future_datedTrue when the gage reported a timestamp AHEAD of the run time, which means its age cannot be computed and it must not be treated as current. Measured once on the retired legacy feed (site 09078141 stamped 2027-02-10, 175 days ahead) and zero times across 4,689 rows on the successor API.
above_threshold_ignoring_stalenessThe raw value-versus-threshold comparison, made regardless of how old the reading is or what USGS flagged it. Published so nothing is lost when above_threshold is withheld. Do NOT use this as a current flood alert - that is exactly what above_threshold is for.
threshold_verdict_suppressedTrue when a threshold was supplied but no current verdict is published, because the reading is stale, future-dated, flagged untrustworthy by USGS, or absent. In that case above_threshold is null (not checked) rather than false (checked and clear).
threshold_verdict_suppressed_reasonPlain-language explanation of why above_threshold is null on this row. Null when a verdict was published or when no threshold was supplied.
site_metadata_statusWhether the separate site-description lookup succeeded for this gage: "ok", "unavailable" (the lookup failed or did not cover this site, so site_name / county_fips / huc / time zone are null and are NOT verified absences), or "not_applicable".
state_nameFull state name for the gage.
county_nameCounty name for the gage.
site_typePlain-language USGS site type, e.g. "Stream".
drainage_area_sq_miDrainage area upstream of the gage, in square miles. Newly available from the successor API - the legacy feed did not publish it.
altitude_ftGage datum altitude in feet relative to vertical_datum. Newly available from the successor API.
vertical_datumVertical datum the altitude is measured against, e.g. NAVD88.
pages_fetchedHow many API pages this run read to assemble the result. The successor API publishes no total-count field, so completeness is established by paging until the server stops offering a next link; this column lets you see the shape of what you got.
sites_in_responseHow many distinct gages the API returned for this query.
results_truncatedTrue when the API still had more pages but this run reached its page budget, so the result set is INCOMPLETE. False when the API confirmed there was nothing more to fetch.

data_status values

ValueMeaning
okA current, usable reading.
estimatedUSGS flagged the value as estimated. Still a usable measurement.
staleThe reading is older than your staleHours threshold. The threshold verdict is withheld.
future_datedThe gage reported a timestamp ahead of the run time, so its age cannot be trusted. Verdict withheld.
test_dataUSGS marked this as test data. Never a real measurement; verdict withheld.
zero_flowUSGS reports zero flow at the gage.
dryThe site is dry — no water at the gage.
ice_affectedThe reading is affected by ice.
equipment_malfunctionEquipment malfunction. Verdict withheld.
site_maintenanceSite maintenance. Verdict withheld.
temporarily_unavailableUSGS reports the value as temporarily unavailable. Verdict withheld.
discontinuedThe gage is discontinued.
rating_developmentThe rating curve is being developed or revised. Verdict withheld.
out_of_seasonThe parameter is monitored seasonally and is out of season.
backwater_affectedThe reading is affected by backwater.
below_minimum_reportableThe value is below the gage's minimum reportable limit.
no_readingNo value was returned and USGS gave no reason.
not_returnedYou asked about this gage and USGS returned nothing at all. This is not a verified absence of an exceedance.

Reliability: what makes this fail loudly instead of answering wrongly

  • A live drift gate runs before any billable row. Three probes, each with its own error boundary, each returning one of three outcomes — pass, fail, or unavailable. A probe that could not be reached is not treated as a probe that failed.
    • Positive canary (load-bearing): USGS 01646500, Potomac River at Little Falls — a gage with a continuous record back to 1930 — must return a plausible discharge in the expected unit with a recent timestamp. This is what catches a unit change or a decimal shift, the two failures that would silently corrupt every threshold comparison.
    • Negative control: a non-existent site number must return zero gages. If it ever returns data, the site filter is not being applied and every row would be about the wrong gage.
    • Qualifier shape: the qualifier field must stay an array. If it became a string, every ICE / EQUIP / TEST flag would silently read as "no qualifier".
  • Rate limits are treated as outages, not as data problems. This API is fronted by api.data.gov at 1,000 requests/hour per IP, and Apify runs from shared IPs — an unrelated tenant can rate-limit us at any moment. A 429 is retried, never reported as a change in the data.
  • Completeness is established positively. This API publishes no total-count field, so there is no free truncation check. Paging continues until the server itself stops offering a next page; a repeated cursor is a hard failure; and if a page budget is ever reached, every row carries results_truncated: true rather than the result being quietly capped. When you name specific gages, completeness is provable — every one gets a row, answered or not.
  • null means "not checked". false means "checked and negative". They are never interchanged. A gage whose site description could not be fetched gets null name and county with site_metadata_status: "unavailable" — not a blank that reads like a verified fact.
  • No silent zeros. An unanswerable request is rejected before the run starts. A well-formed query that returns nothing at all fails the run rather than emitting an empty dataset that reads as "no flood alerts".

Use as an MCP tool

This actor is available to AI agents through Apify's MCP server at https://mcp.apify.com. Billing is unchanged when it is called as a tool, and a run that cannot answer fails without billing.

Client configuration:

{
"mcpServers": {
"apify": {
"command": "npx",
"args": ["-y", "@apify/actors-mcp-server", "--actors", "malonestar/usgs-nwis-streamflow-monitor"],
"env": { "APIFY_TOKEN": "<your token>" }
}
}
}

Worked agent prompt: "Check USGS gages 01646500 and 01578310 for discharge above 5000 cfs, and tell me which readings are too old to trust." The agent should read above_threshold for the verdict and threshold_verdict_suppressed_reason for anything withheld.

Pricing

Pay per result — you are charged per row emitted. A run that fails a gate emits nothing and bills nothing. A statewide discharge pull returns a few hundred rows; a named-gage watch returns exactly as many rows as gages you asked about.

FAQ

Why is above_threshold null on some rows? Because the reading behind it cannot describe current conditions — it is stale, future-dated, or USGS flagged it untrustworthy. threshold_verdict_suppressed_reason says which. above_threshold_ignoring_staleness gives you the raw comparison if you want it.

Why did I get a row with no reading? Either you asked about a gage USGS returned nothing for (data_status: "not_returned"), or the gage reported no value and said why (zero flow, ice, dry, equipment). Both are deliberate: silence must not read as an all-clear.

Why does a statewide pull include gages that stopped reporting in the 1990s? Because USGS publishes them, and dropping them silently would hide part of the universe. Use siteStatus: "active" or maxReadingAgeHours to exclude them — both are documented as derived filters, since the successor API publishes no operator-status flag.

Does this still work now that USGS is retiring NWIS? Yes. As of v1.1 this actor reads the successor API at api.waterdata.usgs.gov and does not touch the retiring host at all.