Versioning
Breaking changes get a new path prefix (/v2, /v3). Non-breaking additions land in the current version. Old versions stay live for ≥6 months after a successor ships, with a Sunset response header advertising the cutoff.
OpenAPI spec
Live spec at https://api.mud2dust.io/v1/openapi.json. An interactive explorer is queued for P1.7 iter 4; for now you can paste that URL into Swagger UI / Stoplight Elements / Scalar to render it.
Object types
Seven first-class shapes share auth, trust scoring, and privacy controls:
| Shape | Covers | Endpoint(s) |
|---|---|---|
| Observation | time-series point readings | POST /v1/observations |
| Profile | multi-depth time-series (drill-and-drop probes, lysimeters, profile sensors) | POST /v1/profiles |
| Sample | discrete lab results (gravimetric VWC, texture, OM, …) | POST /v1/samples |
| Event | single drone / aircraft / PhenoCam capture | POST /v1/uploads/initiate → complete |
| Collection | multi-flight drone survey, multi-pass aircraft campaign | POST /v1/collections |
| Annotation | geotagged agronomist note, citizen photo | POST /v1/annotations |
| Boundary | field / management zone / EC-mapped zone | POST /v1/boundaries |
Auth
User accounts are live: magic-link sign-in, optional email + password, 15-min HS256 JWT in an HttpOnly; Secure; SameSite=None cookie, 30-day rotated refresh token, ORCID linking for the researcher tier. Full reference at /auth. Partner-app OAuth 2.0 + PKCE with per-shape scopes (stations:write, observations:write, …) lands in P4.23. The legacy X-Contributor-Id header is still accepted on the station/observation endpoints as a fallback for partner-app integrations not yet on OAuth.
/v1/point — point or polygon query
The flagship read endpoint. Returns every primitive raster signal mud2dust has at a point — or averaged across a polygon — plus four opinionated derived blocks (irrigation, stickiness, traction, n_stress_proxy) and a coverage report. Three modes on one URL:
| Mode | Parameter(s) | Sampling |
|---|---|---|
| point | ?lat=&lon= | 1 titiler sample per layer |
| polygon | ?geom= URL-encoded GeoJSON | 5×5 grid in bbox + point-in-polygon filter |
| bbox | ?bbox=min_lon,min_lat,max_lon,max_lat | shorthand — converted to a 4-vertex Polygon internally |
The polygon parameter is GeoJSON
?geom= is the canonical way to ask for an averaged-over-area answer. Apps that send field boundaries, watersheds, utility service territories, drone-flown areas, or any other AOI should use this. The value is a GeoJSON Polygon or MultiPolygon object — JSON-stringified, then URL-encoded. Coordinates are [longitude, latitude] per RFC 7946 (note: lon first). Inner rings (holes) are accepted but currently treated as exterior; full hole support lands when contributors start uploading polygons with them.
Example GeoJSON Polygon for a field near Mesa, WA:
{
"type": "Polygon",
"coordinates": [[
[-119.260, 46.480],
[-119.245, 46.480],
[-119.245, 46.495],
[-119.260, 46.495],
[-119.260, 46.480]
]]
}JSON-stringify, URL-encode, and append:
# Pseudo-shell — actual encoding done by your HTTP lib
GEOM='{"type":"Polygon","coordinates":[[[-119.26,46.48],[-119.245,46.48],[-119.245,46.495],[-119.26,46.495],[-119.26,46.48]]]}'
curl "https://api.mud2dust.io/v1/point?geom=$(printf %s "$GEOM" | jq -sRr @uri)&use=apple_orchard"MultiPolygon works the same way; for very large AOIs (over ~8 KB after URL-encoding) a forthcoming POST /v1/point will accept the geometry in the body.
Sample aggregation rules (polygon mode)
- Continuous fields (VWC, σ⁰, NDVI/NDWI/NDRE, LST, ET, clay%, sand%, SOC, pH, DEM, LIA, precip): mean of in-polygon points.
- Categorical (CDL crop class): mode (most common rounded code).
- Soil hydraulics, forecast, irrigation, traction: derived from the aggregated inputs above.
Response shape
Same JSON shape for point and polygon mode. Polygon mode adds a top-level polygon block with rings + bbox + area + sample count + source (so callers know whether the polygon came from ?geom= or ?bbox=). Full schema in the OpenAPI spec at https://api.mud2dust.io/v1/openapi.json under PointQueryResponse.
The top-level blocks:
signals— every primitive at the point/polygon (VWC, σ⁰, NDVI/NDWI/NDRE, LST, precip, ET, soil priors, CDL, etc.)context— crop class, soil texture, hydraulics, ET₀ with source attributioncoverage— geographic-coverage report (CONUS-only sources unavailable, global fallbacks used)now— current VWC + category + % of field capacityforecast— day-by-day VWC trajectory + days-to-targetirrigation— when / how much / how long; orholdif saturated. /explain/irrigation/ ↗ for the math.stickiness— soil-state band (firm / tacky / sticky / slick / saturated) + score + headline. /explain/traction/ ↗ for the math.traction— per-surface advice for 9 surfaces (shoes, boots, bicycle, motorbike, car, truck, orchard tractor, 200 hp tractor, combine), each with a verdict + recommended tread depth (mm) + note.n_stress_proxy— directional N-stress diagnosis (likely_n_stressed / water_stress_first / heat_stress_first / canopy_too_sparse / healthy / inconclusive) — NDRE-led with water + heat + sparse-canopy rule-outs. /explain/nitrogen/ ↗ for the math.notes— human-readable caveats (masked pixels, fallbacks used, etc.)
The map popover at mud2dust.com renders the irrigation, N-stress, and stickiness blocks as colored cards click-through to the corresponding explainer.
For worked recipes — irrigator, hiking, off-road, harvest, concrete-pour, energy/DR — see the examples page.
What's live today
# Queries (P4.x)
GET /v1/point?lat=&lon= point
GET /v1/point?geom=<urlencoded GeoJSON Polygon> polygon (canonical)
GET /v1/point?bbox=min_lon,min_lat,max_lon,max_lat polygon shorthand
# Auth (P3.6 — production)
POST /v1/auth/request-magic-link
POST /v1/auth/verify-magic-link
POST /v1/auth/sign-in-with-password
POST /v1/auth/refresh
POST /v1/auth/sign-out
# Self
GET /v1/me
PATCH /v1/me { username?, display_name? }
POST /v1/me/password { current_password?, new_password }
POST /v1/me/email/request-change { new_email }
POST /v1/me/email/confirm-change { token }
GET /v1/me/orcid/start (start ORCID OAuth)
POST /v1/me/orcid/link (callback → researcher-tier flip)
# Stations + observations (P1.5; auth-gated via JWT cookie or
# the legacy X-Contributor-Id header for partner-app integrations
# not yet on OAuth)
POST /v1/stations
body: { name, geom_internal: {type:"Point", coordinates:[lon,lat]},
vendor, observed_properties: [...], depths_cm?: [...], logger?: {...} }
→ 201 { id: "stn_…", geom_public, geom_public_mode, ... }
POST /v1/observations
body: [ { station_id, observed_property, value, unit, ts, depth_cm? }, ... ]
→ 202 { accepted, rejected, rejected_details: [{index, flags}] }
# Lab samples (P4.15a — auth-required)
POST /v1/samples
body: { geom: {type:"Point", coordinates:[lon,lat]},
sample_method: "lab_analysis"|"gravimetric"|"field_kit",
collected_at, analyzed_at?, sample_depth_cm?, lab_name?,
measurements: [{ analyte, value, unit }, ...] }
→ 201 { accepted, ids[], geom_public, geom_public_mode,
unknown_analytes? }
GET /v1/me/samples?limit=100
→ 200 { items: [{ id, analyte, value, unit, collected_at, lab_name,
lon, lat, ... }, ...] }
Known analytes: clay_pct, sand_pct, silt_pct, ph_h2o, ec_water,
soc, bulk_density, cec, nitrate_n, ammonium_n, total_n, p_olsen,
p_bray, k_extractable, gravimetric_vwc, foliar_n, foliar_p,
foliar_k. Unknown analytes are accepted but flagged.
Public-facing surfaces (other contributors' dashboards, public
STAC) see a 5 km jittered geom_public. The internal exact geom
drives the eventual soil-prior re-fit job (P4.15a iter 2).
# Lab-sample attachments (V.6 — auth-required, two-step upload)
POST /v1/samples/{sample_id}/attachments/initiate
body: { filename, content_type, size_bytes }
→ 200 { s3_key, upload_url, upload_method: "PUT",
upload_expires_in: 600,
upload_required_headers: { "Content-Type": "..." },
max_bytes: 26214400 }
PUT <upload_url> (browser sends file bytes direct to S3)
headers: include upload_required_headers as-is
body: file bytes
→ 200 (S3 ETag in response)
POST /v1/samples/{sample_id}/attachments/complete
body: { s3_key }
→ 200 { ok: true, sample_id, s3_key }
Allowed content types: application/pdf, text/csv,
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
(PDF/CSV/XLSX). Cap 25 MB/file. Multiple attachments per sample
permitted; addAttachmentKey is idempotent.
Storage: s3://mud2dust-contributions-dev/samples/{contributor}/
{sample_id}/{rand}-{filename}. Lifecycle: Standard 90d
→ IA, then 365d → Glacier IR.
Web form for contributors: https://mud2dust.com/contribute/sample/Trust model
Every contribution has a sensor_class (research / professional / consumer / diy) and an operator_class (researcher / agronomist_supported / farmer / hobbyist / unknown). The product of those (× installation quality) yields a training_weight. Contributions ≥ 0.5 are eligible to retrain the public calibration model; everyone else is a correction-taker with full feature parity but excluded from retrain. Users never see their tier as a number — only contribution health indicators.
Privacy / coordinate fuzzing
Every shape stores both geom_internal (exact, used for model training) and geom_public (jittered or aggregated). Default mode is 5km jitter; opt-in to 10km, county- aggregated, exact, or fully private (excluded from public surfaces). Jitter is deterministic per (contributor, external_id) so coords stay stable across requests.