UFFDA
◆ API · v1 · live

UFFDA Developer API

POST /v1/fields/enrich·POST /v1/aoi/aggregate


What this gives you

Two endpoints, one family. The rule for choosing is one sentence: boundaries you bring → enrich; an area you bound → aggregate.

POST /v1/fields/enrichPOST /v1/aoi/aggregate
You sendA FeatureCollection of field polygons you already have (≤25)One AOI Polygon or MultiPolygon — an area you drew or defined
You get backPer-feature enrichment — each field's own crop history, soil, drought, land cover, weatherOne areal rollup — total acres, acres-by-crop, optional AOI-level soil/drought/weather
The question it answers“Enrich these specific boundaries.“Measure this area.
Field cap25 features per callNo cap — one histogram over the whole polygon

Size is never the router. A 40-acre AOI and a 400,000-acre AOI both go to aggregate; a single field and a 25-field farm both go to enrich.


POST /v1/fields/enrich

Boundaries you bring. For fields you already have — enrich each one.


A two-minute first call

Paste this and run it.

bash
curl -X POST https://uffda.ag/api/v1/fields/enrich \
  -H "Content-Type: application/json" \
  -d '{
    "type": "Feature",
    "geometry": {
      "type": "Polygon",
      "coordinates": [[
        [-93.620, 41.560], [-93.605, 41.560],
        [-93.605, 41.575], [-93.620, 41.575],
        [-93.620, 41.560]
      ]]
    }
  }'

The same call in Python:

python
import requests

resp = requests.post(
    "https://uffda.ag/api/v1/fields/enrich",
    json={
        "type": "FeatureCollection",
        "features": [
            {"type": "Feature", "id": "field-001",
             "geometry": {"type": "Polygon", "coordinates": [...]}}
        ],
        "layers": ["crop_history", "soil"],
        "options": {"cdl_years": [2020, 2021, 2022, 2023, 2024]}
    },
    timeout=35,
)
resp.raise_for_status()
data = resp.json()
for feat in data["features"]:
    print(feat["id"], feat["properties"]["enrichment"]["crop_history"])

And in JS:

javascript
const res = await fetch("https://uffda.ag/api/v1/fields/enrich", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    type: "FeatureCollection",
    features: fields, // your GeoJSON features
    layers: ["soil", "weather"],
    options: { weather_window: { start: "2025-04-01", end: "2025-10-31" } },
  }),
});
const data = await res.json();
console.log(data.disclaimer); // every response carries it
data.features.forEach(f => console.log(f.id, f.properties.enrichment));

What you POST

A GeoJSON Feature or FeatureCollection of field polygons. Coordinates in WGS84 (EPSG:4326). Standard GeoJSON — whatever MapLibre, Leaflet, or your own pipeline already works with.

Optional fields:

  • layersFilter which data layers to compute. Omit it and all run. Values: crop_history, soil, drought, land_cover, weather, forest_loss, protected_area, irrigation.
  • options.cdl_yearsWhich years of crop history to pull — 1 to 10 years, each between 2008 and 2030 (years beyond the latest published CDL vintage return an honest nodata status, not an error). Default: the trailing 8 years from the latest CDL vintage — currently 2018–2025.
  • options.weather_windowDate range for weather data. Default: trailing 12 months. Max span: 400 days (~13 months) per call — the weather layer caps each call to guard against runaway upstream queries. Need a longer history? Split into a few calls by date range and stitch the results client-side.
  • options.units"metric" (default) or "imperial". Affects display units; the envelope always names the canonical upstream unit and any conversion applied.

Limits: 25 features per call. 30-second wall-clock timeout. Both limits are sync-only constraints; a batch/async endpoint is on the roadmap.

Sending a large area polygon? If you have an area you want to measure — a county, a watershed, a drawn region — use POST /v1/aoi/aggregate instead. It uses a flat-cost histogram and has no feature cap.


What comes back

A GeoJSON FeatureCollection. Your input geometries come back unchanged, with two additions on each feature's properties:

  • derivedCentroid, area in hectares and acres, bounding box.
  • enrichmentOne block per layer, each value wrapped in a provenance envelope.

The top-level response also carries:

  • uffdaMetadata: API version, request ID, layer set, warnings, and your current rate-limit position.
  • disclaimerOn every response, success or error. (More on this below.)

If a layer fails for one field and succeeds for others, you get back the successful layers and a properties.errors note on the field that had trouble. The call returns 200. Partial failure is expected — don't assume all-or-nothing.


The provenance envelope, and why it's there

Every value in enrichment is wrapped in the same shape:

json
{
  "value":      27.3,
  "value_unit": "g/kg",
  "source": {
    "id":    "uffda:source/soilgrids",
    "name":  "ISRIC SoilGrids 2.0",
    "scope": "global · model estimate"
  },
  "license": {
    "code":               "CC-BY-4.0",
    "attribution":        "ISRIC — World Soil Information (2020). SoilGrids 2.0.",
    "informational_only": true
  },
  "vintage":    "2.0 (2020-present, stable)",
  "confidence": { "kind": "interval", "q05": 18.6, "q95": 38.1 },
  "units": {
    "canonical":  "g/kg",
    "displayed":  "g/kg",
    "conversion": null
  }
}

(Full schema in the API reference.)

Ag data has a unit-collision problem — SOM and SOC aren't the same number, and the errors look plausible until they aren't. The units block makes the conversion explicit, every time. The source.id pins each value to the upstream dataset. The license block carries the upstream attribution.

License fields are informational only. Every license block carries "informational_only": true. These are UFFDA's plain-language readings of upstream metadata — not legal advice. Verify license terms against the upstream source before relying on them for compliance or legal claims.

Layers

crop_history

USDA NASS Cropland Data Layer (CDL). Annual crop classification at 30m resolution, CONUS only, 2008 through the latest published vintage (currently 2025). Request any 1–10 years via options.cdl_years — default is the trailing 8 years from the latest vintage. Per-year value_label (e.g., "Corn", "Soybeans") plus a class-accuracy figure (~92% for US corn/soy from CDL metadata; lower for minor crops). Fields outside CONUS get a null value and a note in properties.errors.

Each year in crop_history.values[] carries a status field:

  • okA CDL crop class was returned for this year. value and value_label are populated.
  • nodataGenuine absence — CDL has no classification at this point for this year (unreleased year, off-coverage pixel). Not a failure; retrying will return the same answer.
  • unavailableThe lookup didn't come back — a transient upstream issue. The year entry also carries retryable: true and a plain-language note. Retrying the call is the right move; a future call will re-fetch rather than serve the failed result from cache.

The block also carries an availability roll-up across all requested years: "complete" (all years returned a result), "partial" (some years unavailable — retry will fill gaps), or "unavailable" (no years came back). Check this field first if you need a complete history; branch on individual status values for per-year handling.

soil

SSURGO as primary for CONUS fields (survey-grade, area-weighted over map units intersecting your polygon); SoilGrids 2.0 as companion (global model estimate, 16-point grid sample, native Q0.05/Q0.95 uncertainty band). Outside CONUS, SoilGrids is primary.

SSURGO contributes Soil Organic Matter (SOM, % by weight) as a single 0–30 cm topsoil aggregate (key: som@0-30). SOC and per-depth carbon slices come only from the SoilGrids companion (keys: soc@0-15, soc@15-30, soc@0-30, units: g/kg). SOM and SOC are different numbers — the Van Bemmelen conversion (SOM ÷ 1.724) appears in the units block as a clearly-labeled illustrative estimate, never a measured value.

Other metrics: pH, clay, sand, silt, CEC, nitrogen, bulk density. SoilGrids returns per-depth slices (0–15, 15–30, 0–30 cm); SSURGO uses a single 0–30 cm key for all metrics. A depth_note field on CONUS soil blocks flags that per-depth slices are available only from the SoilGrids companion.

On CONUS, the first (cold) call for a field returns SSURGO quickly (sub-2s typical) with companion_state: "pending" — the SoilGrids companion is deferred. An immediate re-hit attempts to hydrate the SoilGrids companion; ISRIC's response time varies and can run 20+ seconds, occasionally exceeding our per-layer timeout — in that case soil comes back null with a note in properties.errors for that call. That's upstream ISRIC latency, not a bug on our end; retry after a short wait rather than immediately. Check stats.soilgrids directly to know whether the companion is present — do not rely on the fromCache flag for this (it reflects the SSURGO portion only). Outside CONUS, SoilGrids is computed on the first call and this two-step pattern does not apply.

The companion_state field on CONUS responses tells you whether the SoilGrids companion is still hydrating or ready, without inspecting the companion object itself.

json
// CONUS, first call (cold) — SoilGrids not yet computed:
{ "companion": null, "companion_state": "pending" }

// CONUS, hydrated — companion is fully populated:
{ "companion": { /* SoilGrids values */ }, "companion_state": null }

// Outside CONUS — SoilGrids is primary; companion concept doesn't apply:
{ /* no companion_state key */ }

drought

US Drought Monitor — the latest weekly release. The vintage field carries the release date. The value_label reads like "D2 — Severe Drought". CONUS only.

Not public domain — carries a required verbatim credit. USDM permission terms require the exact NDMC/USDA/NOAA/NASA credit line returned in this layer's license.attribution field; copy it in full, don't shorten it to "US Drought Monitor."

land_cover

ESA WorldCover 2021 at 10m. Dominant class + proportions of others. The confidence.pct_in_class field tells you how much of your polygon was in the dominant class.

License: CC-BY-4.0 — attribution to ESA WorldCover is required; see this layer's license.attribution field for the citation to use.

weather

NASA POWER daily at the polygon centroid, summarized over your requested window (default: trailing 12 months). Summary metrics: GDD total, precipitation total, average high and low temperature. The units block declares base and conversion for each.

forest_loss

Hansen Global Forest Change v1.12 (UMD / Google / USGS / NASA), 30 m, global, annual 2001–2024. License: CC-BY-4.0. Returns forest_loss_pct (share of the field with tree-cover loss since 2001), forest_loss_most_recent_year, and a forest_loss_detail block with per-year loss in hectares and pixels plus a total loss area.

Hansen measures tree-cover loss — harvest, fire, disease, and other disturbance — not deforestation specifically. The layer is useful for flagging loss events and tracking change over time; read the provenance envelope before drawing conclusions about cause.

protected_area

USGS Protected Areas Database of the United States (PAD-US) 3.0, 2022. License: CC0 public domain. Returns a 4-state overlap check against all US protected lands — federal, state, local, private, tribal, and marine — with true polygon intersection (not centroid proximity). US and territories only.

The response carries a state field with one of four values:

  • no_overlapConfirmed all-clear — the field does not intersect any protected area per PAD-US 3.0.
  • overlapOne or more protected areas intersect the field. overlap_ac, overlap_pct, and an areas array with per-unit name, manager type, GAP status, and acreage are included.
  • out_of_coverageField is outside US / territory coverage — PAD-US does not apply.
  • unavailableThe upstream PAD-US service could not be reached at query time. Try again; a no_overlap is only ever returned when the service confirmed it — never on a failed call.

A warning field appears when the PAD-US query exceeded its result-transfer limit — the overlap check may be incomplete for that field; verify against the upstream source.

irrigation

NASA LP DAAC LGRIP30 L3 V002 (Landsat-Derived Global Rainfed and Irrigated-Cropland Product), 30 m, CONUS, 2020 single-year snapshot. License: CC0 (NASA ESDIS open data policy — no copyright restriction; attribution to NASA/USGS/LP DAAC and Teluguntla et al. (2018, 2024) encouraged). Returns the dominant irrigation classification for the field and a percentage breakdown of irrigated vs. rainfed vs. non-cropland pixels.

The response carries a state field with one of five values:

  • irrigatedDominant class is irrigated cropland (LGRIP30 class 2). Includes irrigated_pct, rainfed_pct, and non_cropland_pct breakdowns.
  • rainfedDominant class is rainfed cropland (LGRIP30 class 3). Same breakdown fields.
  • non_croplandNo irrigated or rainfed cropland detected — field classified as non-cropland.
  • out_of_coverageField is outside CONUS / LGRIP30 coverage — irrigation data is not available here.
  • unavailableCOG fetch or compute failed at query time. Try again; a result is only ever returned when the data was successfully sampled — never on a failed call.

Note: LGRIP30 is a single 2020 snapshot derived from Landsat-8. Irrigation status can change year to year. Informational only. Cite: Teluguntla et al. (2018) Remote Sensing 10(12):1920, DOI: 10.3390/rs10121920; dataset: Teluguntla et al. (2024) NASA LP DAAC LGRIP30 L3 V002.


POST /v1/aoi/aggregate

Area you bound. Draw or define a region — get its total acreage, crop breakdown, and optional whole-area soil, drought, and weather. No field cap; one flat-cost histogram over the exact polygon.

What you POST

A single GeoJSON Polygon or MultiPolygon in EPSG:4326, wrapped in an aoi field.

  • aoiRequired. GeoJSON Polygon or MultiPolygon, EPSG:4326.
  • yearCDL year (2008–current). Defaults to 2024. Acreage values carry the requested year; optional layers (soil, drought, weather) carry their own source and vintage fields describing their own data date.
  • includeOptional array of additional whole-area layers: "field_detail", "soil", "drought", "weather", "land_cover", "protected_area", "irrigation", "crop_type", "forest_loss". Omit for acreage-only (fastest). field_detail returns the count and size distribution of FTW field boundaries inside the AOI (CONUS only; geodesic areas). Add "units": "hectares" to get size stats in hectares instead of acres. The protected_area layer returns the same 4-state overlap result as the per-field endpoint, computed over the exact AOI polygon. land_cover is global (ESA WorldCover 2021, same as the enrich layer — see above) and runs both inside and outside CONUS. irrigation is CONUS-only (LGRIP30, 2020); outside CONUS it returns unavailable_off_conus. crop_type is global (ESA WorldCereal, 2021); percentages may not sum to 100% (independent binary predictions). forest_loss is accepted but not yet computed for whole areas — it returns an honest { state: "coming_soon" } today (per-field forest-loss stats are live on enrich). See Optional layers below for full response shapes.

AOIs outside the contiguous US: CDL acreage is CONUS-only, so the acreage block comes back { state: "unavailable_off_conus" } — never a fake zero. This is a 200, not an error: global layers (weather, land_cover, crop_type) still run with real values, and US-only layers you requested come back unavailable_off_conus with a note. A top-level coverage block ({ conus, ran[], skipped[] }) summarizes what ran and what was honestly skipped, so you never have to guess from a null.

What comes back

One areal rollup, not a per-feature array. The acreage block is always present; optional layers appear alongside it if requested. When "field_detail" is in the include array, field_detail returns total_n (distinct FTW field count, de-duped across tile seams) plus a size distribution with min/max/mean/median and histogram bins. Without it, field_detail.state is "skipped" — the acreage rollup is unaffected and no FTW tile fetch occurs.

How acreage is computed: acreage.total_ac and every by_crop[].acres_est are derived from the true geodesic (WGS84) area of the drawn polygon — the same value in aoi.area_ac — scaled by each CDL class's pixel share of the total raster. This keeps the two acres values consistent and removes the projection distortion that a raw pixel-count method would introduce at northern latitudes. Note: these acres are CDL satellite-classification estimates, not NASS farm-survey counts — the two typically differ 5–15%.

bash
curl -X POST https://uffda.ag/api/v1/aoi/aggregate \
  -H "Content-Type: application/json" \
  -d '{
    "aoi": {
      "type": "Polygon",
      "coordinates": [[
        [-97.5, 41.0], [-97.0, 41.0],
        [-97.0, 41.5], [-97.5, 41.5],
        [-97.5, 41.0]
      ]]
    },
    "year": 2024,
    "include": ["field_detail", "soil", "drought", "weather"]
  }'
json
{
  "aoi": {
    "area_ac": 142318.4,
    "area_ha": 57581.2,
    "cropland_pct": 72
  },
  "acreage": {
    "total_ac": 140127.6,
    "year": 2024,
    "provenance": "Acreage estimated by applying CDL class proportions to the true geodesic area…",
    "by_crop": [
      { "crop_code": "corn",     "acres_est": 58153.2, "pct": 41.5 },
      { "crop_code": "soybeans", "acres_est": 43439.6, "pct": 31.0 },
      { "crop_code": "other",    "acres_est": 38534.8, "pct": 27.5 }
    ]
  },
  "field_detail": {
    "state": "complete",
    "total_n": 1847,
    "size_distribution": {
      "unit": "acres",
      "min": 0.1, "max": 952.4, "mean": 53.1, "median": 15.2,
      "histogram": [
        { "label": "< 10 ac",    "count": 742 },
        { "label": "10–40 ac",   "count": 511 },
        { "label": "40–160 ac",  "count": 463 },
        { "label": "160–320 ac", "count": 89  },
        { "label": "320–640 ac", "count": 38  },
        { "label": "> 640 ac",   "count": 4   }
      ]
    }
  },
  "provenance": ["uffda:source/cdl", "uffda:source/ftw-global"],
  "disclaimer": "Informational only — not legal advice and not a warranty.",
  "uffda": { "api_version": "v1", "endpoint": "aoi/aggregate", "request_id": "aoi_…" }
}

Splitting large draws

The cropland histogram runs over the AOI's projected bounding box, and the upstream raster service caps a single rendered image at 4096 px per side (EPSG:3857, 30 m). Rather than rejecting AOIs that exceed that, aggregate splits the draw into sub-cells internally, fetches each in parallel, and merges the results into one histogram before computing acreage — you never see the seams:

  • Compact draws pass through up to roughly 50 million acres. Long, thin strips hit the limit at lower acreage — the bounding box's extent, not the drawn area alone, sets it.
  • acreage.computed_in_parts reports how many sub-cells were used — 1 for a single call, more for a split. Acreage stays exact either way; a split adds a note to the top-level warnings array.
  • Past that ceiling, the draw needs more sub-cells than the 30-second wall budget allows and returns 413 aoi_too_large, with details quoting the real parts_needed against the max_parts ceiling. You almost never need to split a draw manually below ~50M acres — let the endpoint do it.

Optional layers

land_cover Global · ESA WorldCover · 2021

Returns the areal share of each ESA WorldCover 2021 land-cover class within the AOI — the same 10m dataset as the per-field land_cover layer on enrich. Global coverage; runs inside and outside CONUS. License: CC-BY-4.0 — credit ESA WorldCover.

For very large AOIs the response carries a too_large state and no areal values — shrink the draw if you hit this.

irrigation CONUS only · LGRIP30 · 2020

Returns an areal breakdown of the AOI into three classes — Irrigated, Rainfed, and Non-cropland — each as a percentage, an acreage estimate, and a hectare estimate. Source: NASA LP DAAC LGRIP30 L3 V002 (Landsat-Derived Global Rainfed and Irrigated-Cropland Product), 30 m, nominal 2020 epoch. License: CC0 (NASA ESDIS open data policy; attribution to NASA/USGS/LP DAAC and Teluguntla et al. (2018, 2024) encouraged).

When the AOI centroid is outside CONUS, the response carries irrigation.state: "unavailable_off_conus" — no acreage fields are populated. LGRIP30 coverage is CONUS-only; this is not an error.

Note: LGRIP30 is a single 2020 snapshot. Irrigation status can change year to year. Values are informational only.

crop_type Global · ESA WorldCereal · 2021

Returns per-product areal share for four crop-type classes: Active cropland, Maize, Winter cereals, and Spring cereals — each as a percentage, an acreage estimate, and a hectare estimate. Source: ESA WorldCereal 2021 global map, 10 m, CC-BY-4.0. Attribution is required under CC-BY-4.0 — credit ESA WorldCereal and include a link to the license in any product that surfaces this data.

Important: these are independent binary predictions — each class is a separate yes/no model, not a mutually-exclusive classifier. The four percentages will not typically sum to 100% and are not expected to. Read each class on its own merits.

For very large AOIs, the response carries crop_type.state: "too_large" and no areal values — WorldCereal is a heavier read than the CDL histogram and has a lower practical size ceiling. Split or shrink the draw if you hit this.

forest_loss Coming soon

Accepted in include, but whole-area aggregation isn't built yet — the response returns an honest { state: "coming_soon" } rather than a fake value. Per-field forest-loss stats (Hansen Global Forest Change) are live today on enrich.

Have your own boundaries? Use POST /v1/fields/enrich to enrich each boundary individually — soil, crop history, drought, and more per feature. The field_detail layer here counts boundaries you don't have yet; enrich is for ones you do.

Rate limits and fair use

Default: 60 requests / hour per IP, 200 / day per IP. Burst: 10 in any 60-second window. A FeatureCollection counts as one request regardless of feature count.

Rate-limit position is in the uffda.rate_limit block on every response, and in standard headers:

text
X-RateLimit-Limit:     60
X-RateLimit-Remaining: 57
X-RateLimit-Reset:     1748462400
Retry-After:           42   (only on 429)

If you're building something that'll push against these — an MRV platform, a batch enrollment workflow, anything running at scale — reach out and we'll raise your limit. No contract required, just a note so we know who's hitting the endpoint.


Errors and edge cases

The error shape is consistent whether you get a 4xx or a 5xx:

json
{
  "error": {
    "code":    "feature_collection_too_large",
    "message": "v1 sync enrich accepts up to 25 features per call.",
    "hint":    "Split your collection into batches of 25 or fewer and call again.",
    "details": { "received": 87, "max": 25 }
  },
  "disclaimer": "Informational only — not legal advice and not a warranty.",
  "uffda": { "api_version": "v1", "request_id": "req_01HXKM4N7Y2Q…" }
}

Every error carries the same disclaimer and uffda envelope as a success response. Your error-handling code can rely on a single shape.

Common codes:

CodeHTTPWhat happened
feature_collection_too_large413Over 25 features. Split into batches.
geometry_too_complex413More than 10,000 coordinates total across all rings — outer ring plus any holes. Simplify upstream; vertex density doesn't improve results.
invalid_geometry422Missing, malformed, or not a Polygon/MultiPolygon.
unknown_layer422A value in layers[] isn't in the whitelist. details.unknown lists the offenders.
invalid_options422enrich only — a value in options failed validation (bad units, cdl_years, or weather_window). details.field names the offender.
missing_aoi / invalid_year422aggregate only — the aoi field is absent, or year is outside 2008–2030.
aoi_too_large413aggregate only — the draw needs more sub-cells than the 30s wall budget allows (past the ~50M-acre compact ceiling). details quotes the real parts_needed vs. max_parts. See Splitting large draws below.
upstream_error502aggregate only — the CDL histogram service didn't answer (or one sub-cell of a split failed — the whole call fails rather than returning a partial acreage total). Retry.
rate_limited429Check Retry-After for the wait in seconds.
compute_timeout504Hit the 30s wall clock. Try fewer layers or a smaller batch.
maintenance_mode503The endpoint is paused on purpose (kill switch), not broken. Response carries top-level maintenance_mode: true. Check back shortly.

Partial layer failure on a field — e.g., SoilGrids unavailable for one feature — returns 200 with the successful layers and the failure noted in properties.errors for that feature.


Common workflows

MRV field enrollment

You have farmer-supplied polygons and need to qualify fields for a carbon program. POST the collection, pull crop_history and soil, and filter by crop rotation and SSURGO organic matter. The per-year CDL history and the SSURGO–SoilGrids soil comparison are both in the response without a second call.

Gap-filling during verification

Your primary data source has gaps — a field with no farmer-reported crop history for a given year, or missing soil measurements. The CDL and SoilGrids values in the envelope are labeled with source, vintage, and confidence, so you can use them as fill-ins and document exactly what you filled with.

Batch enrollment at scale

v1 is sync, 25 fields per call. For a large enrollment run, loop your collection in batches of 25 and parallelize up to your rate-limit ceiling. The request_id on each response is your audit handle if something goes wrong mid-batch.


API stability

We grow /v1 steadily — new layers and data sources get added to the same endpoint. Adding a field to the response is backward-compatible: clients that don't recognize a key just ignore it.

We won't break what you're already using without notice and a parallel window to move. Removing or renaming a field, or changing what a value means, would be a breaking change — that gets a new endpoint address (/v2), not a silent swap. No schedule for that yet.

When a layer is added, we note it in the API changelog and here. No version bump; same URL.


What's next

v1 has two endpoints. What's next, in rough order:

  • Per-field crop records — extend the field_detail block (now shipping count + size stats) to add per-field dominant crop from CDL — labeled by vintage year, never implied as current ground truth.
  • Async/batch jobs — for collections larger than 25 or compute that needs more than 30 seconds. The sync cap and timeout are v1 constraints, not permanent ones.
  • GeoParquet output — alternative to GeoJSON for callers working at scale.

No dates on any of these. The Sandbox is where things land when they're real.

If there's a workflow this doesn't cover, that's worth knowing.


Disclaimer and attribution boilerplate

Every response carries this disclaimer. If you're surfacing UFFDA-enriched data in your own product, pass it through or adapt it.

Disclaimer (paste-ready):

Informational only — not legal advice and not a warranty. License and provenance fields are UFFDA's best-effort plain-language reading of upstream metadata. Verify license terms against the upstream source before relying on them for compliance, contract, or legal claims.

Attribution:

Each layer's license.attribution field carries the suggested attribution string for that upstream source, ready to drop into a credits block. For the endpoint itself:

Data enrichment via UFFDA API (uffda.ag). Underlying datasets: USDA NASS CDL (public domain), USDA SSURGO (public domain), ISRIC SoilGrids 2.0 (CC-BY-4.0), US Drought Monitor (custom terms — verbatim NDMC/USDA/NOAA/NASA credit required; see license.attribution), ESA WorldCover 2021, NASA POWER, Hansen Global Forest Change v1.12 (CC-BY-4.0, UMD / Google / USGS / NASA), NASA LP DAAC LGRIP30 L3 V002 (CC0, Teluguntla et al. 2018, 2024), ESA WorldCereal 2021 (CC-BY-4.0, attribution required).

v1 — live in production. If you hit something unexpected, let us know.