§1Service health & version
/healthz
Returns dataset version and applied schema migration. Useful as a liveness check.
Request
curl 'https://api.rootfast.org/healthz'
Response · 200
{
"ok": true,
"dataset_version": "0.0.1",
"schema_version": "001"
} Try it → https://api.rootfast.org/healthz
§2One plant, in full
/v1/plants/{usda_symbol}
Returns a plant record with growth habit, duration, native status by region, every USDA characteristic, and all Montana counties where the taxon is recorded. Resolves synonyms transparently.
Request
curl 'https://api.rootfast.org/v1/plants/PIPO'
Response · 200
{
"plant": {
"usda_symbol": "PIPO",
"scientific_name": "Pinus ponderosa Lawson & C. Lawson",
"scientific_name_canonical": "Pinus ponderosa",
"common_name": "ponderosa pine",
"family": "Pinaceae Spreng. ex Rudolphi",
"gbif_taxon_key": 5285053,
"gbif_match_type": "EXACT",
"rank": "Species",
"...": "..."
},
"growth_habits": ["Tree"],
"durations": ["Perennial"],
"native_status": [
{ "region_code": "L48", "status": "Native" },
{ "region_code": "CAN", "status": "Native" }
],
"characteristics": { "drought_tolerance": "High", "...": "..." },
"counties": [
{ "county_fips": "30031", "county_name": "Gallatin", "state_code": "MT", "nativity": "Native" }
]
} §3Flat catalogue
/v1/plants?{filters}&format={json|csv|ndjson}
Every taxon in the catalogue as a flat list, one row per plant — no county aggregation. Accepts the filter parameters under §F (except `nativity`, which requires a distribution join). Use `format=csv` or `format=ndjson` to download in bulk; the response is sent as an attachment with a `rootfast-plants.csv` filename. JSON pagination defaults to 5000 (max 10000); bulk downloads return up to 50000 in a single call.
Request
curl 'https://api.rootfast.org/v1/plants?family=Pinaceae&limit=2'
Response · 200
{
"count": 2,
"total": 45,
"paging": { "limit": 2, "offset": 0, "has_more": true },
"filters": { "family": "Pinaceae" },
"plants": [
{
"usda_symbol": "ABAM",
"scientific_name": "Abies amabilis (Douglas ex Loudon) Douglas ex Forbes",
"common_name": "Pacific silver fir",
"family": "Pinaceae Spreng. ex Rudolphi",
"genus": "Abies",
"rank": "Species",
"gbif_taxon_key": 5285101,
"...": "..."
}
]
} Try it → https://api.rootfast.org/v1/plants?family=Pinaceae&limit=2
§4Plant finder
/v1/plants/search
Cross-cutting search that takes a geographic scope (counties or states) plus filter criteria and returns plants ranked by how many of the requested counties they appear in. Useful for "I live near these places — what fits my conditions?" queries that don't fit a single county or state. Accepts every filter param from §F inside a `filters` object. Default limit 200, max 1000.
Request
curl -X POST 'https://api.rootfast.org/v1/plants/search' \
-H 'content-type: application/json' \
-d '{
"counties": ["30031", "30057", "30077"],
"nativity": "Native",
"filters": {
"growth_habit": "Tree",
"drought_tolerance": "High"
},
"limit": 5,
"include_counties": true
}' Response · 200
{
"scope": { "counties_requested": 3, "states_requested": null },
"filters": { "growth_habit": "Tree", "drought_tolerance": "High" },
"nativity": "Native",
"count": 5,
"limit": 5,
"plants": [
{
"usda_symbol": "ARTR2",
"scientific_name": "Artemisia tridentata Nutt.",
"common_name": "big sagebrush",
"family": "Asteraceae Bercht. & J. Presl",
"matching_county_count": 3,
"requested_county_count": 3,
"match_score": 1.0,
"nativity_rollup": "Native",
"matching_counties": ["30031", "30057", "30077"]
}
]
} §5Plant synonyms
/v1/plants/{usda_symbol}/synonyms
List every synonym known for the plant, drawn from two sources: USDA's Synonyms tab (`usda_synonyms_tab` — names without their own plant record) and any sibling rows in the plants table whose `accepted_plant_id` points back to this taxon (`plants_table`). The endpoint accepts either an accepted symbol or a synonym symbol — synonyms transparently resolve to the canonical plant and the response reports whether resolution happened via `resolved_from_synonym`.
Request
curl 'https://api.rootfast.org/v1/plants/EQAR/synonyms'
Response · 200
{
"accepted": {
"usda_symbol": "EQAR",
"scientific_name": "Equisetum arvense L.",
"scientific_name_canonical": "Equisetum arvense",
"common_name": "field horsetail",
"family": "Equisetaceae Michx. ex DC.",
"gbif_taxon_key": 7924597
},
"queried_symbol": "EQAR",
"resolved_from_synonym": false,
"count": 5,
"synonyms": [
{ "usda_symbol": "EQARA", "scientific_name": "Equisetum arvense L. var. alpestre Wahlenb.", "source": "usda_synonyms_tab" },
{ "usda_symbol": "EQARB", "scientific_name": "Equisetum arvense L. var. boreale (Bong.) Rupr.", "source": "usda_synonyms_tab" },
{ "usda_symbol": "EQCA", "scientific_name": "Equisetum calderi B. Boivin", "source": "usda_synonyms_tab" }
]
} §6Reverse lookup by GBIF
/v1/plants/by-gbif/{taxon_key}
Fetch the plant detail document by its GBIF Backbone taxon_key instead of by USDA symbol. Useful for joining rootfast to any dataset that already speaks GBIF (iNaturalist, ITIS, eBird, BHL, etc.). Response shape is identical to `/v1/plants/{usda_symbol}`. When multiple USDA plants map to the same GBIF key (synonyms), the accepted entry wins.
Request
curl 'https://api.rootfast.org/v1/plants/by-gbif/5285053'
Response · 200
{
"plant": {
"usda_symbol": "PIPO",
"scientific_name": "Pinus ponderosa Lawson & C. Lawson",
"common_name": "ponderosa pine",
"gbif_taxon_key": 5285053,
"gbif_match_type": "EXACT",
"...": "..."
},
"photo": { "url": "...", "thumb_url": "...", "license": "cc-by-nc" },
"growth_habits": ["Tree"],
"durations": ["Perennial"],
"characteristics": { "...": "..." },
"counties": [ { "county_fips": "30031", "county_name": "Gallatin", "state_code": "MT", "nativity": "Native" } ]
} §7A random plant
/v1/plants/random
Returns one random photographed species from the catalogue. Good for "plant of the day" widgets and seed examples. Cached at the edge for 60 s so it rotates often but cheaply.
Request
curl 'https://api.rootfast.org/v1/plants/random'
Response · 200
{
"plant": {
"usda_symbol": "EPSA",
"scientific_name": "Epilobium saximontanum Hausskn.",
"common_name": "Rocky Mountain willowherb",
"family": "Onagraceae Juss.",
"photo_thumb_url": "https://inaturalist-open-data.s3.amazonaws.com/photos/.../square.jpg",
"photo_url": "https://inaturalist-open-data.s3.amazonaws.com/photos/.../medium.jpg"
}
} §8All counties, paginated
/v1/counties?state={code}&limit={n}&offset={n}
Flat list of every county in the catalogue, optionally filtered by state. Default limit is 500, max 5000. Each row carries the FIPS code, state code, centroid lat/lon for mapping.
Request
curl 'https://api.rootfast.org/v1/counties?state=RI'
Response · 200
{
"count": 5,
"total": 5,
"limit": 500,
"offset": 0,
"has_more": false,
"counties": [
{
"county_fips": "44001",
"county_name": "Bristol",
"state_code": "RI",
"centroid_lat": 41.7,
"centroid_lon": -71.28
}
]
} §9Plants in a county
/v1/counties/{county_fips}/plants?{filters}
All taxa recorded in the given county, with nativity. FIPS is the 5-digit Census code (Gallatin County, MT is 30031). Accepts the filter parameters under §F.
Request
curl 'https://api.rootfast.org/v1/counties/30031/plants'
Response · 200
{
"county": {
"county_fips": "30031",
"county_name": "Gallatin",
"state_code": "MT"
},
"count": 1362,
"plants": [
{
"usda_symbol": "ABLA",
"scientific_name": "Abies lasiocarpa (Hook.) Nutt.",
"common_name": "subalpine fir",
"family": "Pinaceae Spreng. ex Rudolphi",
"nativity": "Native"
}
]
} §10List of states
/v1/states
States with ingested data, plus county and taxon counts. Filters out states present in the catalogue but lacking any distribution data — currently just Connecticut, pending USDA's republish against the new planning regions (see /about). CT remains addressable via /v1/states/CT for direct lookups.
Request
curl 'https://api.rootfast.org/v1/states'
Response · 200
{
"count": 54,
"states": [
{
"state_code": "MT",
"state_name": "Montana",
"state_fips": "30",
"country": "US",
"county_count": 56,
"taxa_count": 3215
}
]
} Try it → https://api.rootfast.org/v1/states
§11State header
/v1/states/{state_code}
Single-state header with county and taxon counts. State code is the two-letter postal abbreviation (case-insensitive).
Request
curl 'https://api.rootfast.org/v1/states/MT'
Response · 200
{
"state": {
"state_code": "MT",
"state_name": "Montana",
"state_fips": "30",
"country": "US",
"county_count": 56,
"taxa_count": 3215
}
} §12Plants in a state
/v1/states/{state_code}/plants?limit&offset&{filters}
Every taxon in the state, aggregated across counties. county_count is how many counties the taxon appears in; native_counties / introduced_counties / both_counties give the breakdown. nativity is the state-level rollup: "Native" if no introduced records, "Introduced" if no native records, "Both" if mixed or any county records "Both". Paginated (default limit 5000, max 10000) and accepts the filter parameters under §F.
Request
curl 'https://api.rootfast.org/v1/states/MT/plants'
Response · 200
{
"state": {
"state_code": "MT",
"state_name": "Montana",
"state_fips": "30",
"country": "US"
},
"counts": { "taxa": 3215, "counties": 56 },
"paging": { "limit": 5000, "offset": 0, "count": 3215, "has_more": false },
"plants": [
{
"usda_symbol": "PIPO",
"scientific_name": "Pinus ponderosa Lawson & C. Lawson",
"common_name": "ponderosa pine",
"family": "Pinaceae Spreng. ex Rudolphi",
"photo_thumb_url": null,
"county_count": 22,
"native_counties": 22,
"introduced_counties": 0,
"both_counties": 0,
"nativity": "Native"
}
]
} §13Counties in a state
/v1/states/{state_code}/counties
Lightweight listing of every county in the state with its taxa count. Useful for state-overview pages and county pickers.
Request
curl 'https://api.rootfast.org/v1/states/MT/counties'
Response · 200
{
"state": { "state_code": "MT", "state_name": "Montana", "state_fips": "30", "country": "US" },
"count": 56,
"counties": [
{
"county_fips": "30001",
"county_name": "Beaverhead",
"state_code": "MT",
"centroid_lat": 45.07,
"centroid_lon": -112.86,
"taxa_count": 755
}
]
} §14Hardiness zone reference
/v1/hardiness/zones
Canonical 26-row USDA Plant Hardiness Zone (2023) reference: every half-zone from 1a (-60 to -55 °F) to 13b (65 to 70 °F), with the count of ZIPs and counties currently mapped to each. zone_class_code is the USDA-published integer index (1=1a, 2=1b, …, 26=13b).
Request
curl 'https://api.rootfast.org/v1/hardiness/zones'
Response · 200
{
"count": 26,
"zones": [
{
"zone_code": "1a",
"zone_class_code": 1,
"temp_min_f": -60,
"temp_max_f": -55,
"description": "1a (-60 to -55 °F)",
"zip_count": 5,
"county_count": 0
}
]
} §15Single hardiness zone
/v1/hardiness/zones/{zone_code}
Same shape as one row of /v1/hardiness/zones. zone_code is the half-zone label like "5a" or "13b" (case-insensitive).
Request
curl 'https://api.rootfast.org/v1/hardiness/zones/5a'
Response · 200
{
"zone": {
"zone_code": "5a",
"zone_class_code": 9,
"temp_min_f": -20,
"temp_max_f": -15,
"description": "5a (-20 to -15 °F)",
"zip_count": 2426,
"county_count": 226
}
} §16Hardiness zone by ZIP
/v1/hardiness/zip/{zip}
USDA-published hardiness zone for the given US ZIP code. Accepts 5-digit ZIP and ZIP+4 (extra digits are ignored). Sourced from the PHZM 2023 Zip Code Table, 40,502 ZIPs total. Pair with a plant's temperature_min_f to filter the catalogue by what survives the winter where you live.
Request
curl 'https://api.rootfast.org/v1/hardiness/zip/59715'
Response · 200
{
"zip_code": "59715",
"zone": {
"zone_code": "5a",
"zone_class_code": 9,
"temp_min_f": -20,
"temp_max_f": -15,
"description": "5a (-20 to -15 °F)"
}
} §17Hardiness zone by county
/v1/hardiness/county/{county_fips}
Predominant hardiness zone for a county, sampled at the county centroid against the USDA PHZM polygon layer. A 5-digit FIPS code (e.g. 30031 for Gallatin, MT). Mainland and Hawaii are well-covered (3,199 / 3,227 counties). The 28 missings — mostly American Samoa, Northern Mariana Islands, and a handful of water-heavy coastal counties whose centroid falls outside the zone polygons — return `zone: null` with a note.
Request
curl 'https://api.rootfast.org/v1/hardiness/county/30031'
Response · 200
{
"county": {
"county_fips": "30031",
"county_name": "Gallatin",
"state_code": "MT"
},
"zone": {
"zone_code": "5a",
"zone_class_code": 9,
"temp_min_f": -20,
"temp_max_f": -15,
"description": "5a (-20 to -15 °F)"
}
} §18Full-text taxon search
/v1/search?q={query}
Searches across scientific name, common name, family, and genus. Returns up to 25 hits, ranked by BM25.
Request
curl 'https://api.rootfast.org/v1/search?q=ponderosa'
Response · 200
{
"query": "ponderosa",
"results": [
{ "usda_symbol": "PIPO", "scientific_name": "Pinus ponderosa Lawson & C. Lawson", "common_name": "ponderosa pine", "family": "Pinaceae Spreng. ex Rudolphi" },
{ "usda_symbol": "PIPOP", "scientific_name": "Pinus ponderosa Lawson & C. Lawson var. ponderosa C. Lawson", "common_name": "ponderosa pine", "family": "Pinaceae Spreng. ex Rudolphi" }
]
} §19OpenAPI 3.0 spec
/v1/openapi.json
Machine-readable description of every endpoint, parameter, response shape, and error code. Drop it into Postman / Insomnia / Bruno, generate typed clients with openapi-generator, validate responses in CI, or feed it to LLM tool-calling frameworks so AI agents can use the API directly. The companion interactive UI lives at /v1/docs (Scalar). Spec version 1.0.0.
Request
curl 'https://api.rootfast.org/v1/openapi.json'
Response · 200
{
"openapi": "3.0.3",
"info": {
"title": "Rootfast API",
"version": "1.0.0",
...
},
"servers": [
{ "url": "https://api.rootfast.org" }
],
"paths": { /* 19 paths */ },
"components": { /* schemas, parameters, responses */ }
} §BBulk download
Take the data.
The list endpoints accept ?format=csv or
?format=ndjson for archival / spreadsheet / pipeline
use. The response is sent as an attachment with a sensible
filename; Content-Type is set to
text/csv or application/x-ndjson as
appropriate. Bulk downloads default to no pagination cap (50 000
rows max per call); apply filters under §F to scope the result.
Endpoints that support bulk
- ·
/v1/plants— flat catalogue dump - ·
/v1/counties— every US county (also acceptsformat=geojson— see below) - ·
/v1/states/{code}/plants— one state's taxa - ·
/v1/counties/{fips}/plants— one county's taxa
GeoJSON output
/v1/counties?format=geojson returns a standard
GeoJSON FeatureCollection. Each feature is a Point at
the county centroid (we don't store full polygons yet); properties
carry the FIPS code, county name, and state. Drop the URL straight
into Leaflet, Mapbox GL, deck.gl, QGIS, or Mapshaper. Content-Type
is application/geo+json.
# Every Rhode Island county as GeoJSON
curl 'https://api.rootfast.org/v1/counties?state=RI&format=geojson'
# In Leaflet:
fetch('https://api.rootfast.org/v1/counties?state=RI&format=geojson')
.then(r => r.json())
.then(fc => L.geoJSON(fc).addTo(map)); Examples
# Whole catalogue as CSV curl -O 'https://api.rootfast.org/v1/plants?format=csv' # All MT plants as NDJSON (one JSON object per line) curl 'https://api.rootfast.org/v1/states/MT/plants?format=ndjson' \ > mt-plants.ndjson # Drought-tolerant native trees in Gallatin County as CSV curl 'https://api.rootfast.org/v1/counties/30031/plants?\ growth_habit=Tree&nativity=Native&drought_tolerance=High&format=csv' # Every US county as CSV (FIPS, name, state, centroid) curl 'https://api.rootfast.org/v1/counties?format=csv&limit=5000'
All data is CC0 / public domain — use it freely. A citation back to
rootfast.org is appreciated but not required.
§FFilter parameters
Narrow the list.
The list endpoints /v1/states/{code}/plants and
/v1/counties/{fips}/plants accept query parameters that filter
the result set. Multiple parameters AND together. Enum parameters
accept comma-separated lists (OR within the parameter). Invalid
values return 400 bad_request.
Plant-level
family- Prefix match against the USDA family string. Example:
?family=Pinaceae. genus- Exact match. Example:
?genus=Pinus. rank- One of
Species,Subspecies,Variety,Form. growth_habit- One of
Tree,Shrub,Subshrub,Vine,Forb/herb,Forb,Graminoid,Nonvascular,Lichenous. duration- One of
Annual,Biennial,Perennial. nativity- One of
Native,Introduced,Both. Filters at the county-record level.
Characteristics — enums
drought_toleranceHigh,Medium,Low,Noneshade_toleranceIntolerant,Intermediate,Tolerantmoisture_useHigh,Medium,Low,Nonegrowth_rateSlow,Moderate,RapidtoxicityNone,Slight,Moderate,SeverelifespanShort,Moderate,Longcommercial_availabilityNo Known Source,Routinely Available,Field Collections Only,Contracting Onlyfire_toleranceHigh,Medium,Low,Nonesalinity_toleranceHigh,Medium,Low,Nonenitrogen_fixationHigh,Medium,Low,None
Characteristics — booleans
fire_resistant- Accepts
1/0,true/false, oryes/no. palatable_human- Same.
nursery_stock_product- Same.
berry_nut_seed_product- Same.
lumber_product- Same.
Characteristics — colors (exact, case-insensitive)
flower_color- e.g.
Yellow foliage_color- e.g.
Green fruit_color- e.g.
Red
Characteristics — numeric ranges
min_height_ft/max_height_ft- Mature height in feet.
min_ph/max_ph- Soil pH tolerance.
min_precipitation_in/max_precipitation_in- Inches of annual precipitation.
min_frost_free_days- Minimum required frost-free days.
min_temperature_f/max_temperature_f- Minimum tolerable temperature in °F.
min_root_depth_in- Minimum root depth in inches.
Examples
# Drought-tolerant native trees in Gallatin County, MT GET /v1/counties/30031/plants?growth_habit=Tree&nativity=Native&drought_tolerance=High # Mature shrubs 3–10 ft, full-sun-tolerant, found in Rhode Island GET /v1/states/RI/plants?growth_habit=Shrub&shade_tolerance=Intolerant&min_height_ft=3&max_height_ft=10 # Pinaceae statewide in Montana GET /v1/states/MT/plants?family=Pinaceae # Trees or shrubs with high or medium drought tolerance GET /v1/states/AL/plants?growth_habit=Tree,Shrub&drought_tolerance=High,Medium
§EErrors
Errors are structured.
Every error returns a JSON body of
{ "error": { "code": "<machine-readable>", "message": "<human-readable>" } }.
The HTTP status reflects the failure category and the code string
is stable so clients can branch on it.
- 400
bad_request— Malformed query (e.g. invalidlimit).- 404
not_found— Unknown plant symbol, county FIPS, state code, or route.- 405
method_not_allowed— OnlyGETandHEADare supported.- 500
internal_error— Server-side fault. Rare; please include the URL when reporting.
§CCaching
Cache aggressively.
The dataset only changes when we re-import a SQLite chunk (rarely; never silently).
Every response includes a Cache-Control header tuned to that reality.
s-maxage applies at Cloudflare's edge; max-age is the
browser-side hint.
- /healthz
no-store— always fresh.- /v1/plants/{symbol}
max-age=3600, s-maxage=86400- /v1/plants/random
max-age=0, s-maxage=60— rotates ~every minute.- /v1/counties[/...]
max-age=600–3600, s-maxage=86400- /v1/states[/...]
max-age=600, s-maxage=86400- /v1/search
max-age=60, s-maxage=300
ETag + 304 Not Modified
Every cacheable response carries a weak ETag header derived
from the body content. Send it back as If-None-Match on a
follow-up request and the API will respond with
304 Not Modified and no body if nothing changed. Browsers
handle this automatically; library clients should opt in by storing
the ETag and including it on subsequent fetches.
# Conditional GET — second request is empty + 304 if unchanged
ETAG=$(curl -sI 'https://api.rootfast.org/v1/plants/PIPO' \
| awk '/^etag:/ {print $2}' | tr -d '\r')
curl -sI -H "If-None-Match: $ETAG" \
'https://api.rootfast.org/v1/plants/PIPO'
# HTTP/2 304
# etag: W/"abcdef1234567890"
# cache-control: public, max-age=3600, s-maxage=86400 §RRate limits
Be polite. Limits are generous.
The API is open and uncapped at the dataset level — CC0, no keys, no commercial gating. We do enforce per-IP rate limits to keep a single client from squeezing the service for everyone else. Limits are intentionally loose for normal browsing and tight only on the bulk-export and search endpoints, which are the only ones that can do real damage to the underlying D1.
- Default (most endpoints)
- 120 req / 60 s / IP — ≈ 2 requests/second sustained.
- Bulk export (?format=csv or ?format=ndjson)
- 5 req / 60 s / IP — pull whole-state catalogues sparingly.
- Search (/v1/search & POST /v1/plants/search)
- 60 req / 60 s / IP — debounce live-search UIs.
- /healthz, /v1/openapi.json, /v1/docs
- No limit. Cheap, edge-cached, frequently polled.
When you hit one
The API returns a 429 Too Many Requests with a
Retry-After: 60 header and a structured JSON body
so you can react properly.
# 429 response body
{
"error": {
"code": "rate_limited",
"message": "Bulk-export quota exceeded (5 req/min/IP). Slow down or filter the export."
},
"tier": "bulk",
"retry_after": 60
} Hitting the limits regularly under legitimate use? Get in touch via contact and we'll sort out a higher quota — limits are calibrated against today's traffic, not against any particular user.
§PPlanned
On the roadmap.
Items below are intentionally not implemented yet. They will land as the API matures and as real usage tells us they're needed. If one of these is blocking you, send a note via contact.
API keys
No keys exist today. Anonymous rate limits (see §R) are generous enough for nearly every legitimate use case. If you have a workload that consistently bumps against them, send a note via contact — we'll set up a self-serve key flow with a higher quota if there's real demand. Keys, if they land, will be free for non-commercial use; commercial use is already allowed under CC0 with no key needed.
No timeline — added only when real users need them.
§LLicense & attribution
Use it freely. Cite the sources.
The Rootfast dataset is derived from public-domain federal data. The original source records are in the public domain. The merged dataset and this API are published as a public good. No API key is required and there is no commercial gating planned.
If you build something with this — a tool, a garden plan, a research paper — we would love to hear about it. Full source, ETL pipelines, and the discovery notes for each upstream API live at github.com/cjscrofani/rootfast ↗.