Skip to content

Fetch a point cloud from USGS 3DEP

You are viewing in-progress documentation for v2. Switch to the stable version for the current production release.

POST /domains/{domain_id}/pointclouds/3dep fetches public airborne lidar from the USGS 3D Elevation Program, clips it to your domain, reprojects it to the domain’s CRS, and stores it as a point cloud you can build on.

3DEP is airborne by definition, so the result is always type: als. There is no acquisition type to choose.

  1. An API key: my-api-key.

  2. A domain: your-domain-id.

  3. A coverage check. Every rejection below is visible in advance — see Check 3DEP lidar coverage.

The whole flow in one script:

Check coverage, pin the acquisition, fetch, and poll
"""Check coverage, fetch a 3DEP point cloud, and wait for it to finish."""
import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
domain = ff.Domain.from_id("your-domain-id")
# 1. Pre-flight: is there lidar here?
coverage = ff.point_clouds.check_3dep_coverage(domain)
if not coverage.available:
raise SystemExit("No 3DEP lidar covers this domain.")
# 2. Pin the acquisitions the pre-flight found, so the fetch is reproducible.
pinned = [dataset.name for dataset in coverage.datasets]
point_cloud = ff.point_clouds.create_point_cloud_from_3dep(
domain,
datasets=pinned,
name="Blackfoot ALS (pinned)",
description="Pinned to a single 3DEP acquisition so the fetch is reproducible.",
tags=["blackfoot", "3dep", "pinned"],
)
# 3. Poll until the fetch lands. wait() raises JobFailedError on failure.
point_cloud.wait()
summary = point_cloud.summary
source = point_cloud.to_dict()["source"]
print(f"{summary.point_count:,} points at {summary.density:.1f} pts/m2")
print(f"Classes present: {summary.point_classes}")
print(f"Coverage: {source['coverage_fraction']:.1%}")

The minimal request is metadata only. The backend prefers a single acquisition covering the whole domain, and otherwise combines the fewest that fill it:

POST pointclouds/3dep — let the backend choose
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/pointclouds/3dep' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Blackfoot ALS",
"description": "USGS 3DEP airborne lidar over the Blackfoot River domain.",
"tags": ["blackfoot", "3dep"]
}'

Prefer this. Pass datasets with names from the coverage check, in priority order — where two overlap, the one listed first wins:

POST pointclouds/3dep — pinned
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/pointclouds/3dep' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Blackfoot ALS (pinned)",
"description": "Pinned to a single 3DEP acquisition so the fetch is reproducible.",
"tags": ["blackfoot", "3dep", "pinned"],
"datasets": ["MT_Statewide_P3_4_B21"]
}'

Record the id: your-point-cloud-id.

The fetch runs in the background: pending → running → completed, or failed. Expect roughly a minute for a domain of a few hundred thousand square metres.

GET the point cloud
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/pointclouds/your-point-cloud-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

georeference and summary are null until the status reaches completed — they do not fill in progressively, so check status before reading either.

The finished cloud, coloured by each return’s height above the ground:

An oblique three-dimensional view, from the south, of the point cloud over the whole Blackfoot River domain. Brown returns trace the ground surface up a hillside and across the valley floor; trees rise out of it as green spires, densest along both banks of the river. The river itself is an empty channel across the lower half. An oblique three-dimensional view, from the south, of the point cloud over the whole Blackfoot River domain. Brown returns trace the ground surface up a hillside and across the valley floor; trees rise out of it as green spires, densest along both banks of the river. The river itself is an empty channel across the lower half.

The pinned cloud over the whole domain — 10,512,255 returns, every fifth one drawn — coloured by height above a ground surface built from its class 2 returns. The empty channel is the Blackfoot River: water absorbs the pulse.

Three things the completed resource tells you:

  • summary.point_count and summary.density — what actually arrived, which is not what the pre-flight predicted:
Two bar charts comparing the pre-flight estimate with the delivered cloud. Points: 7,847,291 estimated versus 10,512,255 delivered. Density: 14.8 estimated versus 19.8 points per square metre delivered. Two bar charts comparing the pre-flight estimate with the delivered cloud. Points: 7,847,291 estimated versus 10,512,255 delivered. Density: 14.8 estimated versus 19.8 points per square metre delivered.

The pre-flight estimate against the finished cloud for this domain. Both figures came in about a third higher than predicted, because the catalog’s density is averaged over an acquisition’s whole published extent — including the parts of it that hold no points.

  • summary.point_classes — the ASPRS classes present. Here that is [1, 2, 7, 9, 18, 20]: ground (2) is classified, but there are no vegetation classes (3, 4, 5) at all. That is normal — many acquisitions classify only ground and leave everything else in class 1.
  • source — what was used, what you asked for, the coverage fraction, and when the catalog was read.

All three rejections happen before anything is dispatched, so a doomed fetch never leaves a failed point cloud behind.

{
"detail": "No USGS 3DEP lidar is available for this domain. Check coverage with GET /domains/{domain_id}/pointclouds/3dep/coverage."
}

Nothing to fetch. Check coverage first, or use a canopy source that does not need lidar.

Two more responses have no example here because they cannot be induced on demand:

  • 429 — a quota was exceeded. Point clouds have the tightest count limits in v2; see About quotas.
  • 503 — the USGS 3DEP catalog is temporarily unreachable. Retry shortly.
  • Fetching without pinning, then expecting to reproduce it. Record source.datasets from the completed cloud and pin those names next time.
  • Reading summary or georeference while the status is pending. Both are null until the job lands.
  • Assuming coverage_fraction: 1.0 means no holes. It is computed from catalog boundary polygons. Verify against the cloud’s own bounds and point_count.
  • Taking a two-acquisition mosaic without looking at the densities. The seam propagates into everything you derive. Pin the better survey if the difference matters.
  • Expecting vegetation classes. point_classes without 3, 4, 5 does not mean the cloud has no vegetation — it means the vendor did not label it. What matters downstream is whether class 2 is present, because that decides whether a derived CHM gets measured or inferred ground.