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 offerssearch_monitoring_locations,get_monitoring_location- find sitesget_time_series_metadata- what a site measuresget_daily_values,get_continuous_values,get_latest_values- observationsget_field_measurements,get_peaks- site visits and annual peaksget_items- any collection, any filterto_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¶meter_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']