Developer

BRD Groundtruth API

A read-only REST API for the farm data already in your BRD Groundtruth account — fields, weather, irrigation scheduling, soil health, yield history, equipment, and market prices. Script against your own farm.

Introduction

Every endpoint below is real and live — it's the same data and the same logic your BRD dashboard uses, exposed as JSON. This is v1.0, read-only: nothing in this API creates or modifies records in your account. Write access (logging a spray application, adding a harvest entry, etc.) may follow in a future version.

Prefer not to write HTTP calls by hand? See the official Python client library below, or explore every endpoint interactively in Swagger UI.

Authentication

Authenticate with a personal API key, generated at crops.bishopresearch.com → Dashboard → Settings → API Keys. Pass it as a bearer token on every request:

Authorization: Bearer brd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The plaintext key is shown exactly once, at creation time. BRD stores only a one-way hash of it — if you lose a key, revoke it and generate a new one. Keys are scoped to your account only: they can only read data you already own.

Base URL & versioning

https://crops.bishopresearch.com/api/v1

Every path documented below (e.g. /fields) is relative to this base URL. The v1 in the path is the version — breaking changes will ship as a new version prefix rather than changing v1's behavior underneath you.

Rate limits

60 requests per minute, per API key. Exceeding it returns HTTP 429:

{"detail": "Rate limit exceeded: 60 requests/minute per API key."}

The Python library retries 429 and 5xx responses automatically with exponential backoff before raising.

Errors

Errors are a JSON body with a single detail field describing what went wrong, and a standard HTTP status code:

StatusMeaning
401Missing, invalid, or revoked API key
404Resource not found (or not yours)
422Request understood but rejected — e.g. a field with no location set
429Rate limit exceeded
5xxSomething went wrong on BRD's end
{"detail": "Field not found"}

Account

GET/me

The account this API key belongs to.

curl https://crops.bishopresearch.com/api/v1/me \
  -H "Authorization: Bearer $BRD_API_KEY"
client.me()

Response — real example, captured live:

{
  "email": "testfarmer999@example.com",
  "full_name": "Test Farmer",
  "subscription_tier": "seedling",
  "created_at": "2026-03-15T21:20:47.707494Z"
}

Fields

GET/fields

List every field on your account.

curl https://crops.bishopresearch.com/api/v1/fields \
  -H "Authorization: Bearer $BRD_API_KEY"
client.fields.list()

Response — real example, captured live:

{
  "fields": [
    {
      "id": 1,
      "name": "North 80",
      "acres": 80.0,
      "crop": "Corn",
      "soil_type": "Silt loam",
      "lat": 41.5868,
      "lon": -93.625,
      "boundary": null,
      "notes": "",
      "agro_polygon_id": null,
      "tillage_practice": "conventional",
      "planting_date": null,
      "created_at": "2026-03-15T21:23:06.463595Z",
      "observation_count": 0
    }
  ]
}

GET/fields/{field_id}

Get a single field by ID (must belong to your account, or this returns 404).

curl https://crops.bishopresearch.com/api/v1/fields/1 \
  -H "Authorization: Bearer $BRD_API_KEY"
client.fields.get(1)

Response — example (synthetic, for illustration):

{
  "id": 1,
  "name": "North 80",
  "acres": 80.0,
  "crop": "Corn",
  "soil_type": "Silt loam",
  "lat": 41.5868,
  "lon": -93.625,
  "boundary": null,
  "notes": "Tile-drained, planted no-till since 2023.",
  "agro_polygon_id": null,
  "tillage_practice": "no-till",
  "planting_date": "2026-04-28",
  "created_at": "2026-03-15T21:23:06.463595Z",
  "observation_count": 3
}

Weather

GET/fields/{field_id}/weather

The current Open-Meteo forecast for a field's location. Returns 422 if the field has no lat/lon set.

curl https://crops.bishopresearch.com/api/v1/fields/1/weather \
  -H "Authorization: Bearer $BRD_API_KEY"
client.fields.weather(1)

Response — real example, captured live (forecast truncated to 1 of 7 days):

{
  "field_id": 1,
  "field_name": "North 80",
  "forecast": [
    {
      "date": "2026-08-16",
      "high": 78.3,
      "low": 69.4,
      "precip": 0.0,
      "code": 3,
      "et0": 0.11,
      "wind_mph": 13.4
    }
  ],
  "cumulative_gdd": 2780.2,
  "season_precip_in": 32.62,
  "next7_total_precip": 0.79,
  "frost_risk_7d": false,
  "heavy_rain_7d": false,
  "current_high": 78.3,
  "current_low": 69.4
}

Irrigation

GET/fields/{field_id}/irrigation

FAO-56 irrigation schedule for a field, built from real ET0/precipitation forecasts and a crop Kc curve.

ParamTypeDefaultNotes
daysint7Clamped to 1–14
curl "https://crops.bishopresearch.com/api/v1/fields/1/irrigation?days=3" \
  -H "Authorization: Bearer $BRD_API_KEY"
client.fields.irrigation(1, days=3)

Response — real example, captured live:

{
  "field_id": 1,
  "field_name": "North 80",
  "crop": "Corn",
  "kc_coefficient": 0.35,
  "gdd_accumulated": 2780.0,
  "gdd_since_planting": false,
  "schedule": [
    {
      "date": "2026-08-16",
      "et0_in": 0.11,
      "etc_in": 0.038,
      "precip_in": 0.0,
      "daily_deficit_in": 0.038,
      "cumulative_deficit_in": 0.038,
      "irrigate": false
    }
  ],
  "total_et0_in": 0.38,
  "total_etc_in": 0.13,
  "total_precip_in": 0.55,
  "days": 3
}

Soil Health

GET/soil-health

Cross-field soil health summary, built from your own recorded lab tests — latest test per field, a rating, deficit-based lime/P/K planning estimates, and farm-wide averages.

curl https://crops.bishopresearch.com/api/v1/soil-health \
  -H "Authorization: Bearer $BRD_API_KEY"
client.soil_health.summary()

Response — example (synthetic, for illustration — the live account this doc was captured against had no soil test on file yet, so this shows what a field with a recorded lab test looks like):

{
  "fields": [
    {
      "field_id": 1,
      "field_name": "North 80",
      "acres": 80.0,
      "latest_test": {
        "sample_date": "2026-04-02",
        "ph": 6.2,
        "om_pct": 3.4,
        "cec": 18.0,
        "p_ppm": 28.0,
        "k_ppm": 165.0,
        "zn_ppm": 1.8,
        "lab": "A&L Great Lakes Laboratories",
        "notes": null
      },
      "rating": "Fair"
    }
  ],
  "recommendations": [
    {"field_id": 1, "field_name": "North 80", "lime_tons_ac": 0.5, "p_lbs_ac": 24, "k_lbs_ac": 21, "cost_per_ac": 45.15}
  ],
  "aggregates": {"avg_ph": 6.2, "avg_om": 3.4, "avg_p": 28.0, "avg_k": 165.0},
  "note": "Lime/P/K estimates are deficit-based planning figures from your latest soil test, not a calibrated lab buffer-pH recommendation. Confirm with your agronomist or lab before applying."
}

Yield History

GET/yield-history

Yield history built from your own recorded harvest entries — one point per field per season you've entered. No county/state averages are fabricated when you have no data yet.

curl https://crops.bishopresearch.com/api/v1/yield-history \
  -H "Authorization: Bearer $BRD_API_KEY"
client.yield_history.list()

Response — example (synthetic, for illustration — the live account this doc was captured against had no harvest entries yet, so this shows a field with multiple seasons on record):

{
  "fields": [
    {
      "field_id": 1,
      "field_name": "North 80",
      "acres": 80.0,
      "crop": "Corn",
      "years": [
        {"year": 2024, "yield_bu_ac": 198.4, "crop": "Corn"},
        {"year": 2025, "yield_bu_ac": 205.1, "crop": "Corn"}
      ]
    }
  ],
  "note": "Built from your entered harvest records (Harvest Log). Add entries there for fields with no history shown here."
}

The Python library's client.yield_history.list() returns the fields array directly.

Equipment

GET/equipment

Your logged equipment (a manually-maintained fleet inventory — name, year, make/model, hours, last service date, status).

curl https://crops.bishopresearch.com/api/v1/equipment \
  -H "Authorization: Bearer $BRD_API_KEY"
client.equipment.list()

Response — example (synthetic, for illustration — the live account this doc was captured against had no equipment logged yet):

{
  "equipment": [
    {
      "id": 1,
      "name": "Deere 8320R",
      "type": "Tractor",
      "year": 2019,
      "make": "John Deere",
      "model": "8320R",
      "total_hours": 2140.5,
      "last_service_date": "2026-05-12",
      "status": "Active"
    },
    {
      "id": 2,
      "name": "Patriot Sprayer",
      "type": "Sprayer",
      "year": 2021,
      "make": "Case IH",
      "model": "Patriot 4440",
      "total_hours": 615.0,
      "last_service_date": "2026-06-01",
      "status": "Active"
    },
    {
      "id": 3,
      "name": "Kinze Planter",
      "type": "Planter",
      "year": 2017,
      "make": "Kinze",
      "model": "3660",
      "total_hours": 890.0,
      "last_service_date": "2025-10-20",
      "status": "Maintenance"
    }
  ]
}

Market Prices

GET/market/prices

Corn/soybean/wheat price estimates plus real day-over-day change (not field-specific). live: false means the server doesn't have a USDA AMS key configured and prices are estimates — check data_source.

curl https://crops.bishopresearch.com/api/v1/market/prices \
  -H "Authorization: Bearer $BRD_API_KEY"
client.market.prices()

Response — real example, captured live:

{
  "corn": {"futures": 4.6, "change": 0.0, "change_pct": 0.0},
  "soybeans": {"futures": 11.1, "change": 0.0, "change_pct": 0.0},
  "wheat": {"futures": 6.1, "change": 0.0, "change_pct": 0.0},
  "data_source": "Estimated (USDA_MARS_API_KEY not configured)",
  "live": false,
  "updated_at": "2026-08-16T21:22:59.701473"
}

Python Library

The official groundtruth_ag package wraps every endpoint above in a resource-oriented client, with typed exceptions and automatic retry on rate limits / transient server errors.

Install

pip install groundtruth-ag

Quickstart

# Generate a key at crops.bishopresearch.com → Dashboard → Settings → API Keys
from groundtruth_ag import Client

client = Client(api_key="brd_live_...")
# or: export BRD_API_KEY=brd_live_... and just call Client()

for field in client.fields.list():
    schedule = client.fields.irrigation(field["id"], days=7)
    print(field["name"], schedule["schedule"])

Resource methods

CallEndpoint
client.me()GET /me
client.fields.list()GET /fields
client.fields.get(id)GET /fields/{id}
client.fields.weather(id)GET /fields/{id}/weather
client.fields.irrigation(id, days=7)GET /fields/{id}/irrigation
client.soil_health.summary()GET /soil-health
client.yield_history.list()GET /yield-history
client.equipment.list()GET /equipment
client.market.prices()GET /market/prices

Errors

from groundtruth_ag import Client, NotFoundError, RateLimitError, AuthenticationError

client = Client()
try:
    client.fields.get(999999)
except NotFoundError:
    print("no such field")
ExceptionHTTP status
AuthenticationError401
NotFoundError404
ValidationError422
RateLimitError429 (after retries exhausted)
ServerError5xx (after retries exhausted)
ConnectionErrorno response received (network/timeout)

Context manager

with Client(api_key="brd_live_...") as client:
    print(client.me())
# connection closed automatically
Source, tests, and the full README: the groundtruth_ag package ships alongside this API — built on httpx, sync-only for now with an async client a natural (not yet built) follow-on. No pagination is implemented: every v1 resource returns a small, complete list scoped to your own farm.