Skip to content

Fetch a point cloud from USGS 3DEP

You are viewing in-progress documentation for v2 (Beta). 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, and will it fit the budget?
POINT_BUDGET = 200_000_000
coverage = ff.point_clouds.check_3dep_coverage(domain)
if not coverage.available or coverage.estimated_point_count > POINT_BUDGET:
raise SystemExit("This domain cannot be fetched — check coverage first.")
# 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: pendingrunningcompleted, 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.

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,835,003 estimated versus 10,496,309 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 four 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.