USGS Water Data client (georest.usgs.waterdata) - usage examples

Walks through every public function in src/georest/usgs/waterdata.py against the live USGS Water Data OGC API (https://api.waterdata.usgs.gov/ogcapi/v1):

  • list_collections, get_queryables - what the API offers

  • search_monitoring_locations, get_monitoring_location - find sites

  • get_time_series_metadata - what a site measures

  • get_daily_values, get_continuous_values, get_latest_values - observations

  • get_field_measurements, get_peaks - site visits and annual peaks

  • get_items - any collection, any filter

  • to_rows - flatten to plain dicts (CSV / pandas ready)

  • API keys, max_items / truncated, rate_limit, and error handling

Every function returns a GeoJSON FeatureCollection dict - one Feature per observation for the time-series collections, one per site for monitoring-locations.

Rate limits. Anonymous callers get “a few queries per hour”. This notebook makes ~20 requests, so set USGS_API_KEY first (free key: https://api.waterdata.usgs.gov/signup/) or run the cells selectively.

Setup

This notebook needs georest installed:

pip install georest

or, from a clone of this repository, pip install -e . from the repo root.

import json
import os

import georest
from georest.usgs import waterdata as wd

print("georest", georest.__version__)
print("base URL", wd.BASE_URL)

# Or set it for this process only (never commit a key):
# wd.set_api_key("your-key-here")

def show(fc, n=3):
    """Print the parts of a FeatureCollection that matter when eyeballing a result."""
    feats = fc.get("features", [])
    print(f"{len(feats)} features"
          + ("  [TRUNCATED - server had more]" if fc.get("truncated") else "")
          + (f"  rate_limit={fc['rate_limit']}" if fc.get("rate_limit") else ""))
    for f in feats[:n]:
        print(" ", f.get("id"), json.dumps(f.get("properties"), default=str)[:200])
georest 0.5.0.dev0
base URL https://api.waterdata.usgs.gov/ogcapi/v1

Lees Ferry on the Colorado River (USGS-09380000), gauged continuously since 1921, is the fixture site throughout. Bare ids ("09380000") get the USGS- prefix automatically.

SITE = "USGS-09380000"
LEES_FERRY_BBOX = (-111.7, 36.8, -111.5, 37.0)   # minx, miny, maxx, maxy (lon/lat)

1. list_collections and get_queryables - what the API offers

COLLECTIONS is the module’s reference list of the data collections; list_collections() is the live, authoritative catalog (it also includes small code tables). get_queryables tells you every property that works as a direct filter on a collection.

cols = wd.list_collections()
for c in cols:
    print(f"{c['id']:32s} {c.get('title', '')}")
latest-continuous                Latest continuous
continuous                       Continuous values
latest-daily                     Latest daily values
daily                            Daily values
edr/daily                        Daily values environmental data retrieval (EDR)
latest-field-measurements        Latest field measurements
field-measurements               Field measurements
channel-measurements             Channel measurements
combined-metadata                Combined metadata
field-measurements-metadata      Field measurements metadata
time-series-metadata             Time series metadata
monitoring-locations             Monitoring locations
peaks                            Peaks
time-series-revisions            Time series revisions
time-series-methods              Time series methods
agency-codes                     Agency codes
altitude-datums                  Altitude datums
aquifer-codes                    Aquifer codes
aquifer-types                    Aquifer types
coordinate-accuracy-codes        Coordinate accuracy codes
coordinate-datum-codes           Coordinate datum codes
coordinate-method-codes          Coordinate method codes
counties                         County identifiers
countries                        Country identifiers
hydrologic-unit-codes            Hydrologic unit codes (HUCs)
medium-codes                     Medium codes
methods                          Water measurement methods
method-categories                Method categories
method-citations                 Method citations
citations                        Citations
combined-method-citations        Combined method citations
national-aquifer-codes           National aquifer codes
parameter-codes                  Parameter codes
reliability-codes                Reliability codes
site-types                       Site types
states                           State identifiers
statistic-codes                  Statistic codes
topographic-codes                Topographic codes
time-zone-codes                  Time zone codes
print(wd.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')
q = wd.get_queryables("daily")
print(len(q), "queryable properties on 'daily'")
for name in list(q)[:15]:
    print(f"  {name:36s} {q[name].get('type', '')}")
52 queryable properties on 'daily'
  geometry                             
  id                                   string
  time_series_id                       string
  monitoring_location_id               string
  parameter_code                       string
  statistic_id                         string
  time                                 string
  value                                string
  unit_of_measure                      string
  approval_status                      string
  qualifier                            string
  last_modified                        string
  agency_code                          string
  agency_name                          string
  monitoring_location_number           string

2. search_monitoring_locations - find sites

Filters combine with AND. site_type_code: ST stream, LK lake, GW well, SP spring… A list of codes becomes a CQL2 IN. hydrologic_unit_code prefix-matches.

# Streams inside a bounding box
fc = wd.search_monitoring_locations(bbox=LEES_FERRY_BBOX, site_type_code="ST")
show(fc)
14 features
  USGS-09379950 {"id": "USGS-09379950", "agency_code": "USGS", "agency_name": "U.S. Geological Survey", "monitoring_location_number": "09379950", "monitoring_location_name": "R200-01 11 MI ABV LEES FERRY IN GCNRA, AZ
  USGS-09379965 {"id": "USGS-09379965", "agency_code": "USGS", "agency_name": "U.S. Geological Survey", "monitoring_location_number": "09379965", "monitoring_location_name": "R200-02 6.5 MI ABV LEES FERRY IN GCNRA, A
  USGS-09380000 {"id": "USGS-09380000", "agency_code": "USGS", "agency_name": "U.S. Geological Survey", "monitoring_location_number": "09380000", "monitoring_location_name": "COLORADO RIVER AT LEES FERRY, AZ", "distr
# Every stream and lake site in a HUC4 (prefix match), capped at 200
fc = wd.search_monitoring_locations(hydrologic_unit_code="1407",
                                    site_type_code=["ST", "LK"], max_items=200)
show(fc, n=5)
200 features  [TRUNCATED - server had more]
  AZ003-365613111285900 {"id": "AZ003-365613111285900", "agency_code": "AZ003", "agency_name": "Arizona Game and Fish Department", "monitoring_location_number": "365613111285900", "monitoring_location_name": "AGF-GLENNCANYON
  AZ003-365617111290100 {"id": "AZ003-365617111290100", "agency_code": "AZ003", "agency_name": "Arizona Game and Fish Department", "monitoring_location_number": "365617111290100", "monitoring_location_name": "AGF-LAKE POWELL
  USBLM-09337800 {"id": "USBLM-09337800", "agency_code": "USBLM", "agency_name": "U.S. Bureau of Land Management", "monitoring_location_number": "09337800", "monitoring_location_name": "Escalante River above Calf Cree
  USBLM-09337810 {"id": "USBLM-09337810", "agency_code": "USBLM", "agency_name": "U.S. Bureau of Land Management", "monitoring_location_number": "09337810", "monitoring_location_name": "Calf Creek at BLM campground", 
  USGS-09328960 {"id": "USGS-09328960", "agency_code": "USGS", "agency_name": "U.S. Geological Survey", "monitoring_location_number": "09328960", "monitoring_location_name": "COLORADO RIVER AT GYPSUM CANYON NEAR HITE
# By state + county FIPS, trimming the response to a few properties
fc = wd.search_monitoring_locations(state_code="49", county_code="025",
                                    properties=["monitoring_location_name", "site_type"],
                                    max_items=20)
show(fc, n=5)
20 features  [TRUNCATED - server had more]
  USEPA-370427111405201 {"monitoring_location_name": "(D-43-15) 2bad-1", "site_type": "Well"}
  USEPA-370511111410001 {"monitoring_location_name": "(D-43-10) 2bdc-1", "site_type": "Well"}
  USEPA-370609112232701 {"monitoring_location_name": "(C-43- 2) 5bcc-1", "site_type": "Well"}
  USEPA-370615111460701 {"monitoring_location_name": "(D-43- 2) 1bda-1", "site_type": "Well"}
  USEPA-370628111545401 {"monitoring_location_name": "(C-43- 4) 1baa-1", "site_type": "Well"}

3. get_monitoring_location - one site

Returns a single GeoJSON Feature (not a collection): full site attributes and its Point.

site = wd.get_monitoring_location("09380000")
print(site["id"], site["geometry"])
for k in ("monitoring_location_name", "site_type", "state_name", "county_name",
          "drainage_area", "altitude", "vertical_datum", "hydrologic_unit_code"):
    print(f"  {k:28s} {site['properties'].get(k)}")
USGS-09380000 {'type': 'Point', 'coordinates': [-111.58787222222222, 36.864333333333335]}
  monitoring_location_name     COLORADO RIVER AT LEES FERRY, AZ
  site_type                    Stream
  state_name                   Arizona
  county_name                  Coconino County
  drainage_area                111800.0
  altitude                     3109.13
  vertical_datum               NAVD88
  hydrologic_unit_code         140700061105

4. get_time_series_metadata - what a site measures

One Feature per (parameter, statistic, computation) with begin/end dates and the time_series_id. Check this before pulling values so you ask for a parameter the site actually reports.

fc = wd.get_time_series_metadata(monitoring_location_id=SITE)
for f in fc["features"]:
    p = f["properties"]
    print(f"{p.get('parameter_code')}  {p.get('parameter_name', '')[:40]:40s} "
          f"{p.get('statistic_id')}  {p.get('computation_period_identifier', ''):12s} "
          f"{p.get('begin')} → {p.get('end')}")
00010  Temperature, water                       00002  Daily        1986-10-17T06:00:00+00:00 → 2026-10-26T06:00:00+00:00
63680  Turbidity, FNU                           00002  Daily        2024-02-15T07:00:00+00:00 → 2026-10-26T06:00:00+00:00
00400  pH                                       00008  Daily        2022-07-09T06:00:00+00:00 → 2026-10-26T06:00:00+00:00
00400  pH                                       00001  Daily        2022-07-09T06:00:00+00:00 → 2026-10-26T06:00:00+00:00
00010  Temperature, water                       00003  Daily        1986-10-17T06:00:00+00:00 → 2026-10-27T06:00:00+00:00
63680  Turbidity, FNU                           00001  Daily        2024-02-15T07:00:00+00:00 → 2026-10-26T06:00:00+00:00
00300  Dissolved oxygen                         00011  Points       2024-02-13T22:15:00+00:00 → 2026-10-28T10:00:00+00:00
80155  Suspnd sedmnt disch                      00003  Daily        1942-11-10T06:00:00+00:00 → 1965-09-30T06:00:00+00:00
63680  Turbidity, FNU                           00011  Points       2024-02-13T22:15:00+00:00 → 2026-10-28T10:00:00+00:00
80154  Suspnd sedmnt conc                       00003  Daily        1928-10-01T07:00:00+00:00 → 1965-08-13T06:00:00+00:00
00400  pH                                       00011  Points       2022-07-08T20:30:00+00:00 → 2026-10-28T10:00:00+00:00
00095  Specific cond at 25C                     00011  Points       2007-10-01T07:00:00+00:00 → 2026-10-28T10:00:00+00:00
00095  Specific cond at 25C                     00001  Daily        1986-10-17T06:00:00+00:00 → 2026-10-27T06:00:00+00:00
00060  Discharge                                00003  Daily        1921-10-01T07:00:00+00:00 → 2026-10-26T06:00:00+00:00
00095  Specific cond at 25C                     00002  Daily        1986-10-17T06:00:00+00:00 → 2026-10-27T06:00:00+00:00
00060  Discharge                                00011  Points       1985-10-01T07:09:00+00:00 → 2026-10-28T10:00:00+00:00
00010  Temperature, water                       00011  Points       2007-10-01T07:00:00+00:00 → 2026-10-28T10:00:00+00:00
00095  Specific cond at 25C                     00003  Daily        1986-10-17T06:00:00+00:00 → 2026-10-26T06:00:00+00:00
00060  Discharge                                None  Water Year   1884-01-01T07:00:00+00:00 → 2023-04-25T06:00:00+00:00
00065  Gage height                              00011  Points       2007-10-01T07:00:00+00:00 → 2026-10-28T10:00:00+00:00
00400  pH                                       00003  Daily        2022-07-09T06:00:00+00:00 → 2026-10-27T06:00:00+00:00
00010  Temperature, water                       00001  Daily        1986-10-17T06:00:00+00:00 → 2026-10-26T06:00:00+00:00
00400  pH                                       00002  Daily        2022-07-09T06:00:00+00:00 → 2026-10-26T06:00:00+00:00
00065  Gage height                              None  Water Year   1884-01-01T07:00:00+00:00 → 2023-04-25T06:00:00+00:00
00045  Precipitation                            None  Points       2026-06-29T00:00:00+00:00 → 2026-10-28T09:00:00+00:00
63680  Turbidity, FNU                           00008  Daily        2024-02-15T07:00:00+00:00 → 2026-10-26T06:00:00+00:00

5. get_daily_values - daily statistics (old /nwis/dv)

Defaults: parameter_code="00060" (discharge, ft³/s), statistic_id="00003" (daily mean). start/end accept strings, date/datetime objects, or an open side. Output is sorted by (time_series_id, time) client-side - the API does not sort. Geometry is skipped by default (the same point on every row).

fc = wd.get_daily_values(SITE, "00060", start="2024-01-01", end="2024-01-31")
show(fc)
print("first:", fc["features"][0]["properties"]["time"],
      " last:", fc["features"][-1]["properties"]["time"])
31 features
  c0beb6eb-42fe-4889-9c36-61ad8080f007 {"time_series_id": "925d13501026437bb82f4d02caf7222b", "monitoring_location_id": "USGS-09380000", "parameter_code": "00060", "statistic_id": "00003", "time": "2024-01-01", "value": "11300", "unit_of_m
  8e631e1b-88be-47c3-b491-a874d924c3d2 {"time_series_id": "925d13501026437bb82f4d02caf7222b", "monitoring_location_id": "USGS-09380000", "parameter_code": "00060", "statistic_id": "00003", "time": "2024-01-02", "value": "11900", "unit_of_m
  1b63d564-f110-4ad5-bddf-21fbe44dd373 {"time_series_id": "925d13501026437bb82f4d02caf7222b", "monitoring_location_id": "USGS-09380000", "parameter_code": "00060", "statistic_id": "00003", "time": "2024-01-03", "value": "11900", "unit_of_m
first: 2024-01-01  last: 2024-01-31
import datetime as dt

# date objects, open-ended range (everything since), daily max + min water temperature
# via a list of statistics. (Section 4 shows which daily series a site has - Lees Ferry
# reports gage height only as 15-minute points, so 00065 here would return 0 features.)
fc = wd.get_daily_values(SITE, "00010", statistic_id=["00001", "00002"],
                         start=dt.date(2024, 6, 1), end=None, max_items=100)
show(fc)
100 features  [TRUNCATED - server had more]
  0ce7c84d-b5ee-4d54-9bee-c32343b9d800 {"time_series_id": "0366a27ef37e4dc98926ed9ff09346ae", "monitoring_location_id": "USGS-09380000", "parameter_code": "00010", "statistic_id": "00002", "time": "2024-07-03", "value": "12.5", "unit_of_me
  00cd9d87-cb09-40a2-ab50-9d3f5e53c255 {"time_series_id": "0366a27ef37e4dc98926ed9ff09346ae", "monitoring_location_id": "USGS-09380000", "parameter_code": "00010", "statistic_id": "00002", "time": "2024-07-17", "value": "11.8", "unit_of_me
  00c2b0a6-a63d-43ee-ae48-e643d84bf3cb {"time_series_id": "0366a27ef37e4dc98926ed9ff09346ae", "monitoring_location_id": "USGS-09380000", "parameter_code": "00010", "statistic_id": "00002", "time": "2024-07-28", "value": "12.0", "unit_of_me
# Several sites at once → CQL2 IN, one call
fc = wd.get_daily_values(["09380000", "09402500"], start="2024-03-01", end="2024-03-07")
show(fc, n=4)
sites = {f["properties"]["monitoring_location_id"] for f in fc["features"]}
print("sites in result:", sites)
14 features
  56598c8e-2f73-4bb0-b1a1-38eb5f7a0520 {"time_series_id": "9173a72e5fee42a287bdd54a6b70e6d4", "monitoring_location_id": "USGS-09402500", "parameter_code": "00060", "statistic_id": "00003", "time": "2024-03-01", "value": "12400", "unit_of_m
  00ae1cc3-5bc4-45c2-a395-8dfc8d689a47 {"time_series_id": "9173a72e5fee42a287bdd54a6b70e6d4", "monitoring_location_id": "USGS-09402500", "parameter_code": "00060", "statistic_id": "00003", "time": "2024-03-02", "value": "11900", "unit_of_m
  691a238e-0918-4efb-a207-4f7a460b16a1 {"time_series_id": "9173a72e5fee42a287bdd54a6b70e6d4", "monitoring_location_id": "USGS-09402500", "parameter_code": "00060", "statistic_id": "00003", "time": "2024-03-03", "value": "11200", "unit_of_m
  5bf2b286-3426-42ac-a69d-51970a1b9ee8 {"time_series_id": "9173a72e5fee42a287bdd54a6b70e6d4", "monitoring_location_id": "USGS-09402500", "parameter_code": "00060", "statistic_id": "00003", "time": "2024-03-04", "value": "10600", "unit_of_m
sites in result: {'USGS-09380000', 'USGS-09402500'}

6. to_rows - flatten for CSV / pandas / plotting

One plain dict per Feature. value (and anything else named in numeric) becomes a float, or None when the API sent something non-numeric (e.g. an "Ice" marker).

fc = wd.get_daily_values(SITE, "00060", start="2024-01-01", end="2024-01-10")
rows = wd.to_rows(fc)
rows[:3]
[{'time_series_id': '925d13501026437bb82f4d02caf7222b',
  'monitoring_location_id': 'USGS-09380000',
  'parameter_code': '00060',
  'statistic_id': '00003',
  'time': '2024-01-01',
  'value': 11300.0,
  'unit_of_measure': 'ft^3/s',
  'approval_status': 'Approved',
  'qualifier': None,
  'last_modified': '2025-03-10T20:12:22.537090+00:00',
  'id': 'c0beb6eb-42fe-4889-9c36-61ad8080f007'},
 {'time_series_id': '925d13501026437bb82f4d02caf7222b',
  'monitoring_location_id': 'USGS-09380000',
  'parameter_code': '00060',
  'statistic_id': '00003',
  'time': '2024-01-02',
  'value': 11900.0,
  'unit_of_measure': 'ft^3/s',
  'approval_status': 'Approved',
  'qualifier': None,
  'last_modified': '2025-03-10T20:12:22.537090+00:00',
  'id': '8e631e1b-88be-47c3-b491-a874d924c3d2'},
 {'time_series_id': '925d13501026437bb82f4d02caf7222b',
  'monitoring_location_id': 'USGS-09380000',
  'parameter_code': '00060',
  'statistic_id': '00003',
  'time': '2024-01-03',
  'value': 11900.0,
  'unit_of_measure': 'ft^3/s',
  'approval_status': 'Approved',
  'qualifier': None,
  'last_modified': '2025-03-10T20:12:22.537090+00:00',
  'id': '1b63d564-f110-4ad5-bddf-21fbe44dd373'}]
# CSV with the stdlib
import csv
import io

buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=["time", "value", "unit_of_measure", "approval_status"],
                        extrasaction="ignore")
writer.writeheader()
writer.writerows(rows)
print(buf.getvalue()[:400])
time,value,unit_of_measure,approval_status
2024-01-01,11300.0,ft^3/s,Approved
2024-01-02,11900.0,ft^3/s,Approved
2024-01-03,11900.0,ft^3/s,Approved
2024-01-04,12000.0,ft^3/s,Approved
2024-01-05,12000.0,ft^3/s,Approved
2024-01-06,11900.0,ft^3/s,Approved
2024-01-07,11900.0,ft^3/s,Approved
2024-01-08,11900.0,ft^3/s,Approved
2024-01-09,11900.0,ft^3/s,Approved
2024-01-10,11900.0,ft^3/s,Approv
# pandas, if you have it (not a georest dependency)
try:
    import pandas as pd
    df = pd.DataFrame(rows).set_index("time")
    display(df[["value", "approval_status"]].head())
    print(df["value"].describe())
except ImportError:
    print("pandas not installed - skip")
value approval_status
time
2024-01-01 11300.0 Approved
2024-01-02 11900.0 Approved
2024-01-03 11900.0 Approved
2024-01-04 12000.0 Approved
2024-01-05 12000.0 Approved
count       10.000000
mean     11860.000000
std        201.108042
min      11300.000000
25%      11900.000000
50%      11900.000000
75%      11900.000000
max      12000.000000
Name: value, dtype: float64

7. get_continuous_values - sensor readings (old /nwis/iv)

Typically every 15 minutes, so ~35 000 rows per year per parameter. The default max_items=10_000 covers roughly 100 days; the result says truncated: True when it bit. start="P2D" is an ISO 8601 duration - the last two days. time carries a UTC offset.

fc = wd.get_continuous_values(SITE, "00065", start="P2D")
show(fc)
rows = wd.to_rows(fc)
print("first:", rows[0]["time"], rows[0]["value"], " last:", rows[-1]["time"], rows[-1]["value"])
165 features
  9b715316-9d68-4f18-9ee5-5debb59af937 {"time_series_id": "c1d72c8390d144f78b51568a3755a5ad", "monitoring_location_id": "USGS-09380000", "parameter_code": "00065", "statistic_id": "00011", "time": "2026-10-26T16:00:00+00:00", "value": "7.1
  8bbb1fbb-579c-484b-a353-345e369f17b6 {"time_series_id": "c1d72c8390d144f78b51568a3755a5ad", "monitoring_location_id": "USGS-09380000", "parameter_code": "00065", "statistic_id": "00011", "time": "2026-10-26T16:15:00+00:00", "value": "7.1
  08841521-6880-4d47-b566-5148e908734a {"time_series_id": "c1d72c8390d144f78b51568a3755a5ad", "monitoring_location_id": "USGS-09380000", "parameter_code": "00065", "statistic_id": "00011", "time": "2026-10-26T16:30:00+00:00", "value": "7.1
first: 2026-10-26T16:00:00+00:00 7.12  last: 2026-10-28T15:00:00+00:00 7.07
# Deliberately small cap to see the truncated flag
fc = wd.get_continuous_values(SITE, "00060", start="P7D", max_items=50)
show(fc, n=1)
print("truncated:", fc.get("truncated"))
50 features  [TRUNCATED - server had more]
  aee35497-d73c-4b26-b66b-38b31fe25987 {"time_series_id": "a62122d8ff094125b63bb2f73410b2b4", "monitoring_location_id": "USGS-09380000", "parameter_code": "00060", "statistic_id": "00011", "time": "2026-10-21T16:00:00+00:00", "value": "523
truncated: True

8. get_latest_values - most recent observation per series

source="daily" (default) or "continuous". No site is required: bbox + parameter_code gives current conditions at every gage in an area, so geometry is kept by default here (useful for a map).

fc = wd.get_latest_values(SITE)
show(fc, n=10)
16 features
  6f7ad86b-5ce8-4119-98e3-fc00f6a30b09 {"time_series_id": "0366a27ef37e4dc98926ed9ff09346ae", "monitoring_location_id": "USGS-09380000", "parameter_code": "00010", "statistic_id": "00002", "time": "2026-10-27", "value": "15.9", "unit_of_me
  87a2f588-cf80-490f-9876-8d373b3fdb24 {"time_series_id": "04c70580cd624597b2f379c24af9153a", "monitoring_location_id": "USGS-09380000", "parameter_code": "63680", "statistic_id": "00002", "time": "2026-10-27", "value": "1.5", "unit_of_mea
  1986b1ae-ba0c-45a3-b0d3-0cb518a90aed {"time_series_id": "08c90a0fd93d4278a584c0ccce0124c6", "monitoring_location_id": "USGS-09380000", "parameter_code": "00400", "statistic_id": "00008", "time": "2026-10-27", "value": "7.9", "unit_of_mea
  ab4fcf5b-4e0f-4181-a03d-24d8a29573b2 {"time_series_id": "08d465a966084bd38618a7d6a35e38d9", "monitoring_location_id": "USGS-09380000", "parameter_code": "00400", "statistic_id": "00001", "time": "2026-10-27", "value": "7.9", "unit_of_mea
  dcd36082-9f71-4de9-9052-027eeead1c76 {"time_series_id": "2230030e9ad64e8686103010b232b355", "monitoring_location_id": "USGS-09380000", "parameter_code": "00010", "statistic_id": "00003", "time": "2026-10-27", "value": "16.2", "unit_of_me
  aad6e605-6986-4fac-8f80-790e20b06673 {"time_series_id": "2957943e70724ae29baa7aa489199ad6", "monitoring_location_id": "USGS-09380000", "parameter_code": "63680", "statistic_id": "00001", "time": "2026-10-27", "value": "2.0", "unit_of_mea
  02540581-1664-4843-b63f-31e4a1b6a2ba {"time_series_id": "31775fb4034a420aab389bbe33ace6bf", "monitoring_location_id": "USGS-09380000", "parameter_code": "80155", "statistic_id": "00003", "time": "1965-09-30", "value": "300", "unit_of_mea
  3dd2a10e-5af6-4085-a5a4-418344b04ae9 {"time_series_id": "3e6b1910319146d98e41aacfcba14e4d", "monitoring_location_id": "USGS-09380000", "parameter_code": "80154", "statistic_id": "00003", "time": "1965-08-13", "value": "5.0", "unit_of_mea
  78d6ba86-d0af-43cc-998a-41904afd69e7 {"time_series_id": "83eeeb7bb2fd4e3b8180f4475dfcbcd7", "monitoring_location_id": "USGS-09380000", "parameter_code": "00095", "statistic_id": "00001", "time": "2026-10-27", "value": "907", "unit_of_mea
  0fff898c-bb92-43ae-96a6-6d0b89e9bfb7 {"time_series_id": "925d13501026437bb82f4d02caf7222b", "monitoring_location_id": "USGS-09380000", "parameter_code": "00060", "statistic_id": "00003", "time": "2026-10-26", "value": "6480", "unit_of_me
# Current discharge at every gage in a box (continuous → the newest 15-minute reading)
fc = wd.get_latest_values(source="continuous", parameter_code="00060",
                          bbox=(-112.5, 36.5, -111.0, 37.5))
for f in fc["features"]:
    p = f["properties"]
    print(f"{p['monitoring_location_id']:16s} {p['time']}  {p['value']:>10s} {p['unit_of_measure']}  "
          f"{f['geometry']['coordinates']}")
USGS-09403690    1997-10-01T05:53:06+00:00        0.30 ft^3/s  [-112.390754951319, 37.127206311423]
USGS-09382000    2026-10-28T15:00:00+00:00        25.9 ft^3/s  [-111.59462862125075, 36.87221097504765]
USGS-09380000    2026-10-28T15:00:00+00:00        4920 ft^3/s  [-111.58787222222222, 36.864333333333335]
USGS-09381500    2006-10-01T05:45:00+00:00        0.44 ft^3/s  [-112.021575591544, 37.4810955559468]
USGS-09381800    2026-10-28T15:10:00+00:00        15.1 ft^3/s  [-111.906010191065, 37.1074842837047]
USGS-09379910    2004-08-03T06:55:00+00:00       13500 ft^3/s  [-111.483491287611, 36.9216550128599]

9. get_field_measurements and get_peaks

Field measurements are the manual site-visit readings (groundwater levels, gage height and discharge measurements used to calibrate sensors). Peaks are the annual maximum streamflow, one per water year, with a qualifier list such as ["REGULATED"].

fc = wd.get_field_measurements(SITE, start="2023-01-01", max_items=20)
show(fc)
20 features  [TRUNCATED - server had more]
  00192b8b-3f9f-4ea6-aa96-c201cc2bd123 {"field_measurements_series_id": "68c51475-2b51-4fe3-ab4a-366a874f27bd", "reading_type": "MeanGageHeight", "field_visit_id": "d2babd94-f4a9-4854-a1f8-9bcbd977fe71", "parameter_code": "00065", "monitor
  37483474-87cf-44b4-96a6-5d2dd076e6b2 {"field_measurements_series_id": "ca22444a-d650-4edc-8f5d-935e9a2076de", "reading_type": "Discharge", "field_visit_id": "179f0876-a835-4008-8601-12b249178835", "parameter_code": "00060", "monitoring_l
  032f5da1-ec6d-4867-b43e-d1ff92958c4f {"field_measurements_series_id": "68c51475-2b51-4fe3-ab4a-366a874f27bd", "reading_type": "MeanGageHeight", "field_visit_id": "ef2f7c61-f643-484d-b151-e041c8704f52", "parameter_code": "00065", "monitor
fc = wd.get_peaks(SITE, start="2010-01-01")
for r in wd.to_rows(fc):
    print(r["water_year"], r["time"], f"{r['value']:>10,.0f} ft³/s", r.get("qualifier"))
2010 2010-01-01     19,200 ft³/s ['REGULATED']
2011 2011-07-07     26,200 ft³/s ['REGULATED']
2012 2011-11-20     21,200 ft³/s ['REGULATED']
2013 2012-11-20     44,800 ft³/s ['REGULATED']
2014 2013-11-15     37,700 ft³/s ['REGULATED']
2015 2014-11-11     38,400 ft³/s ['REGULATED']
2016 2016-07-07     20,400 ft³/s ['REGULATED']
2017 2016-11-09     36,800 ft³/s ['REGULATED']
2018 2018-07-07     19,000 ft³/s ['REGULATED']
2019 2018-11-06     39,700 ft³/s ['REGULATED']
2020 2020-08-18     21,300 ft³/s ['REGULATED']
2021 2021-03-23     20,200 ft³/s ['REGULATED']
2022 2022-08-30     16,200 ft³/s ['REGULATED']
2023 2023-04-25     40,300 ft³/s ['REGULATED']
2010 2010-01-01         10 ft³/s None
2011 2011-07-07         12 ft³/s None
2012 2011-11-20         11 ft³/s None
2013 2012-11-20         14 ft³/s None
2014 2013-11-15         13 ft³/s None
2015 2014-11-11         13 ft³/s None
2016 2016-07-07         11 ft³/s ['REGULATED']
2017 2016-11-09         13 ft³/s ['REGULATED']
2018 2018-07-07         10 ft³/s ['REGULATED']
2018 2018-08-02         10 ft³/s ['GHNOTASSCPKQ', 'REGULATED']
2019 2018-11-06         13 ft³/s ['REGULATED']
2020 2020-08-18         11 ft³/s ['REGULATED']
2021 2021-03-23         11 ft³/s ['REGULATED']
2022 2022-08-30         10 ft³/s ['REGULATED']
2023 2023-04-25         13 ft³/s ['REGULATED']

10. get_items - any collection, any filter

The generic fetch under every helper. Use it for a collection without a helper, or with a CQL2 filter. Any queryable property works as a keyword equality filter. Unlike the helpers, this path does not add time/value back when you trim with properties=.

# A code table
fc = wd.get_items("agency-codes", max_items=10)
show(fc, n=10)
10 features  [TRUNCATED - server had more]
  AK001 {"id": "AK001", "agency_name": "Alaska Department of Transportation and Public Facilities"}
  AK002 {"id": "AK002", "agency_name": "Alaska Department of Environmental Conservation"}
  AK004 {"id": "AK004", "agency_name": "Alaska Department of Natural Resources (DNR)"}
  AK008 {"id": "AK008", "agency_name": "Alaska Department of Fish and Game"}
  AK010 {"id": "AK010", "agency_name": "Alaska DNR, Division of Land and Water Management"}
  AK011 {"id": "AK011", "agency_name": "Alaska DNR, Division of Geological and Geophysical Surveys"}
  AL001 {"id": "AL001", "agency_name": "Alabama Geological Survey"}
  AL002 {"id": "AL002", "agency_name": "Alabama Water Improvement Commission"}
  AL003 {"id": "AL003", "agency_name": "Auburn University Water Resources Research Institute, AL"}
  AL006 {"id": "AL006", "agency_name": "Alabama State Highway Department"}
# CQL2 text filter + a direct property filter on the daily collection
fc = wd.get_items("daily", monitoring_location_id=SITE, parameter_code="00060",
                  filter="statistic_id IN ('00001','00003') AND approval_status='Approved'",
                  start="2024-01-01", end="2024-01-05", skip_geometry=True,
                  properties="time,statistic_id,value")
show(fc, n=10)
5 features
  1b63d564-f110-4ad5-bddf-21fbe44dd373 {"time": "2024-01-03", "statistic_id": "00003", "value": "11900"}
  46472b83-e34a-41c1-9c59-82a65cccc72a {"time": "2024-01-04", "statistic_id": "00003", "value": "12000"}
  8e631e1b-88be-47c3-b491-a874d924c3d2 {"time": "2024-01-02", "statistic_id": "00003", "value": "11900"}
  b2a6aaf1-9d02-4ae0-b53d-2bdc8676079e {"time": "2024-01-05", "statistic_id": "00003", "value": "12000"}
  c0beb6eb-42fe-4889-9c36-61ad8080f007 {"time": "2024-01-01", "statistic_id": "00003", "value": "11300"}

11. API keys, rate_limit, and errors

Resolution per call: explicit api_key= ("" forces anonymous) → set_api_key(...) → USGS_API_KEY / USGS_WATERDATA_API_KEY in the environment → anonymous. The key is sent only as the X-Api-Key header - never in a URL, so it cannot appear in an error message.

When the API reports the remaining hourly budget, results carry it in-band as rate_limit.

fc = wd.get_daily_values(SITE, start="2024-01-01", end="2024-01-02")
print("rate_limit:", fc.get("rate_limit"))   # None when the server sent no X-RateLimit headers
rate_limit: None
# A bad request surfaces the API's own description in the RuntimeError message
try:
    wd.get_daily_values(SITE, start="not-a-date")
except RuntimeError as exc:
    print(type(exc).__name__)
    print(exc)
RuntimeError
Request failed (400): https://api.waterdata.usgs.gov/ogcapi/v1/collections/daily/items?datetime=not-a-date/..&skipGeometry=true&monitoring_location_id=USGS-09380000&parameter_code=00060&statistic_id=00003&limit=10000&f=json
{
  "code": "InvalidParameterValue",
  "type": "InvalidParameterValue",
  "description": "Unknown string format: not-a-date"
}
# Reserved / invalid arguments are rejected before any request
for bad in (lambda: wd.get_daily_values(SITE, cursor="x"),
            lambda: wd.get_daily_values(SITE, page_size=0),
            lambda: wd.get_latest_values(source="hourly"),
            lambda: wd.get_daily_values([])):
    try:
        bad()
    except (TypeError, ValueError) as exc:
        print(f"{type(exc).__name__}: {exc}")
TypeError: cursor is set by the client; use limit=/max_items= instead of passing it as a filter
ValueError: limit must be an int in 1..50000, got 0
ValueError: source must be one of ['continuous', 'daily'], got 'hourly'
ValueError: monitoring_location_id list is empty
# A 429 the client won't wait out raises RateLimitError (a RuntimeError subclass)
# with .retry_after - catch it by name and back off rather than looping.
try:
    fc = wd.get_daily_values(SITE, start="2020-01-01", end="2020-01-02")
    print("ok:", len(fc["features"]), "rows")
except wd.RateLimitError as exc:
    print("rate limited; retry after", exc.retry_after, "seconds")
ok: 2 rows

12. Sanity check: the client-side sort

The API returns rows in storage order (a January window can come back starting on the 22nd). Every time-series helper sorts by (time_series_id, time); get_items does not.

raw = wd.get_items("daily", monitoring_location_id=SITE, parameter_code="00060",
                   statistic_id="00003", start="2020-01-01", end="2020-01-31",
                   skip_geometry=True, properties="time,value")
helper = wd.get_daily_values(SITE, "00060", start="2020-01-01", end="2020-01-31")
print("get_items order  :", [f["properties"]["time"] for f in raw["features"][:5]])
print("helper order     :", [f["properties"]["time"] for f in helper["features"][:5]])
get_items order  : ['2020-01-22', '2020-01-27', '2020-01-24', '2020-01-04', '2020-01-14']
helper order     : ['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04', '2020-01-05']