This guide shows the same Western Water Datahub workflow in four toggleable forms:

The examples use https://api.wwdh.internetofwater.app as the base URL. Re-run discovery before important work because the Datahub is provider-backed and collection behavior can change.

1. Set the base URL and response format

Most API calls use f=json. Collection metadata, GeoJSON, and CoverageJSON are all JSON-family responses.

GET /collections?f=json HTTP/1.1
Host: api.wwdh.internetofwater.app
Accept: application/json

Full URL:

https://api.wwdh.internetofwater.app/collections?f=json

Useful f values:

f value Use Common response
json Programmatic access JSON, GeoJSON, or CoverageJSON
html Browser inspection HTML page generated by pygeoapi
csv Some feature or location endpoints CSV table
jsonld Linked-data representation when advertised JSON-LD

Expected output: A successful request returns a JSON document, not a rendered table. For /collections, the top-level keys include links, collections, and sometimes parameterGroups.

Example JSON shape:

{
  "links": [
    {
      "rel": "self",
      "type": "application/json",
      "href": "https://api.wwdh.internetofwater.app/collections?f=json"
    }
  ],
  "collections": [
    {
      "id": "rise-edr",
      "title": "USBR Reclamation Information Sharing Environment (RISE)"
    }
  ]
}

How to read it: Confirm the response is HTTP 200 and the body is JSON. If a browser shows a formatted HTML page instead, check that the URL includes f=json.

Next decision: Move from the service-level response to the collection list and choose the dataset family you want to inspect.

2. List collections and identify dataset families

/collections is the catalog. Each collection has an id, title, extent, links, and sometimes data_queries. If data_queries contains locations, cube, or area, the collection supports EDR sampling routes. If it does not, it is often a feature layer accessed through /items.

curl -sS 'https://api.wwdh.internetofwater.app/collections?f=json'

Look for collections[].id, collections[].title, and collections[].data_queries.

Examples of current collection families include reservoir operations (rise-edr, usace-edr, resviz-edr, teacup-edr, resops-edr), snow station data (snotel-edr, awdb-forecasts-edr), basin snow summaries (snotel-huc06-means), NOAA forecast layers, drought monitor layers, and DOI/USBR reference boundaries.

Expected output: A collection listing, with one record per dataset. Each useful row has at least an id, a title, links, an extent, and possibly a data_queries object.

Example JSON shape:

{
  "collections": [
    {
      "id": "snotel-edr",
      "title": "USDA Snowpack Telemetry Network (SNOTEL)",
      "data_queries": {
        "locations": {},
        "cube": {},
        "area": {},
        "items": {}
      }
    },
    {
      "id": "snotel-huc06-means",
      "title": "USDA Snotel Snow Water Equivalent Aggregated by HUC6"
    }
  ]
}

How to read it: Treat id as the value you put in URLs. A non-empty data_queries object means the collection advertises EDR sampling routes such as locations, cube, or area. A missing or empty data_queries object usually means the collection is a feature layer best explored through /items.

Next decision: Pick one collection id, then inspect that collection before requesting data values.

3. Inspect one collection

Before retrieving values, inspect the collection metadata. This tells you which parameters and query routes are valid.

curl -sS 'https://api.wwdh.internetofwater.app/collections/snotel-edr?f=json'

Important fields:

  • parameter_names: variables to request with parameter-name.
  • data_queries: supported EDR routes.
  • links: HTML, JSON, /items, /queryables, and documentation links.
  • extent: rough spatial and temporal coverage.

Use exact parameter ids in requests. For example, SNOTEL snow water equivalent is WTEQ; USACE flood storage is Flood Storage; RISE parameter ids are numeric strings such as 3 or 1830.

Expected output: One collection metadata document. For an EDR collection, expect fields such as title, extent, links, data_queries, and parameter_names.

Example JSON shape:

{
  "id": "snotel-edr",
  "title": "USDA Snowpack Telemetry Network (SNOTEL)",
  "parameter_names": {
    "WTEQ": {
      "type": "Parameter",
      "description": {"en": "Snow Water Equivalent"}
    },
    "SNWD": {
      "type": "Parameter",
      "description": {"en": "Snow Depth"}
    }
  },
  "data_queries": {
    "locations": {},
    "cube": {},
    "area": {},
    "items": {}
  }
}

How to read it: Use parameter_names keys exactly as parameter-name values. Use data_queries to decide which routes are valid. Use links to find related HTML pages, /items, /queryables, schemas, and source documentation.

Next decision: If you need to find places, continue to /locations or /items. If you already know a location id, skip ahead to a value request.

4. Find locations or features

Use /locations for station-style EDR collections. Use /items for feature layers, catalogs, boundaries, forecast layers, and collections without advertised EDR data queries.

GET /collections/snotel-edr/locations?f=json&bbox=-107,37,-105,40 HTTP/1.1
Host: api.wwdh.internetofwater.app
Accept: application/geo+json, application/json

Full URL:

https://api.wwdh.internetofwater.app/collections/snotel-edr/locations?f=json&bbox=-107,37,-105,40

GeoJSON features describe places. For station collections, the feature id is usually the value to use in /locations/{locId}. Properties such as station name, network code, elevation, or project name vary by provider.

Expected output: A GeoJSON FeatureCollection. Each feature has an id, a geometry, and a properties object with provider-specific metadata.

Example JSON shape:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": 303,
      "geometry": {
        "type": "Point",
        "coordinates": [-105.06766, 37.33067]
      },
      "properties": {
        "stationTriplet": "303:CO:SNTL",
        "name": "Apishapa",
        "stateCode": "CO",
        "networkCode": "SNTL"
      }
    }
  ]
}

How to read it: Use geometry to locate the site or feature. Use the feature id as the stable API identifier unless the provider documents another id field. Use properties for labels, station codes, network names, elevations, project names, and other context. If features is empty, the bbox or filters did not match anything, or the provider did not honor that filter combination.

Next decision: Choose a feature or location id for a single-site query, or keep the bbox and use /cube for a bulk request.

5. Request one known location

Once you know a location id, request a traditional single-site time series through /locations/{locId}. This example uses SNOTEL location 303 and snow water equivalent (WTEQ).

GET /collections/snotel-edr/locations/303?f=json&datetime=2024-02-01/2024-02-07&parameter-name=WTEQ HTTP/1.1
Host: api.wwdh.internetofwater.app
Accept: application/prs.coverage+json, application/json

Full URL:

https://api.wwdh.internetofwater.app/collections/snotel-edr/locations/303?f=json&datetime=2024-02-01/2024-02-07&parameter-name=WTEQ

The response is CoverageJSON. For one site and one parameter, it is usually a PointSeries: one x, one y, many t values, and one range array.

Expected output: A CoverageJSON Coverage or CoverageCollection. For this example, expect a PointSeries with domain.axes.x, domain.axes.y, domain.axes.t, and a ranges.WTEQ.values array.

Example JSON shape:

{
  "type": "Coverage",
  "domain": {
    "domainType": "PointSeries",
    "axes": {
      "x": {"values": [-105.06766]},
      "y": {"values": [37.33067]},
      "t": {
        "values": [
          "2024-02-01T00:00:00Z",
          "2024-02-02T00:00:00Z"
        ]
      }
    }
  },
  "ranges": {
    "WTEQ": {
      "type": "NdArray",
      "axisNames": ["t"],
      "values": [11.6, 11.7]
    }
  }
}

How to read it: x and y identify the site location. t lists the returned timestamps. ranges.WTEQ.values contains the SWE values, aligned to the axis order in ranges.WTEQ.axisNames. In edr4r, the tidy table should have one row per timestamp with datetime and value columns.

Next decision: Flatten the coverage into rows for analysis, or move from one location to a bbox-based /cube request.

6. Request many sites in a bounding box

Use /cube when a collection advertises it and you want values for many stations, reservoirs, or spatial cells in one request. Keep the bbox and time window small while exploring.

GET /collections/snotel-edr/cube?f=json&bbox=-106.2,39.6,-105.8,40.0&datetime=2024-02-01/2024-04-30&parameter-name=WTEQ HTTP/1.1
Host: api.wwdh.internetofwater.app
Accept: application/prs.coverage+json, application/json

For reservoir collections, the same pattern works with reservoir parameters. For example, RISE reservoir storage often uses parameter id 3, and USACE outflow uses Outflow.

Expected output: A CoverageJSON CoverageCollection or Coverage containing the values found inside the bbox and time window. Station collections often return one coverage per matched site; grid-like collections may return one coverage with multi-axis arrays.

Example JSON shape:

{
  "type": "CoverageCollection",
  "coverages": [
    {
      "type": "Coverage",
      "domain": {
        "domainType": "PointSeries",
        "axes": {
          "x": {"values": [-105.06766]},
          "y": {"values": [37.33067]},
          "t": {"values": ["2024-02-01T00:00:00Z"]}
        }
      },
      "ranges": {
        "WTEQ": {
          "axisNames": ["t"],
          "values": [11.6]
        }
      }
    }
  ]
}

How to read it: Inspect each coverage’s domain to see which place, cell, or time axis it represents. Inspect ranges for the requested parameter values. If the response is much smaller than expected, the bbox, datetime interval, or parameter may not overlap available data.

Next decision: Flatten the coverage into a table, join it to site metadata, or broaden only one dimension at a time.

7. Query feature layers with /queryables and /items

Feature layers expose geometries and attributes. They may be basin summaries, forecast layers, drought polygons, boundaries, or catalog records. Start with /queryables, then request /items.

curl -sS 'https://api.wwdh.internetofwater.app/collections/snotel-huc06-means/queryables?f=json'
curl -sS 'https://api.wwdh.internetofwater.app/collections/snotel-huc06-means/items?f=json&limit=3'

Feature responses are GeoJSON FeatureCollections. They describe features and attributes, not CoverageJSON time-series arrays.

Expected output: /queryables returns a JSON Schema-like document whose properties are filterable fields. /items returns a GeoJSON FeatureCollection.

Example /queryables JSON shape:

{
  "type": "object",
  "properties": {
    "geometry": {"$ref": "https://geojson.org/schema/Geometry.json"},
    "name": {"type": "string"},
    "basin_index": {"type": "number"}
  }
}

Example /items JSON shape:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "090400",
      "geometry": {"type": "MultiPolygon"},
      "properties": {
        "name": "Upper South Saskatchewan River",
        "basin_index": 72.9,
        "latest_full_day_of_data": "06-17"
      }
    }
  ]
}

How to read it: Use queryable property names as candidate filters on /items. Read item geometry as the feature shape and item properties as attributes such as basin names, drought categories, forecast values, region ids, or station lists. Do not look for CoverageJSON domain or ranges in feature responses.

Next decision: Add filters, lower limit, or add bbox until the feature set is manageable. If the feature belongs to an EDR collection, you may use its identifiers in a later sampling query.

8. Query an area

When a collection advertises area, send a WKT polygon in coords. This is useful for watershed, district, or management-area queries. Encode spaces in WKT as %20 if you write the URL by hand.

Readable WKT:

POLYGON((-106 39,-105.8 39,-105.8 39.2,-106 39.2,-106 39))

Encoded request:

GET /collections/snotel-edr/area?f=json&coords=POLYGON((-106%2039,-105.8%2039,-105.8%2039.2,-106%2039.2,-106%2039))&datetime=2024-02-01/2024-04-30&parameter-name=WTEQ HTTP/1.1
Host: api.wwdh.internetofwater.app
Accept: application/prs.coverage+json, application/json

For complex polygons, use a client that URL-encodes parameters for you. Very long URLs can hit browser, proxy, or server limits.

Expected output: A CoverageJSON response for values associated with the polygon query. Depending on the provider, the response may represent matched stations inside the polygon, sampled values for the area, or an aggregate-like coverage.

Example JSON shape:

{
  "type": "CoverageCollection",
  "coverages": [
    {
      "type": "Coverage",
      "domain": {
        "domainType": "PointSeries",
        "axes": {
          "x": {"values": [-105.9]},
          "y": {"values": [39.1]},
          "t": {"values": ["2024-02-01T00:00:00Z"]}
        }
      },
      "ranges": {
        "WTEQ": {
          "axisNames": ["t"],
          "values": [12.4]
        }
      }
    }
  ]
}

How to read it: Start with domain.domainType and domain.axes to understand whether the response is point-series-like, area-like, or multi-axis. Then read ranges for the requested parameter values. If the request fails or returns too much data, simplify the polygon or test the same area with a bbox first.

Next decision: Flatten the coverage into rows, or reduce the polygon/time/parameter scope and retry.

9. Flatten CoverageJSON into a table

EDR data queries usually return CoverageJSON. The data model is: domain.axes for coordinates and ranges for parameter values. Feature routes return GeoJSON instead.

CoverageJSON aligns axes and ranges:

{
  "type": "Coverage",
  "domain": {
    "domainType": "PointSeries",
    "axes": {
      "x": {"values": [-105.0]},
      "y": {"values": [37.3]},
      "t": {"values": ["2024-02-01T00:00:00Z"]}
    }
  },
  "ranges": {
    "WTEQ": {
      "axisNames": ["t"],
      "values": [11.6]
    }
  }
}

The first WTEQ value belongs to the first t value at the listed x/y coordinate.

The simple raw flatteners above are for point-series coverages. Grids and multi-axis coverages need array indexing across each axisNames dimension. Use covjson_to_tibble() when you want that handled for you.

Expected output: A rectangular table with columns such as coverage_id, parameter, datetime, x, y, and value.

Example row-oriented JSON shape:

[
  {
    "coverage_id": "1",
    "parameter": "WTEQ",
    "datetime": "2024-02-01T00:00:00Z",
    "x": -105.06766,
    "y": 37.33067,
    "value": 11.6
  }
]

How to read it: Each row is the traditional observation, forecast, or modeled-value record most analysts expect: one parameter at one place and time. coverage_id keeps a connection back to the original CoverageJSON object. Missing or NA values usually mean the provider returned an explicit missing value for that coordinate/time.

Next decision: Plot, map, join, or export the table. If the coverage is a grid or another multi-axis structure, use a parser that understands axisNames rather than the simple point-series helper.

10. Scale up carefully

Once a small query works, enlarge one dimension at a time: wider bbox, longer time range, or more parameters. That keeps failures diagnosable and avoids stressing shared upstream services.

Small:

bbox=-106.2,39.6,-105.8,40.0
datetime=2024-02-01/2024-02-07
parameter-name=WTEQ

Then enlarge one part:

bbox=-107,37,-105,40
datetime=2024-02-01/2024-04-30
parameter-name=WTEQ,SNWD

Expected output: The response shape should be the same as the small query, but with more features, coverages, timestamps, parameters, or rows after flattening.

Example JSON shape:

{
  "type": "CoverageCollection",
  "coverages": [
    {"type": "Coverage", "ranges": {"WTEQ": {"values": [11.6]}}},
    {"type": "Coverage", "ranges": {"WTEQ": {"values": [8.2]}}}
  ]
}

How to read it: Watch response time, payload size, row count, feature count, and missing-value patterns. If the larger request fails, the last dimension you enlarged is the most likely source of the problem.

Next decision: Cache the largest trusted result locally, then widen one more dimension only when you need it.

11. Troubleshooting raw requests

Expected output: This section does not introduce a new request. Use it when the response you got does not match the expected output from an earlier step.

Example error JSON shape:

{
  "code": "InvalidParameterValue",
  "description": "The requested parameter is not available for this collection."
}

How to read it: Match the observed symptom to the closest row in the table, then adjust only the likely cause before retrying. Keep the last successful small request handy so you can compare behavior.

Next decision: Return to the earlier workflow step, simplify the query, and add filters, parameters, or time range back one at a time.

Symptom Likely cause What to try
HTTP 404 on a route Collection does not advertise that query type Re-check /collections/{id}?f=json
HTTP 400 or 500 Query too broad, invalid parameter, or upstream provider failure Reduce bbox, date range, and parameter count
Empty features No feature matched the filters Try /items?f=json&limit=1, then add filters back
Empty or missing parameter_names Feature-style collection, not EDR sampling Use /queryables and /items
URL works in Python/R but not a browser Unencoded spaces or reserved characters Let the client encode query params, or encode spaces as %20
Values are hard to interpret CoverageJSON is array-oriented Pair domain.axes with ranges, or use covjson_to_tibble()

Good habits: