Crops API
Reference for the public, read-only REST API powering crops.every.farm, including conventions, every endpoint under /api/v1, the snowflake scoring model, example integrations, and roadmap.
Public REST API powering crops.every.farm. All endpoints return JSON, are unauthenticated, read-only, and served from the Nuxt edge functions on Vercel.
Base URL:
https://crops.every.farm/api/v1
Every endpoint wraps its payload in a { "data": ... } envelope. List endpoints add a meta (or pagination) sibling for paging.
#Conventions
- Versioning — the
/v1prefix is permanent. Breaking changes will ship under a new major version. - Region — most endpoints accept
?region=lake_erie. Today onlylake_erieis populated; new regions will be added to/metaas they come online. - Filtering by list — comma-separated values:
?category=fruit,nut. - Errors — standard HTTP status codes with
{ "statusCode", "statusMessage" }.400= bad input,404= unknown slug,500= upstream/database failure. - Caching — most
/api/v1/*responses are SWR-cached at the Vercel edge (TTLs 1 h – 24 h). Endpoints that accept pagination or filter query params —/api/v1/crops,/api/v1/crops/compare,/api/v1/buyers, and/api/v1/audit-log— are served from origin on every request. Vercel's path-level ISR cache would strip query strings and serve one frozen response for every URL variant, so these endpoints opt out. See Caching & Performance for the full matrix.
#Endpoints at a glance
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/meta |
Self-describing index — enums, axes, available endpoints |
| GET | /api/v1/regions |
Active regions for the LocationPicker |
| GET | /api/v1/crops |
Paginated, filterable crop list |
| GET | /api/v1/crops/:slug |
Full crop detail (economics, soil, risks, snowflake, prices) |
| GET | /api/v1/crops/compare?slugs=… |
Side-by-side compare (2–6 crops) |
| GET | /api/v1/crops/:slug/snowflake |
Just the 5-axis suitability score |
| GET | /api/v1/crops/:slug/sources |
Per-crop audit trail and source attribution |
| GET | /api/v1/data-sources |
Registry of every upstream data source |
| GET | /api/v1/buyers |
Filterable list of every active buyer |
| GET | /api/v1/audit-log |
Workspace-wide change log with filters |
#GET /api/v1/meta
Returns a self-describing manifest of the API: every accepted enum value, the snowflake schema, and the live endpoint catalog. Call this first when integrating — it's the source of truth for valid filter values.
{
"data": {
"default_region": "lake_erie",
"available_regions": ["lake_erie"],
"categories": ["fruit", "vegetable", "grain", "nut", "herb", "fiber", "other"],
"lifecycles": ["annual", "perennial", "biennial"],
"price_trends": ["increasing", "stable", "declining"],
"sort_options": ["name_asc", "net_return_desc", "gross_revenue_desc", "total_score_desc", "years_to_production_asc"],
"snowflake": { "axes": ["market_fit", "climate_fit", "infrastructure_fit", "financial_return", "risk_profile"], "max_per_axis": 6, "max_total": 30 },
"list_filters": { "search": "string (ilike match on common_name)", "category": "csv of category enums", "zone": "integer USDA hardiness zone", "min_snowflake_total": "integer 0-30", "min_axis_<axis>": "integer 0-6 per axis", "fields": "csv; sparse response projection", "include": "csv of optional includes: price_history, last_audited_at", "...": "(see live /meta response for full catalog)" },
"list_response_fields": ["id", "slug", "common_name", "snowflake", "updated_at", "last_audited_at", "price_history", "…"],
"endpoints": { "list": "GET /api/v1/crops", "detail": "GET /api/v1/crops/:slug", "compare": "GET /api/v1/crops/compare?slugs=slug1,slug2", "snowflake": "GET /api/v1/crops/:slug/snowflake", "sources": "GET /api/v1/crops/:slug/sources", "audit_log": "GET /api/v1/audit-log", "data_sources": "GET /api/v1/data-sources", "regions": "GET /api/v1/regions", "buyers": "GET /api/v1/buyers", "meta": "GET /api/v1/meta" }
}
}list_filters and list_response_fields are machine-readable so downstream tooling (precision-polyculture pipelines, screener UIs, AI agents) can discover the full filter and projection surface of /api/v1/crops without reading the handler source. Hit the endpoint directly for the live catalog.
#GET /api/v1/regions
Lists every active region the platform supports. Drives the LocationPicker, useUserLocation.knownRegions, and any external consumer that needs to know what regions exist. The frontend falls back to a hardcoded list if this endpoint fails. Cached aggressively (swr: 3600) because regions change approximately never.
#Query parameters
None.
#Response
{
"data": {
"regions": [
{
"regionKey": "lake_erie",
"label": "Lake Erie Concord Grape Belt",
"description": "The grape-growing region along Lake Erie spanning Chautauqua County NY and Erie County PA.",
"country": "United States",
"adminRegion": "NY / PA",
"hardinessZone": "5b",
"lat": 42.35,
"lng": -79.32
}
],
"total": 1
}
}#GET /api/v1/crops
The screener. Paginated list of every crop in the database with snowflake totals and headline economics.
#Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
search |
string | — | ilike match against common_name |
category |
csv | — | Any of categories from /meta |
lifecycle |
csv | — | Any of lifecycles from /meta |
revenue_min / revenue_max |
number | — | Bounds on gross_revenue_per_acre (USD) |
net_return_min / net_return_max |
number | — | Bounds on net_return_per_acre (USD) |
price_trend |
csv | — | Any of price_trends |
insurance |
bool | — | true = only crops with USDA crop insurance |
years_to_production_max |
number | — | Useful to exclude long-establishment perennials |
region |
string | lake_erie |
Snowflake region (does not filter the crop list itself) |
sort |
enum | total_score_desc |
One of sort_options from /meta. Unrecognized values fall back to the default rather than erroring. |
page |
int | 1 |
1-indexed |
limit |
int | 20 |
Capped at 100 |
#Extended filters
In addition to the base filters above, the list endpoint accepts the following for finer-grained querying. All are additive and AND-combined:
| Param | Type | Example | Notes |
|---|---|---|---|
zone |
int | zone=6 |
USDA hardiness zone. Returns crops where hardiness_zone_min ≤ zone ≤ hardiness_zone_max. |
min_snowflake_total |
int 0–30 | min_snowflake_total=22 |
Lower bound on the regional snowflake total. |
min_axis_<axis> |
int 0–6 | min_axis_market_fit=5&min_axis_climate_fit=5 |
Lower bound per axis. One param per axis; combined AND. Valid axes: market_fit, climate_fit, infrastructure_fit, financial_return, risk_profile. |
max_years_to_production |
int | max_years_to_production=3 |
Alias for years_to_production_max — pick whichever name reads better in your caller. |
#Response shaping with fields= and include=
The default response returns the object shape shown below — one object per crop with a standard set of catalog columns plus a snowflake rollup. Two parameters let you trim or extend that shape:
fields=slug,net_return_per_acre,soil_ph_min,soil_ph_max— sparse projection. Only the listed columns are returned on each crop (plusidandslug, which are always present). Unknown field names are silently dropped. The full allowlist is returned by/api/v1/metaaslist_response_fields, and includes catalog columns that aren't in the default shape —soil_ph_min/soil_ph_max,water_req_inches,frost_free_days_min,gdd_min,typical_yield_per_acre,yield_unit,labor_hours_per_acre,establishment_cost_per_acre,economics_year,subcategory,description,hardiness_zone_min/hardiness_zone_max,updated_at.include=price_history,last_audited_at— optional includes.price_historyattaches the per-year price array inline under each crop;last_audited_atattaches a per-cropmax(data_audit_log.created_at)so pipelines can decide whether to re-pull without scanning the full audit log.
Example combining both: ?fields=slug,snowflake,updated_at&include=last_audited_at returns a minimal object per crop containing only the snowflake, the crop's updated_at, and the latest audit timestamp.
#Response
{
"data": [
{
"id": "…uuid…",
"common_name": "Black Walnut",
"slug": "black-walnut",
"scientific_name": "Juglans nigra",
"category": "nut",
"lifecycle": "perennial",
"image_url": "https://…",
"gross_revenue_per_acre": 1850,
"net_return_per_acre": 920,
"total_input_cost_per_acre": 930,
"price_trend_3yr": "increasing",
"crop_insurance_available": false,
"years_to_full_production": 12,
"data_completeness": 78,
"snowflake": {
"total_score": 22,
"axes": { "market_fit": 5, "climate_fit": 5, "infrastructure_fit": 4, "financial_return": 4, "risk_profile": 4 }
}
}
],
"meta": { "page": 1, "limit": 20, "total": 79, "total_pages": 4 }
}#GET /api/v1/crops/:slug
Full dossier for a single crop. This is the same payload that drives the public crop detail page.
#Path parameter
slug— kebab-case crop slug (e.g.black-walnut,elderberry,hops).
#Query parameters
region— snowflake region (defaultlake_erie).
#Response shape
data
├── crop ← row from `crops` table (everything except joined relations)
├── economics ← single `crop_economics` row, or null
├── snowflake
│ ├── total_score ← 0–30
│ ├── max_score ← always 30
│ ├── region
│ └── axes[] ← ordered: market_fit, climate_fit, infrastructure_fit, financial_return, risk_profile
│ ├── axis
│ ├── value ← 0–6
│ ├── max_value ← always 6
│ └── checks[] ← the 6 underlying checks per axis with score + rationale
├── soil_preferences[]
├── drainage_preferences[]
├── market_channels[]
├── risks[]
├── equipment[]
├── storage[]
└── price_history[] ← per-year wholesale price points, sorted ascendingReturns 404 if the slug doesn't exist.
#GET /api/v1/crops/compare
Side-by-side comparison for 2–6 crops. Preserves the order of slugs you pass in (so the UI's column order is stable).
#Query parameters
slugs— required, comma-separated. Min 2, max 6. Unknown slugs are silently dropped.region— snowflake region (defaultlake_erie).
#Response
{
"data": {
"crops": [
{
"slug": "sweet-corn",
"common_name": "Sweet Corn",
"scientific_name": "Zea mays",
"category": "vegetable",
"lifecycle": "annual",
"image_url": "…",
"hardiness_zone_min": 3,
"hardiness_zone_max": 11,
"years_to_full_production": 1,
"labor_hours_per_acre": 22,
"economics": {
"gross_revenue_per_acre": 2400,
"net_return_per_acre": 780,
"establishment_cost_per_acre": 0,
"annual_operating_cost_per_acre": 1620,
"price_trend_3yr": "stable",
"crop_insurance_available": true
},
"snowflake": { "total_score": 24, "axes": { "market_fit": 5, "climate_fit": 5, "infrastructure_fit": 5, "financial_return": 4, "risk_profile": 5 } }
}
]
}
}#Errors
400— fewer than 2 slugs or more than 6.404— none of the requested slugs matched.
#GET /api/v1/crops/:slug/snowflake
Lightweight version of the detail endpoint that returns only the 5-axis suitability score. Use this when you want to render snowflakes in a list without paying for the full crop payload.
#Query parameters
region— snowflake region (defaultlake_erie).detail—trueto include the per-check breakdown under each axis. Defaults tofalse(justaxis,value,max_value).
#Response (without detail)
{
"data": {
"crop_id": "…uuid…",
"slug": "hops",
"common_name": "Hops",
"region": "lake_erie",
"total_score": 21,
"max_score": 30,
"axes": [
{ "axis": "market_fit", "value": 4, "max_value": 6 },
{ "axis": "climate_fit", "value": 5, "max_value": 6 },
{ "axis": "infrastructure_fit", "value": 3, "max_value": 6 },
{ "axis": "financial_return", "value": 5, "max_value": 6 },
{ "axis": "risk_profile", "value": 4, "max_value": 6 }
]
}
}#GET /api/v1/crops/:slug/sources
Full data-provenance trail for one crop — every field that has ever been written, with the source name, source URL, source date, and last-updated timestamp. This is what powers the "Sources" section on each crop page.
#Query parameters
table— filter to a single underlying table (e.g.crop_risks,crop_economics).limit— max audit entries returned (default100, max500).
#Response
data
├── crop ← { id, common_name, slug }
├── total_changes ← integer
├── source_summary[] ← grouped by table
│ ├── table_name ← raw, e.g. `crop_economics`
│ ├── display_name ← human label, e.g. "Economics & Pricing"
│ └── sources[] ← { source_name, source_url, source_date, last_updated, change_count }
└── audit_log[] ← raw rows from `data_audit_log`, newest first#GET /api/v1/buyers
Filterable list of every active buyer in the catalog. Powers the public /buyers browse page and is available as a REST endpoint for external consumers.
#Query parameters
| Param | Type | Example | Notes |
|---|---|---|---|
search |
string | search=walnut |
ilike match against the buyer name |
type |
csv | type=processor,distributor |
Filter by buyer type. Current values: cooperative, distributor, processor, retailer, wholesaler |
state |
csv | state=NY,PA |
Filter by US state code |
accepts_contracts |
bool | accepts_contracts=true |
Only buyers that offer contracts |
accepts_spot |
bool | accepts_spot=true |
Only buyers that offer spot purchases |
crop |
csv | crop=black-walnut,hops |
Buyers who purchase ANY of the listed crop slugs |
include |
csv | include=crops |
Inline the list of crops each buyer purchases, with price per unit when available |
limit |
int | limit=50 |
Cap (default 200, max 500) |
#Response
{
"data": [
{
"id": "…uuid…",
"slug": "hammons-products-company",
"name": "Hammons Products Company",
"buyer_type": "processor",
"description": "Family-owned buyer and processor of wild American black walnuts…",
"city": "Stockton",
"state": "MO",
"website": "https://black-walnuts.com",
"contact_email": null,
"contact_phone": "+1-888-429-6887",
"accepts_contracts": false,
"accepts_spot": true,
"has_location": false,
"is_research_only": true,
"source_name": "Hammons Products Company – About Us",
"source_url": "https://black-walnuts.com/about-us/",
"source_date": "2026-04-14",
"updated_at": "2026-04-14T22:06:17.160138+00:00",
"crops": [
{ "crop_id": "…", "slug": "black-walnut", "common_name": "Black Walnut", "price_per_unit": null, "price_unit": null }
]
}
],
"meta": { "total": 40 }
}- Only
is_active = truebuyers are ever returned. Deactivated rows are invisible to the public surface. has_locationis a derived boolean reflecting whether the buyer has been geocoded. Buyers without coordinates still appear in this list — the proximity-search RPCnearby_buyersis the one that filters them out.is_research_onlyflags buyers compiled from public web research (not directly contacted). Surface this in your UI if you're presenting the data to end users.
#GET /api/v1/data-sources
Registry of every upstream data source the platform pulls from — USDA NASS, university extensions, peer-reviewed journals, market reports, etc. Useful for citing data programmatically.
#Query parameters
| Param | Notes |
|---|---|
type |
government, university_extension, journal, industry, market_report |
region |
Source covers this region (e.g. lake_erie, national) |
coverage |
Source covers this data category (e.g. economics, climate) |
#Response
{
"data": {
"sources": [
{ "id": "…", "name": "USDA NASS Quick Stats", "source_type": "government", "url": "https://quickstats.nass.usda.gov", "regions": ["national", "lake_erie"], "coverage": ["economics", "yield"], "last_accessed": "2026-04-01" }
],
"total": 88
}
}#GET /api/v1/audit-log
Workspace-wide change feed. Every insert, update, and delete to a crop-related table is logged here with the source citation that triggered it. Useful for AI agents that need to verify data freshness or build dashboards.
#Query parameters
| Param | Notes |
|---|---|
crop |
partial (ilike) match on crop_name |
table |
exact table name (e.g. crop_risks) |
action |
insert, update, or delete |
task_run |
filter by a specific ingestion run ID |
region |
exact region match |
since |
ISO 8601 date — only entries on or after this timestamp |
limit |
default 50, max 500 |
offset |
for pagination |
#Response
{
"data": {
"entries": [
{
"id": "…",
"created_at": "2026-04-15T18:42:00Z",
"crop_name": "Black Walnut",
"table_name": "crop_economics",
"record_id": "…uuid…",
"action": "update",
"field_name": "gross_revenue_per_acre",
"old_value": "1700",
"new_value": "1850",
"change_summary": "Annual economics refresh — PSU Extension 2026 budget",
"source_type": "automated_daily",
"source_name": "PSU Extension",
"source_url": "https://extension.psu.edu/…",
"source_date": "2026-03-12",
"task_run_id": "…",
"task_day_type": "economics_refresh",
"region": "lake_erie"
}
],
"pagination": { "total": 4366, "limit": 50, "offset": 0, "has_more": true }
}
}#Snowflake scoring model
Every crop has a regional snowflake score out of 30, broken into 5 axes of 6 points each. Each axis is the sum of 6 binary/scaled checks (e.g. "price trend is positive over 3 years", "crop insurance available", "established processing infrastructure within region").
| Axis | What it captures |
|---|---|
market_fit |
Demand strength, buyer concentration, pricing power |
climate_fit |
Hardiness-zone match, water needs, heat/cold tolerance |
infrastructure_fit |
Equipment availability, storage, processing within region |
financial_return |
Net return per acre, payback period, capital intensity |
risk_profile |
Pest/disease pressure, weather risk, regulatory exposure |
Use /api/v1/crops/:slug/snowflake?detail=true to see the individual checks and the rationale string we wrote when assigning each score.
#Example integrations
#Top 5 nuts by net return
GET /api/v1/crops?category=nut&sort=net_return_desc&limit=5#Compare three perennials
GET /api/v1/crops/compare?slugs=elderberry,hops,black-walnut#Quote a snowflake on an external site
GET /api/v1/crops/elderberry/snowflake#Show users where a number came from
GET /api/v1/crops/black-walnut/sources?table=crop_economics#High-confidence fruit candidates (precision-polyculture)
GET /api/v1/crops?category=fruit&min_snowflake_total=22&min_axis_market_fit=5#All crops compatible with USDA zone 6
GET /api/v1/crops?zone=6&limit=100#Short-payback perennials
GET /api/v1/crops?lifecycle=perennial&max_years_to_production=3#Sparse-field pipeline pull
Only the columns needed for per-pixel scoring, with cache-invalidation metadata:
GET /api/v1/crops?fields=slug,net_return_per_acre,soil_ph_min,soil_ph_max,hardiness_zone_min,hardiness_zone_max,snowflake&include=last_audited_at&limit=100#Find buyers for a specific crop
GET /api/v1/buyers?crop=black-walnut&include=crops#All processors that accept spot purchases
GET /api/v1/buyers?type=processor&accepts_spot=true#Recently shipped
Changes since the initial API doc. Listed newest first so callers can see at a glance whether they need to refresh their integration.
#2026-04
- Pagination and filtering on
/api/v1/cropsand/api/v1/crops/compareactually honor query params. Previously, Vercel's path-level ISR cache was stripping the query string, so every variant of those URLs returned the same frozen payload. Those endpoints now serve from origin on every request. If your integration was working around this by fetching crops one slug at a time, you can drop that workaround. sort=total_score_descpaginates correctly. Previously the sort applied after pagination (so “top 5” was really the alphabetically-first 5 re-ordered among themselves). It now fetches the full filtered set, sorts globally, and slices the requested page.- Deterministic pagination order. All paginated queries carry a secondary ordering on the primary key, so rows tied on the user-selected sort column don't shift between pages or appear twice across requests.
- New filters on
/api/v1/crops:zone(USDA hardiness-zone match),min_snowflake_total,min_axis_<axis>(one per snowflake axis), andmax_years_to_production(alias foryears_to_production_max). fields=sparse projection. Request only the columns you need. The allowlist exposes catalog columns not in the default shape —soil_ph_min/soil_ph_max,water_req_inches,frost_free_days_min,gdd_min,typical_yield_per_acre,yield_unit,labor_hours_per_acre,establishment_cost_per_acre,economics_year,subcategory,description,hardiness_zone_min/hardiness_zone_max,updated_at.include=optional extensions.price_historyinlines the per-year price array;last_audited_atattaches a per-cropmax(data_audit_log.created_at)so downstream pipelines can cache-invalidate without scanning the full audit log./api/v1/metais self-describing. Newlist_filtersandlist_response_fieldskeys let tooling discover the full filter and projection surface without reading handler source./api/v1/regionsendpoint. Data-driven region catalog. Adding a row to theregionstable automatically surfaces the region in the picker without a code change.
#Roadmap
- Additional regions beyond
lake_erie(Pacific Northwest and Southern Appalachia are next). - Issued API keys for production integrations and per-key usage controls.
- Webhooks on audit-log entries for downstream consumers.
Updated
Something missing or out of date? Tell us — the docs are updated with every release.