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.
Prerequisites
Section titled “Prerequisites”-
An API key: my-api-key.
-
A domain: your-domain-id.
-
A coverage check. Every rejection below is visible in advance — see Check 3DEP lidar coverage.
The whole flow in one script:
"""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_000coverage = 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.summarysource = 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%}")Step 1 — Create the point cloud
Section titled “Step 1 — Create the point cloud”The minimal request is metadata only. The backend prefers a single acquisition covering the whole domain, and otherwise combines the fewest that fill it:
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"]}'import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
domain = ff.Domain.from_id("your-domain-id")point_cloud = ff.point_clouds.create_point_cloud_from_3dep( domain, name="Blackfoot ALS", description="USGS 3DEP airborne lidar over the Blackfoot River domain.", tags=["blackfoot", "3dep"],){ "id": "your-point-cloud-id", "domain_id": "your-domain-id", "type": "als", "name": "Blackfoot ALS", "description": "USGS 3DEP airborne lidar over the Blackfoot River domain.", "status": "pending", "progress": null, "created_on": "2026-08-03T19:48:51.049070Z", "modified_on": "2026-08-03T19:48:51.049070Z", "checksum": "be23257713294d6db4f7d355962ad4f3", "source": { "name": "3dep", "datasets": ["MT_Statewide_P3_4_B21"], "requested_datasets": null, "coverage_fraction": 1.0, "catalog_fetched_on": "2026-08-03T19:48:12.218824+00:00" }, "georeference": null, "summary": null, "error": null, "tags": ["blackfoot", "3dep"]}Pin the acquisition instead
Section titled “Pin the acquisition instead”Prefer this. Pass datasets with names from the coverage check, in priority
order — where two overlap, the one listed first wins:
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"]}'import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
domain = ff.Domain.from_id("your-domain-id")point_cloud = ff.point_clouds.create_point_cloud_from_3dep( domain, datasets=["MT_Statewide_P3_4_B21"], name="Blackfoot ALS (pinned)", description="Pinned to a single 3DEP acquisition so the fetch is reproducible.", tags=["blackfoot", "3dep", "pinned"],){ "id": "your-point-cloud-id", "domain_id": "your-domain-id", "type": "als", "name": "Blackfoot ALS (pinned)", "description": "Pinned to a single 3DEP acquisition so the fetch is reproducible.", "status": "pending", "progress": null, "created_on": "2026-08-03T19:48:51.536751Z", "modified_on": "2026-08-03T19:48:51.536751Z", "checksum": "690b4bf260244d8db52e1eaa6888ad49", "source": { "name": "3dep", "datasets": ["MT_Statewide_P3_4_B21"], "requested_datasets": ["MT_Statewide_P3_4_B21"], "coverage_fraction": 1.0, "catalog_fetched_on": "2026-08-03T19:48:12.218824+00:00" }, "georeference": null, "summary": null, "error": null, "tags": ["blackfoot", "3dep", "pinned"]}Record the id: your-point-cloud-id.
Step 2 — Poll to completed
Section titled “Step 2 — Poll to completed”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.
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'import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
point_cloud = ff.get_point_cloud("your-domain-id", "your-point-cloud-id")point_cloud.wait(){ "id": "your-point-cloud-id", "domain_id": "your-domain-id", "type": "als", "name": "Blackfoot ALS (pinned)", "description": "Pinned to a single 3DEP acquisition so the fetch is reproducible.", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-08-27T12:40:16.430238Z", "modified_on": "2026-08-27T12:40:25.291413Z", "checksum": "28c93fe2d0a64e02bd1dce3febbe3158", "source": { "name": "3dep", "requested_datasets": ["MT_Statewide_P3_4_B21"], "coverage_fraction": 1.0, "catalog_fetched_on": "2026-08-27T12:40:06.141456+00:00", "datasets": ["MT_Statewide_P3_4_B21"] }, "georeference": { "crs": "EPSG:32612", "bounds": [ 294094.992, 5198981.67, 1026.3600000000001, 294784.075, 5199749.485, 1274.19 ] }, "summary": { "point_count": 10512255, "point_classes": [1, 2, 7, 9, 18, 20], "density": 19.837437726449274 }, "error": null, "tags": ["blackfoot", "3dep", "pinned"]}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_countandsummary.density— what actually arrived, which is not what the pre-flight predicted:

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 class1.source— what was used, what you asked for, the coverage fraction, and when the catalog was read.
When the request is rejected
Section titled “When the request is rejected”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.
{ "detail": "'MT_NoSuchAcquisition_9_9999' is not a USGS 3DEP lidar acquisition."}A typo, or a name from somewhere other than the coverage endpoint.
{ "detail": "3DEP acquisition 'AZ_MohaveCo_2_2021' does not overlap this domain."}The acquisition exists but does not intersect this domain. Distinct from the error above, and worth distinguishing: the name is fine, the geography is not.
{ "detail": "This domain would return roughly 227,552,496 points, which exceeds the 200,000,000 point limit for a single fetch. Use a smaller domain."}Both numbers are named. Shrink the domain and re-check coverage.
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.
Next steps
Section titled “Next steps”- Inspect the point cloud — confirm it covers the domain and is dense enough before building on it.
- Build a CHM grid from it — the next step toward a tree inventory.
- Manage your point clouds — list, tag, rename, and delete.
Common pitfalls
Section titled “Common pitfalls”- Fetching without pinning, then expecting to reproduce it. Record
source.datasetsfrom the completed cloud and pin those names next time. - Reading
summaryorgeoreferencewhile the status ispending. Both arenulluntil the job lands. - Assuming
coverage_fraction: 1.0means no holes. It is computed from catalog boundary polygons. Verify against the cloud’s ownboundsandpoint_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_classeswithout3,4,5does not mean the cloud has no vegetation — it means the vendor did not label it. What matters downstream is whether class2is present, because that decides whether a derived CHM gets measured or inferred ground.