georest.usgs.waterdata¶
USGS Water Data OGC API client - the modern replacement for NWIS Water Services.
Base URL https://api.waterdata.usgs.gov/ogcapi/v1. Every function returns a
GeoJSON FeatureCollection dict (one Feature per observation for the
time-series collections, one per site for monitoring-locations), matching
the rest of georest. to_rows() flattens one into plain dicts for CSV,
pandas, or plotting.
Legacy NWIS endpoint → collection (from the USGS migration guide):
/nwis/site monitoring-locations
/nwis/site?seriesCatalogOutput time-series-metadata
/nwis/dv daily
/nwis/iv continuous / latest-continuous
/nwis/gwlevels, /measurements field-measurements
/nwis/peak peaks
Things verified against the live API (2026-09-14) that shape this module:
The API does not sort. The time-series functions sort their output by
(time_series_id, time)client-side.valueis a string (precision). The FeatureCollection is returned as sent;to_rows()converts tofloat.There is no
numberMatched.max_itemsbounds every fetch and atruncated: Truemember says when it bit;max_items=Nonemeans unlimited. Fifteen-minutecontinuousdata is roughly 35 000 rows per year per parameter, so the default of 10 000 covers about 100 days.Anonymous use works but is rate limited to “a few queries per hour”. Get a key at https://api.waterdata.usgs.gov/signup/ and set
USGS_API_KEY(or callset_api_key()). The key is sent only as theX-Api-Keyheader.
API-key resolution, per call: an explicit api_key= argument wins
("" forces anonymous); else the value given to set_api_key(); else
the first non-empty of the USGS_API_KEY / USGS_WATERDATA_API_KEY
environment variables; else anonymous. Reading a .env file is the
application’s job, not this library’s.
Copyright 2026 Ryan Rock and Ian Housman
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
Module Attributes
The data collections, for reference. |
|
The parameter codes most queries want. |
|
Statistic codes for the |
Functions
|
Sensor readings, typically every 15 minutes - the |
|
Daily statistics for a site - the |
|
Manually collected site-visit readings - the |
|
Fetch Features from any collection as a GeoJSON FeatureCollection. |
|
The most recent observation per time series - |
|
One site as a single GeoJSON Feature (not a collection). |
|
Annual peak streamflow - the |
|
|
|
What time series exist - the |
|
The live collection catalog: |
|
Find sites in the |
|
Set the process-wide API key, e.g. from a secret store at server start. |
|
Flatten a FeatureCollection into one plain dict per Feature. |
- georest.usgs.waterdata.COLLECTIONS = ('monitoring-locations', 'daily', 'latest-daily', 'continuous', 'latest-continuous', 'field-measurements', 'latest-field-measurements', 'channel-measurements', 'peaks', 'time-series-metadata', 'field-measurements-metadata', 'combined-metadata', 'time-series-revisions', 'time-series-methods')¶
The data collections, for reference.
list_collections()is the authoritative, live list (it also includes the small code tables such asagency-codesandaquifer-codes).
- georest.usgs.waterdata.PARAMETER_CODES = {'00010': 'Temperature, water, degrees Celsius', '00045': 'Precipitation, total, inches', '00060': 'Discharge, cubic feet per second', '00065': 'Gage height, feet', '72019': 'Depth to water level, feet below land surface'}¶
The parameter codes most queries want. Any 5-digit USGS parameter code is valid; these are just the common ones, described for readability.
- georest.usgs.waterdata.STATISTIC_CODES = {'00001': 'Maximum', '00002': 'Minimum', '00003': 'Mean', '00008': 'Median'}¶
Statistic codes for the
daily/latest-dailycollections.
- georest.usgs.waterdata.set_api_key(key: str | None) None[source]¶
Set the process-wide API key, e.g. from a secret store at server start.
Takes precedence over the environment variables; an explicit
api_key=argument on a call still wins.None(or"") clears it, so resolution falls back to the environment again.Example:
import os from georest.usgs import waterdata # Once at startup, from wherever you keep secrets; every call then uses it waterdata.set_api_key(os.environ.get("USGS_API_KEY"))
- georest.usgs.waterdata.list_collections(*, api_key: str | None = None, timeout: int | None = None) list[dict][source]¶
The live collection catalog:
[{"id", "title", "description", "links", ...}].- Raises:
RuntimeError – On an HTTP or connection failure (
RateLimitErroron 429).ValueErrorif the body isn’t JSON.
Example:
from georest.usgs import waterdata [c["id"] for c in waterdata.list_collections()]
- georest.usgs.waterdata.get_queryables(collection: str, *, api_key: str | None = None, timeout: int | None = None) dict[source]¶
{property_name: json_schema}for a collection. Every key is a valid direct filter forget_items()and the helpers built on it -dailyalone has ~55, including every site attribute.Example:
from georest.usgs import waterdata sorted(waterdata.get_queryables("daily")) # every property you can filter daily values on
- georest.usgs.waterdata.get_items(collection: str, *, bbox: str | dict | list | tuple | None = None, start: str | date | datetime | None = None, end: str | date | datetime | None = None, properties: str | Iterable[str] | None = None, skip_geometry: bool = False, filter: str | None = None, max_items: int | None = 10000, page_size: int = 1000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
Fetch Features from any collection as a GeoJSON FeatureCollection.
The escape hatch under every helper below; use it directly for a collection without one (
channel-measurements,time-series-methods, the code tables) or for a filter the helpers don’t spell out.- Parameters:
collection – A collection id - see
COLLECTIONS/list_collections().bbox –
minx,miny,maxx,maxyas a string, 4-tuple, or Esri-style dict.start – The
datetimewindow - strings,dateordatetimeobjects; either side may be open.startmay also be a ready interval ("2020-01-01/2020-01-31","../2020-12-31") or an ISO 8601 duration such as"P7D"or"PT6H"(the last 7 days / 6 hours). The server rejects durations, so they are resolved client-side to<now minus duration>/..; years and months ("P1Y","P3M") raiseValueError.end – The
datetimewindow - strings,dateordatetimeobjects; either side may be open.startmay also be a ready interval ("2020-01-01/2020-01-31","../2020-12-31") or an ISO 8601 duration such as"P7D"or"PT6H"(the last 7 days / 6 hours). The server rejects durations, so they are resolved client-side to<now minus duration>/..; years and months ("P1Y","P3M") raiseValueError.properties – Property names to keep; the response is trimmed to exactly these (verified). Note this generic function does not add the time-series essentials back - the helpers do.
skip_geometry – Return
geometry: nullon every Feature. Large observation pulls don’t need a point repeated per row.filter – A CQL2 text expression (
"statistic_id IN ('00001','00003')").max_items – Stop after this many Features;
Nonefor unlimited. When the server had more, the result carriestruncated: True.page_size – Rows per request,
1..50_000.api_key – Overrides the process/environment key for this call;
""forces anonymous.timeout – Seconds per request (default 60).
**filters – Any queryable property as an equality filter, e.g.
parameter_code="00060",state_code="49".
- Returns:
A
FeatureCollectiondict, plustruncatedandrate_limitmembers when applicable (seegeorest.usgs._ogc.get_items()).- Raises:
TypeError – If
f,limit,cursororoffsetis passed in**filters.ValueError – For a bad
bbox/page_size/max_items, an intervalstartcombined withend, a non-JSON body, or a page withoutfeatures.RuntimeError – On an HTTP error status or connection failure; the message includes the API’s own
descriptionwhen it sent one. A 429 the server won’t let us wait out raises theRateLimitErrorsubclass, whoseretry_aftersays how long it asked for.
Example:
from georest.usgs import waterdata # Any collection with any filter: stream sites in Arizona's Coconino County sites = waterdata.get_items("monitoring-locations", state_code="04", county_code="005", site_type_code="ST", max_items=20) len(sites["features"])
- georest.usgs.waterdata.search_monitoring_locations(*, bbox: str | dict | list | tuple | None = None, state_code: str | None = None, county_code: str | None = None, site_type_code: str | list[str] | None = None, hydrologic_unit_code: str | None = None, monitoring_location_id: str | int | list | None = None, agency_code: str | None = None, properties: str | Iterable[str] | None = None, filter: str | None = None, max_items: int | None = 1000, page_size: int = 1000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
Find sites in the
monitoring-locationscollection.Every filter is optional and they combine with AND. Common ones:
- Parameters:
bbox – Spatial window,
minx,miny,maxx,maxy(WGS84 lon/lat).state_code – Two-digit ANSI/FIPS state code, e.g.
"49"for Utah.county_code – County FIPS code within the state.
site_type_code –
"ST"stream,"LK"lake,"GW"well,"SP"spring,"ES"estuary… A list becomes a CQL2IN.hydrologic_unit_code – A HUC; the API prefix-matches, so
"1407"finds every HUC8 under that HUC4.monitoring_location_id – One id or a list; bare numbers get the
USGS-prefix.agency_code –
"USGS","USEPA", …properties – As for
get_items().max_itemsdefaults to 1 000 here.filter – As for
get_items().max_itemsdefaults to 1 000 here.max_items – As for
get_items().max_itemsdefaults to 1 000 here.page_size – As for
get_items().max_itemsdefaults to 1 000 here.api_key – As for
get_items().max_itemsdefaults to 1 000 here.timeout – As for
get_items().max_itemsdefaults to 1 000 here.**filters –
As for
get_items().max_itemsdefaults to 1 000 here.
- Returns:
A FeatureCollection of site Points; each Feature’s
idis themonitoring_location_id("USGS-09380000").
Example:
from georest.usgs import waterdata # Stream gages in a box (west, south, east, north) around Lees Ferry, Arizona sites = waterdata.search_monitoring_locations(bbox=(-111.7, 36.8, -111.5, 37.0), site_type_code="ST") [(f["id"], f["properties"]["monitoring_location_name"]) for f in sites["features"]]
- georest.usgs.waterdata.get_monitoring_location(monitoring_location_id: str | int, *, api_key: str | None = None, timeout: int | None = None) dict[source]¶
One site as a single GeoJSON Feature (not a collection).
monitoring_location_idmay be bare ("09380000") or prefixed ("USGS-09380000").- Raises:
RuntimeError – A 404 for an unknown id surfaces as
Request failed (404): ….
Example:
from georest.usgs import waterdata site = waterdata.get_monitoring_location("USGS-09380000") site["properties"]["monitoring_location_name"], site["geometry"]["coordinates"]
- georest.usgs.waterdata.get_time_series_metadata(*, monitoring_location_id: str | int | list | None = None, parameter_code: str | list[str] | None = None, statistic_id: str | None = None, bbox: str | dict | list | tuple | None = None, properties: str | Iterable[str] | None = None, filter: str | None = None, max_items: int | None = 10000, page_size: int = 10000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
What time series exist - the
time-series-metadatacollection (the old/nwis/site?seriesCatalogOutput=true).One Feature per (site, parameter, statistic, computation) with its
begin/enddates, unit andtime_series_id. Use it to discover whichparameter_codevalues a site actually reports before pulling values.Example:
from georest.usgs import waterdata # What this gage measures, and over which dates series = waterdata.get_time_series_metadata(monitoring_location_id="USGS-09380000") sorted({f["properties"]["parameter_name"] for f in series["features"]})
- georest.usgs.waterdata.get_daily_values(monitoring_location_id: str | int | list, parameter_code: str | list[str] = '00060', statistic_id: str | list[str] = '00003', start: str | date | datetime | None = None, end: str | date | datetime | None = None, *, properties: str | Iterable[str] | None = None, skip_geometry: bool = True, filter: str | None = None, max_items: int | None = 10000, page_size: int = 10000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
Daily statistics for a site - the
dailycollection (old/nwis/dv).One Feature per day, sorted by
(time_series_id, time).timeis aYYYY-MM-DDstring;valueis a string - seeto_rows().- Parameters:
monitoring_location_id –
"09380000"or"USGS-09380000"; a list of ids is allowed and becomes a CQL2IN.parameter_code – Default
"00060"(discharge). SeePARAMETER_CODES.statistic_id – Default
"00003"(daily mean). SeeSTATISTIC_CODES.start – Date window (inclusive), open on either side.
end – Date window (inclusive), open on either side.
skip_geometry – Default
True- the site’s point is the same on every row; useget_monitoring_location()for it.properties – Trim the response;
time_series_id,monitoring_location_id,timeandvalueare always kept.max_items – Default 10 000 (~27 years of one daily series).
Nonefor unlimited;truncated: Truein the result when it bit.filter – As for
get_items().page_size – As for
get_items().api_key – As for
get_items().timeout – As for
get_items().**filters –
As for
get_items().
Example:
from georest.usgs import waterdata fc = waterdata.get_daily_values("USGS-09380000", "00060", start="2024-01-01", end="2024-01-31") rows = waterdata.to_rows(fc) # [{"time": "2024-01-01", "value": 9870.0, ...}, ...]
- georest.usgs.waterdata.get_continuous_values(monitoring_location_id: str | int | list, parameter_code: str | list[str] = '00060', start: str | date | datetime | None = None, end: str | date | datetime | None = None, *, properties: str | Iterable[str] | None = None, skip_geometry: bool = True, filter: str | None = None, max_items: int | None = 10000, page_size: int = 10000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
Sensor readings, typically every 15 minutes - the
continuouscollection (old/nwis/iv, “instantaneous values”).timeis an RFC 3339 timestamp with offset. Volume warning: one parameter at one site is ~35 000 rows per year, so the defaultmax_itemsof 10 000 covers roughly 100 days; a long pull needs a narrowstart/endwindow ormax_items=Noneplus an API key.start="P7D"asks for the last seven days.Sorted by
(time_series_id, time). Other arguments as forget_daily_values()(there is nostatistic_idhere).Example:
from georest.usgs import waterdata # Fifteen-minute discharge readings for one day fc = waterdata.get_continuous_values("USGS-09380000", "00060", start="2024-06-01", end="2024-06-02") rows = waterdata.to_rows(fc) len(rows), rows[0]["time"], rows[0]["value"]
- georest.usgs.waterdata.get_latest_values(monitoring_location_id: str | int | list | None = None, parameter_code: str | list[str] | None = None, *, source: str = 'daily', statistic_id: str | list[str] | None = None, bbox: str | dict | list | tuple | None = None, properties: str | Iterable[str] | None = None, skip_geometry: bool = False, filter: str | None = None, max_items: int | None = 10000, page_size: int = 10000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
The most recent observation per time series -
latest-dailyorlatest-continuous(source="daily"/"continuous").Unlike the history functions, no site is required: a
bboxwithparameter_code="00060"gives the current discharge at every gage in an area, which is whyskip_geometrydefaults toFalsehere.- Raises:
ValueError – For a
sourceother than"daily"or"continuous".
Example:
from georest.usgs import waterdata latest = waterdata.to_rows(waterdata.get_latest_values("USGS-09380000", "00060")) latest[0]["time"], latest[0]["value"], latest[0]["unit_of_measure"]
- georest.usgs.waterdata.get_field_measurements(monitoring_location_id: str | int | list | None = None, parameter_code: str | list[str] | None = None, start: str | date | datetime | None = None, end: str | date | datetime | None = None, *, bbox: str | dict | list | tuple | None = None, properties: str | Iterable[str] | None = None, skip_geometry: bool = True, filter: str | None = None, max_items: int | None = 10000, page_size: int = 10000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
Manually collected site-visit readings - the
field-measurementscollection (old/nwis/gwlevelsand/measurements): groundwater levels, gage height and discharge measurements used to calibrate the sensors.timeis the visit date (YYYY-MM-DD);time_of_daycarries the clock time when known. Sorted by(monitoring_location_id, time)(these rows have notime_series_id).Example:
from georest.usgs import waterdata # Discharge measured in the river by USGS hydrographers visits = waterdata.to_rows(waterdata.get_field_measurements( "USGS-09380000", "00060", start="2024-01-01", end="2024-12-31")) [(v["time"], v["value"], v["observing_procedure"]) for v in visits]
- georest.usgs.waterdata.get_peaks(monitoring_location_id: str | int | list, start: str | date | datetime | None = None, end: str | date | datetime | None = None, *, properties: str | Iterable[str] | None = None, skip_geometry: bool = True, filter: str | None = None, max_items: int | None = 10000, page_size: int = 10000, api_key: str | None = None, timeout: int | None = None, **filters: object) dict[source]¶
Annual peak streamflow - the
peakscollection (old/nwis/peak).One Feature per water year with the peak
value(ft³/s), its date intime,water_year, and aqualifierlist (e.g.["REGULATED"]). Sorted by time.start/endwork as everywhere else, butpeaksrejects the OGCdatetimeparameter (“datetime query not supported”, verified 2026-09-14), so they are sent as a CQL2timefilter instead.Example:
from georest.usgs import waterdata # The largest flow of each year since 2000 peaks = waterdata.to_rows(waterdata.get_peaks("USGS-09380000", start="2000-01-01")) max(peaks, key=lambda p: p["value"] or 0)
- georest.usgs.waterdata.to_rows(fc: dict, numeric: Iterable[str] = ('value',)) list[dict][source]¶
Flatten a FeatureCollection into one plain dict per Feature.
Each row is the Feature’s
properties(copied), with the Feature’sidadded under"id"when the properties don’t already carry one, and every key named innumericconverted withfloat()-Nonewhen the value is missing or non-numeric (the API sendsvalueas a string, and a qualifier such as"Ice"can stand in for a number). Rows are sorted by(time_series_id, time)like the series functions.A flat list of dicts feeds
csv.DictWriter,pandas.DataFrame(rows)and plotting directly, and keepsqualifier/approval_statusalongside the value. Group bytime_series_idwithitertools.groupbyif a per-series shape is wanted.Example:
import pandas as pd from georest.usgs import waterdata fc = waterdata.get_daily_values("USGS-09380000", "00060", start="2024-01-01", end="2024-01-31") flow = pd.DataFrame(waterdata.to_rows(fc)) # one row per day, values as numbers flow[["time", "value"]]