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 /v1 prefix is permanent. Breaking changes will ship under a new major version.
  • Region — most endpoints accept ?region=lake_erie. Today only lake_erie is populated; new regions will be added to /meta as 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 (plus id and slug, which are always present). Unknown field names are silently dropped. The full allowlist is returned by /api/v1/meta as list_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_history attaches the per-year price array inline under each crop; last_audited_at attaches a per-crop max(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 (default lake_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 ascending

Returns 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 (default lake_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 (default lake_erie).
  • detail — true to include the per-check breakdown under each axis. Defaults to false (just axis, 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 (default 100, max 500).

#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 = true buyers are ever returned. Deactivated rows are invisible to the public surface.
  • has_location is a derived boolean reflecting whether the buyer has been geocoded. Buyers without coordinates still appear in this list — the proximity-search RPC nearby_buyers is the one that filters them out.
  • is_research_only flags 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/crops and /api/v1/crops/compare actually 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_desc paginates 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), and max_years_to_production (alias for years_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_history inlines the per-year price array; last_audited_at attaches a per-crop max(data_audit_log.created_at) so downstream pipelines can cache-invalidate without scanning the full audit log.
  • /api/v1/meta is self-describing. New list_filters and list_response_fields keys let tooling discover the full filter and projection surface without reading handler source.
  • /api/v1/regions endpoint. Data-driven region catalog. Adding a row to the regions table 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.