Crops API

Reference for the public, read-only REST API powering crops.every.farm: conventions, every endpoint under /api/v1 including farm-conditions matching at /recommend, the snowflake model, example integrations, and roadmap.

Public REST API powering crops.every.farm. All endpoints return JSON, are unauthenticated, read-only, and served by Nuxt server routes 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 and finger_lakes are populated; /meta lists available_regions from the regions table. For /api/v1/recommend, region is an optional preset — the endpoint is designed to take a farm's own conditions instead.
  • Filtering by list — comma-separated values: ?category=fruit,nut.
  • Browsers — the API sends no CORS headers, so call it from a server rather than from a page on another origin.
  • Errors — standard HTTP status codes with { "statusCode", "statusMessage" }. 400 = bad input, 404 = unknown slug, 500 = upstream/database failure.
  • Caching — /meta (24 h), /regions, /data-sources and /crops/:slug with its sub-resources (1 h) are cached at the Vercel edge per full URL, so query parameters such as ?region= and ?detail= are honoured and each combination is cached separately. /crops, /crops/compare, /buyers, /audit-log, /recommend, /systems/evaluate, /interactions, /interactions/guilds and /regions/presets are served from origin on every request. 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, POST /api/v1/recommend Rank crops against a farm's conditions — per-criterion verdicts with evidence status, and an FAO land-evaluation class per crop
GET, POST /api/v1/systems/evaluate Evaluate a crop system (guild, intercrop, alley, understory, rotation) on a farm: each crop's FAO class, the system class, interactions by land quality, measured land equivalent ratio
GET /api/v1/interactions Crop-to-crop interactions (companions, antagonists, rotation effects), every row cited
GET /api/v1/interactions/guilds Guilds: crops that go together around a chosen crop
GET /api/v1/regions Active regions for the LocationPicker
GET /api/v1/regions/presets Climate presets that /recommend accepts as region, including ones not in the picker
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/fit One crop's fit across the region presets
GET /api/v1/crops/:slug/cultivars Named cultivars of a crop, with sources
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": ["finger_lakes", "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|POST /api/v1/recommend

Rank the catalog against a farm's conditions. This is the endpoint built for agents: the farm's context (fields, soils, topography, infrastructure) lives with the caller; send it as parameters and get back crops worth trialling, each with a per-criterion breakdown and the evidence status of every crop attribute read.

POST accepts the same fields as a JSON body, which is easier for arrays.

#Conditions

All optional — every condition you supply becomes one criterion per crop; supply more to get a finer ranking. Two criteria are always evaluated whatever you send: risk_load (the share of high-severity risks on record) and buyers (buyers on record). They count toward the score but never cap the tier.

Group Parameter Meaning
preset region Fills zone, gdd, frost_free_days, annual_precip_in from a named region: lake_erie (lake plain: zones 6a–7a, 2,923 GDD, 185 frost-free days, 41.1 in), chautauqua_uplands (Allegheny Plateau around Jamestown: 6a, 2,364 GDD, 158 days, 45.9 in), finger_lakes (Seneca, Cayuga, Keuka and Canandaigua lake slopes: 6a–6b, 2,418 GDD, 172 days, 30.2 in). Each is the most conservative of the region's NOAA stations. Values are NOAA 1991–2020 normals and the 2023 USDA hardiness map; /api/v1/meta lists them under recommend.region_presets. Explicit values override, so "Lake Erie, but a hillside with 150 frost-free days" is one call.
climate zone USDA hardiness zone, 6 or 6a. Hard gate for crops that overwinter (perennials and fall-sown grains); n/a for spring- and summer-sown annuals, whose range is set by season length and heat instead.
gdd Growing degree days, base 50 °F, seasonal accumulation
frost_free_days Length of the frost-free season
chill_hours Winter chilling hours (32–45 °F). Perennials only; n/a for annuals
annual_precip_in Mean annual precipitation, inches
soil / site soil_texture Comma list of texture classes; the best one on the farm counts. Hard gate.
drainage One NRCS drainage class. Hard gate.
soil_ph Soil pH. Stored crop ranges are extension lime targets, so a reading within 1.0 of the range is marginal (correctable with lime or sulfur) and only a reading more than 1.0 outside it is a gate
air_drainage good · moderate · poor — how well cold air drains off the field. Scores spring-frost risk at bloom for perennials; a frost pocket under a frost-sensitive crop is unfit. frost_pocket=true is shorthand for poor
sun_exposure full_sun · partial_shade · shade — for alley and understory positions in agroforestry designs
slope_pct Scored against a crop's workable slope limit where recorded
aspect Accepted but not scored (reported in unsupported_conditions); use air_drainage for frost risk
infrastructure irrigation true relaxes the rainfall criterion
cold_storage true/false — refrigerated storage on farm
labor_hours_per_acre Labor the farm can commit per acre per year
economics max_years_to_production Planning horizon
max_establishment_cost_per_acre Capital ceiling, $/acre
min_net_return_per_acre Target net return, $/acre
strategy categories, exclude_categories, lifecycle Narrow the candidate set
exclude_crops (alias already_growing) Slugs the farm already grows — diversification excludes them
output include_unfit true returns crops that failed a hard gate, with the reason (default false)
include_criteria false omits the per-criterion breakdown (default true)
limit 1–100, default 25

Vocabulary for soil_texture and drainage is published under recommend.vocab in /api/v1/meta. Unknown values are reported in warnings and ignored rather than failing the request.

#Response

{
  "data": {
    "conditions": { "zone": 6, "frost_free_days": 180, "soil_texture": ["silt_loam"], "drainage": "moderately_well_drained", "soil_ph": 6.4 },
    "unsupported_conditions": [],
    "excluded_unfit": 23,
    "results": [
      {
        "slug": "cabbage",
        "common_name": "Cabbage",
        "category": "vegetable",
        "lifecycle": "biennial",
        "score": 100,
        "fit": "strong",
        "gates_failed": [],
        "coverage": { "evaluated": 11, "unknown": 0, "not_applicable": 0, "cited_share": 0.45, "contested": [] },
        "gaps": [],
        "criteria": [
          {
            "key": "drainage", "label": "Drainage", "verdict": "fit", "weight": 3,
            "farm": "moderately_well_drained", "crop": "ideal",
            "note": "moderately well drained ground is rated ideal for this crop.",
            "cited": true, "reads": ["crop_drainage_preferences"]
          }
        ],
        "fao": {
          "profile": "fao1983-max-limitation", "combination": "maximum_limitation",
          "class": "S2", "subclass": ["oxygen"], "unrated": [], "management_level": "intermediate",
          "ratings": [
            { "land_quality": "oxygen", "rating": "s2", "criteria": ["drainage"], "basis": "catalog_rating" },
            { "land_quality": "nutrient_availability", "rating": "s1", "criteria": ["soil_ph"], "basis": "graded_requirement", "requirement_id": "…", "note": "soil_ph against optimal/absolute limits" }
          ]
        },
        "evidence_url": "/api/v1/crops/cabbage/sources",
        "detail_url": "/api/v1/crops/cabbage"
      }
    ]
  },
  "meta": { "crops_considered": 81, "generated_at": "…", "engine_version": "2026-10-23.1" }
}

#How to read it

  • verdict is one of fit · marginal · unfit · unknown · n/a. unknown is never scored as unfit — it means the catalog has not recorded that attribute for that crop. It is counted in coverage.unknown and the attribute is listed in gaps, so an agent can tell a poor fit from a poorly-researched crop.
  • fit tiers: strong (score ≥ 75, ≥ 60 % of asked criteria evaluable, and no criterion unfit), possible (≥ 50), poor (below, or a hard gate failed), unknown (nothing evaluable). Coverage and known problems cap the tier: a crop scoring high on two evaluated criteria with five unknown is possible, not strong, and so is a peach orchard on a frost pocket.
  • cited on each criterion, and cited_share per crop, say how much of the recommendation rests on a supporting source citation (primary, corroborating or derived). Follow detail_url for the quoted citations; evidence_url is the crop's change log.
  • contested: true on a criterion, and the keys in coverage.contested, mean a source on record contradicts the stored value. A contradicting citation never counts toward cited. Tell the grower the sources disagree and show both before relying on that criterion.
  • fao is the FAO land-evaluation class (FAO 1976 Framework; Soils Bulletin 52, 1983): S1 highly, S2 moderately, S3 marginally suitable, N not suitable, with subclass naming the limiting land qualities. Each physical criterion is rated on the land quality it assesses (s1–s3, n). pH and annual rainfall are rated against the crop's graded requirements where they exist (mostly FAO ECOCROP optimal and absolute limits, basis: "graded_requirement"), the rest from the criterion's verdict (ideal s1, suitable s2, marginal s3, unfit n; pH inside the lime band is s2, correctable by liming). The default combination is maximum limitation: the class is the worst rating, and any n is N. Send profile=fao1983-multiplicative for the multiplicative rule. Economics, labour, storage and years to production are not land qualities, so they stay in score. A crop can score 100 and still be S2: the score lets strengths offset weaknesses, the FAO class does not.
  • Score is agronomic fit unless you send economics. With climate and soil alone, broad-tolerance commodity crops (buckwheat, oats, dry bean) score 100 — correctly, they can grow there. To propose diversification, also send min_net_return_per_acre, max_establishment_cost_per_acre, labor_hours_per_acre, categories, and exclude_crops.

#Errors

  • 400 — no farm conditions supplied, or a non-object POST body.

#GET /api/v1/interactions

Cited pairwise crop interactions, the building block for companion, guild, alley and rotation design. Every interaction carries the source quote that documents it; there are no uncited rows.

Parameter Meaning
crops Comma list of crop slugs (required unless crop or all is given)
all true returns every interaction in the catalog, for graph views
mode within (default): only interactions where both crops are in crops ("what happens if I put these together?"). any: every interaction touching any listed crop ("what goes well or badly with these?")
crop One slug; shorthand for crops=<slug>&mode=any
type Comma list of interaction types: allelopathic, shared_pest_host, alley_overstory, understory, nitrogen_source, rotation_benefit, rotation_conflict, pest_suppression, pollination_partner, nurse, competition
effect Comma list of positive, negative, neutral

Each interaction returns a, b (slugs and names), interaction, effect, magnitude, evidence_strength, mechanism, spatial_pattern, applies_when, and sources (publisher, URL, verbatim quote). In within mode the response also carries pairs — total, documented and the list of undocumented pairs — because an undocumented pair is a research gap, not a clean bill. A pair that is documented for several reasons (two shared pests, say) keeps every mechanism in mechanism, separated by "Also:". Direction matters: (a, b, allelopathic) means a affects b. Symmetric types (shared_pest_host, competition) are stored once. neutral records a documented tolerance, for example a crop observed to grow normally within black walnut's root zone. Negative interactions sort first.

GET /api/v1/interactions?crops=black-walnut,tomato,sweet-corn,pumpkin

#GET /api/v1/interactions/guilds

Mutual-benefit clusters ("guilds"): maximal sets of catalog crops in which every pair has a documented benefit of growing them together — nurse, nitrogen, alley, understory, pest suppression or pollination. Rotation benefits are sequences, so they are returned separately as chains.

Parameter Meaning
mode strict (default): every pair needs a documented positive interaction. loose: a documented tolerance (for example a crop unaffected by walnut juglone) also counts as compatible; each guild still needs at least one positive pair
exclude_negative true (default) drops guilds with any documented negative between members (shared pest, rotation conflict, competition); false returns them with the negatives listed
crop Only results that include this crop, plus partners: every crop it has a documented grow-together relation with

Returns guilds (members, the compatible pairs with their interaction kinds and source counts, and any negatives), rotation_chains (sequences where every step is a documented rotation benefit, with any later-conflict flagged) and trial_candidates: pairs with no documented relation whose positive documentation would complete a guild, because both already have documented benefits with the same partners. Those are the intercropping trials the literature is missing.

GET /api/v1/interactions/guilds
GET /api/v1/interactions/guilds?mode=loose&exclude_negative=false
GET /api/v1/interactions/guilds?crop=pawpaw

#GET|POST /api/v1/systems/evaluate

Evaluates a crop system, meaning several crops grown together or in sequence, on a farm, following FAO Soils Bulletin 52 §6.3.5 and §8.4.3.

Parameter Meaning
crops 2–8 crop slugs (required)
arrangement optional: row_intercrop, strip_intercrop, mixed_intercrop, relay, alley, understory, multistrata, rotation, double
farm conditions exactly as /api/v1/recommend (region, zone, gdd, frost_free_days, annual_precip_in, soil_texture, drainage, soil_ph, slope_pct, air_drainage, sun_exposure, …)
profile evaluation profile, as for /recommend
share.<slug> optional land share of each crop (normalised to 1; default equal shares)
gross.<slug>, cost.<slug> optional: the farm's own gross revenue and cost per acre for that crop grown alone (default: the catalog's cited Lake Erie figures)
wage, discount_rate, horizon_years optional economics overrides (defaults $16.00/h, 0.05, the longest perennial lifespan or 20 years, between 5 and 40)

The response holds:

  • components: each crop's score, tier and FAO class on this farm.
  • system:
    • class_first_approximation: the least suitable crop's class, as SB52 §8.4.3 prescribes.
    • class: that class after interaction adjustments.
    • limiting: which crops and land qualities set the class.
  • interactions: every documented interaction among the crops.
    • Each one carries the land quality it changes (land_quality), whose land it changes (affected), and the direction (improves, worsens or no_change).
    • relation says whether the crops grow together or in sequence.
    • undocumented_pairs lists the pairs with no evidence either way.
    • Only an adjustment the evidence supports numerically is applied: a juglone-type allelopathy makes the sensitive crop not suitable. Other interactions state a direction but not a severity, so they are reported without changing a class.
  • ler: the measured land equivalent ratio.
    • measured: the median of published trials that grew exactly these crops, with each trial listed.
    • subset: only trials of some of the crops exist. They are listed but not extrapolated.
    • none: no trial exists. used_in_economics is then 1.0, meaning no land advantage is assumed.
  • economics_defaults: a 5% discount rate, and the New York minimum wage outside New York City, Long Island and Westchester ($16.00 an hour, per the NY Department of Labor).
  • economics: per-acre economics of the system against the best of its crops grown alone.
    • per_acre.revenue_mix = Σ partial LER × each crop's sole gross revenue; cost_mix = Σ land share × each crop's sole cost; net_mix, best_sole and advantage_vs_best_sole compare the two. monetary_advantage_index (Willey 1979) is returned when the LER is measured.
    • Each crop's partial LER is the trials' median partial LER, else the measured total LER split by land share, else its land share (LER 1.0, no advantage assumed). components[].partial_ler_basis says which.
    • Cost is the published budget's own gross minus net, else its operating cost, else its input cost. Labour is shown in hours and at the wage but is not subtracted, because the budgets differ on whether they already include it.
    • npv (when a crop is perennial): establishment cost × share in year 0, no revenue from a perennial until its years to full production, discounted over the horizon, against the best sole crop's NPV.
    • complete is false and missing names the crops without cited gross and cost figures; send gross.<slug> and cost.<slug> to fill them. assumptions lists every assumption in words.
  • land_use_type: the matching catalog or literature land use type, when one exists.
GET /api/v1/systems/evaluate?crops=field-corn-dent,dry-bean,pumpkin&region=lake_erie&soil_texture=silt_loam&drainage=moderately_well_drained&soil_ph=6.4
GET /api/v1/systems/evaluate?crops=black-walnut,tomato,pawpaw&region=lake_erie&arrangement=alley

The first returns S2 (limited by drainage), with a measured LER from Iowa State's Three Sisters trial. In the second, tomato becomes N because black walnut's juglone suppresses it, while pawpaw, which tolerates juglone, stays S1.


#GET /api/v1/crops/:slug/fit

One crop evaluated by the matching engine against every region preset, so a caller can answer "where in the study area does this crop fit, and what limits it?" without ranking the whole catalog. Any other /recommend farm parameter (soil_texture, drainage, soil_ph, air_drainage…) may be passed and is merged into every preset. Each entry carries the preset's conditions, fit, score, gates_failed, coverage and the per-criterion verdicts.

GET /api/v1/crops/peach/fit
GET /api/v1/crops/peach/fit?soil_texture=clay_loam&drainage=somewhat_poorly_drained

#GET /api/v1/crops/:slug/cultivars

Named cultivars of a crop. A non-null attribute (hardiness zone, GDD, chill, days to maturity, disease resistances) overrides the species value for that cultivar; null means "same as the species". sources maps each set attribute to the quote that states it, and sources._row documents the cultivar itself.

GET /api/v1/crops/table-grape/cultivars

#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 at the edge for 1 hour 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": "6a",
        "lat": 42.35,
        "lng": -79.32
      }
    ],
    "total": 2
  }
}

#GET /api/v1/regions/presets

The climate presets that /api/v1/recommend accepts as region. Includes presets that are not in the site's location picker, such as chautauqua_uplands. Served from origin.

#Query parameters

None.

#Response

{
  "data": [
    {
      "name": "lake_erie",
      "display_name": "Lake Erie Concord Grape Belt",
      "description": "The grape-growing region along Lake Erie spanning Chautauqua County NY and Erie County PA. …",
      "states": ["NY", "PA"],
      "zones": ["6a", "6b", "7a"],
      "gdd": 2923,
      "frost_free_days": 185,
      "annual_precip_in": 41.09,
      "ui_region": true,
      "latitude": 42.35,
      "longitude": -79.32
    }
  ]
}

ui_region is true for presets that also appear in the site's location picker.


#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": 81, "total_pages": 5 }
}

#GET /api/v1/crops/:slug

Full dossier for a single crop. The public crop detail page reads the same tables.

#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
├── cultivars[]            ← named cultivars
└── interactions[]         ← this crop's interactions with other crops

crop, economics and each child row also carry a sources object keyed by column (_row for the whole row): publisher, url, quote, role, confidence, note and date accessed.

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": 48 }
}
  • 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 academic, university_extension, government, industry, extension, industry_association, peer_reviewed
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": 251
  }
}

#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": 8021, "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.

Note: the snowflake is a regional score — one value per crop per region. For a specific farm, use /api/v1/recommend, which evaluates the same kind of criteria against the farm's own conditions and reports per-criterion evidence. The snowflake remains for the crops.every.farm UI and is no longer being expanded to new regions; the UI will move to the matching engine with region presets.


#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-10

  • /api/v1/recommend (GET and POST). Farm-conditions matching: send zone, GDD, frost-free days, chill, rainfall, soil texture, drainage, pH, irrigation, cold storage, labor, capital and horizon; get ranked crops with a per-criterion verdict, a reason in grower language, and whether the crop attribute behind each verdict is cited. unknown is reported as unknown, never scored as unfit. region= works as a preset you can override per field. Described under recommend in /api/v1/meta.
  • Second region: finger_lakes. Colder and shorter-season than Lake Erie. First set on 1988 Federal Register AVA baselines; rebased on 2026-10-23 to NOAA 1991–2020 normals (2,418 GDD base 50 °F, 172 frost-free days, 30.2 in, zones 6a–6b). available_regions in /meta is now read from the regions table.
  • Snowflake rescored and re-cited. 1,170 citations that pointed at crops.every.farm itself were replaced with citations to the primary sources of each score's inputs; three checks that scored on row counts (so thorough research lowered a score) now read severity, specialisation and viability columns.
  • USDA GRIN anchors: grin_taxon_id on crops (GRIN Taxonomy nomen number, cited) and grin_accession / grin_accession_id / grin_taxon_id on cultivars (NPGS accession, cited). Returned by /crops/:slug and /crops/:slug/cultivars. Prototype covers apple, table and juice grape, and both cherries.
  • Per-acre system economics (2026-10-26): /api/v1/systems/evaluate returns economics: revenue, cost and net per acre of the mix against the best sole crop, the monetary advantage index, and an NPV for systems with perennials, each crop's figures cited from the catalog or sent by the farm (share., gross., cost., wage, discount_rate, horizon_years). The public /interactions page is now a full-screen map with guild, combination and trial panels, and evaluates any combination on a farm.
  • FAO land evaluation (2026-10-23): /recommend results carry an fao class (S1–N) with the limiting land qualities, and /api/v1/systems/evaluate evaluates guilds, intercrops, alleys and rotations. Behind them: 27 FAO land qualities, about 1,540 cited graded crop requirements, 158 land use types, and 212 published intercrop trials with their land equivalent ratios.
  • More interactions and the traits that let crops share land (2026-10-22): about 1,470 cited interactions, now including measured water and nitrogen sharing in intercrops, vineyard-floor and young-vineyard intercrops for the Lake Erie Grape Belt, nut-tree alley trials with yield by distance from the tree row, and the first grow-together partners for Brussels sprouts, turnip, table beet, quinoa, kale and cauliflower. /crops/:slug now returns rooting depth for 55 crops (was 8), mature canopy height for 71 (was 17) and wind tolerance for 37 (was 7), each cited (mostly FAO Irrigation and Drainage Paper 56). Each new interaction states the site conditions it was observed under. The guild finder has four five-crop guilds in strict mode.
  • Positive intercropping evidence: interactions grew to about 1,230 cited rows (field intercrops, living mulches and wind strips, orchard and vineyard alleys, trap crops, pollinator partners), and conflicting findings are recorded as contradicting sources on the row they dispute. Mutual-benefit guilds of four crops now exist (the largest loose guild has five). /interactions?all=true and /crops/:slug page through the table, so every row and every source is returned; before 2026-10-14 the list stopped at 1,000 rows.
  • Guild finder (/interactions page and GET /api/v1/interactions/guilds): mutual-benefit crop clusters, a crop's best partners, rotation chains, and the undocumented pairs whose trials would complete a guild.
  • Interactions grew from 184 to about 925 cited rows (rotation conflicts and benefits, shared pests and diseases across vegetables, field crops, fruit and nuts, nitrogen credits, cover-crop allelopathy, alley and understory pairings, trap crops and intercrop competition). /recommend now scores a neighbours criterion from them when already_growing is sent: an allelopathic suppressor among the farm's crops makes a candidate unfit, other documented concerns marginal, documented benefits fit; undocumented pairs are not counted against a crop. The public /interactions page gained a combination checker (/interactions?crops=a,b,c).
  • GET /api/v1/crops/:slug/fit: one crop against every region preset, shown on crop pages as "Fit by region".
  • Public UI for the matcher and the interactions: /match (farm-conditions form, ranked results with per-criterion verdicts and a crop × criterion matrix; conditions in the URL) and /interactions (network of every cited interaction). Crop pages gained a climate-envelope and site block, a Cultivars section and a Grows With / Avoid Near section. GET /api/v1/regions/presets lists every recommend preset with its cited climate baselines; /api/v1/regions now carries them too under climate.
  • /api/v1/interactions and /api/v1/crops/:slug/cultivars: 176 cited crop interactions (black walnut juglone sensitivity and tolerance; pests shared across grapes, berries and tree fruit such as spotted wing drosophila, Japanese beetle, plum curculio, clubroot and spotted lanternfly; rotation benefits and conflicts; alley and understory pairings) and 123 cited cultivars across apple, aronia, blueberry, chestnut, elderberry, hazelnut, honeyberry, pawpaw, raspberry, strawberry, sweet and tart cherry, table grape and juice grape.
  • New crop: juice-grape (Concord / Niagara), the anchor crop of the Lake Erie Grape Belt, with economics from the Cornell Concord budget, soils, drainage, risks, equipment, market channels, storage and five cultivars, all cited.
  • exclude_crops / already_growing fixed for multi-word slugs (they were never excluded), and each candidate now lists with_farm_crops: documented interactions with the crops the farm already grows.
  • Site criteria in /recommend: air_drainage / frost_pocket (spring frost at bloom for perennials) and sun_exposure (alley and understory positions). A crop with any unfit criterion is now capped at possible.
  • Contested evidence: a citation that contradicts a stored value no longer counts toward cited; the criterion carries contested: true instead.
  • Region presets cited to NOAA 1991–2020 normals and the 2023 USDA hardiness map. lake_erie is now zones 6a–7a with 2,923 GDD and 185 frost-free days (previously the 2012 map's 5b–6b and an uncited 2,500 / 180). New preset chautauqua_uplands covers the Allegheny Plateau south of the escarpment.
  • New schema for site and polyculture data: crops.max_slope_pct, frost_sensitivity_bloom, prefers_air_drainage, shade_tolerance, canopy_height_ft, root_depth_in, wind_exposure_tolerance; cultivars; crop_interactions. All nullable; /recommend reports unsupported_conditions until they are researched. See Data Model & Schema.

#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

  • Cultivar-level matching: /recommend evaluating cultivars rows as overrides of the species envelope.
  • Companion / guild / alley recommendations from crop_interactions.
  • The crops.every.farm UI moving from the regional snowflake to /recommend with region presets.
  • Additional regions as presets (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.