Quickstart
This page takes you from an API key to NDVI statistics for a real field. You need an API key and a terminal with curl, Python 3 with requests, or Node.js 18 or newer.
-
Store your key in an environment variable.
Keep the key out of your code and out of version control.
export AGROSAT_API_KEY="ags_..." -
Check that the key works.
GET /v1/pingreturns the organization the key belongs to. It costs no units.curl https://api.agrosat.uz/v1/ping \-H "X-API-Key: $AGROSAT_API_KEY"import osimport requestsAPI = "https://api.agrosat.uz/v1"session = requests.Session()session.headers["X-API-Key"] = os.environ["AGROSAT_API_KEY"]r = session.get(f"{API}/ping", timeout=30)print(r.status_code, r.json())// Node.js 18+, saved as quickstart.mjsconst API = "https://api.agrosat.uz/v1";const headers = { "X-API-Key": process.env.AGROSAT_API_KEY };const res = await fetch(`${API}/ping`, { headers });console.log(res.status, await res.json());A live key answers
200:{ "success": true, "data": { "organization_id": "cmf1q2w3e0000ab12cd34ef56" } }A
401 invalid_api_keymeans the key is missing, mistyped, revoked or expired. -
Register a field.
Send the boundary as a GeoJSON
PolygonorMultiPolygonin WGS84, with your own identifier inexternal_id. This example is a 41 ha field in Syrdarya region.curl https://api.agrosat.uz/v1/fields \-H "X-API-Key: $AGROSAT_API_KEY" \-H "Content-Type: application/json" \-d '{"name": "North parcel 3","external_id": "f-1001","geometry": {"type": "Polygon","coordinates": [[[68.552, 40.3084], [68.560, 40.3084], [68.560, 40.3139], [68.552, 40.3139], [68.552, 40.3084]]]}}'field = {"name": "North parcel 3","external_id": "f-1001","geometry": {"type": "Polygon","coordinates": [[[68.552, 40.3084], [68.560, 40.3084], [68.560, 40.3139],[68.552, 40.3139], [68.552, 40.3084]]],},}r = session.post(f"{API}/fields", json=field, timeout=30)field_id = r.json()["data"]["id"]print(r.status_code, field_id)const field = {name: "North parcel 3",external_id: "f-1001",geometry: {type: "Polygon",coordinates: [[[68.552, 40.3084], [68.560, 40.3084], [68.560, 40.3139], [68.552, 40.3139], [68.552, 40.3084]]],},};const created = await fetch(`${API}/fields`, {method: "POST",headers: { ...headers, "Content-Type": "application/json" },body: JSON.stringify(field),});const fieldId = (await created.json()).data.id;console.log(created.status, fieldId);The response is
201with the new field. Itsstatusispendingwhile AgroSat prepares imagery and statistics:{"success": true,"data": {"id": "cmgz7k2qv0004l8p3a9d1x6rt","external_id": "f-1001","name": "North parcel 3","area_ha": 41.53,"status": "pending","created_at": "2026-04-28T09:14:03.512Z"}}Sending the same request again is safe: the same
external_idwith the same geometry returns the existing field with200. -
Request NDVI statistics.
Ask for a window of up to 366 days. While the field is still
pending, the API answers202with aRetry-Afterheader; wait that many seconds and repeat the request. A202costs nothing.curl -i "https://api.agrosat.uz/v1/fields/cmgz7k2qv0004l8p3a9d1x6rt/statistics?index=ndvi&from=2026-05-01&to=2026-05-31" \-H "X-API-Key: $AGROSAT_API_KEY"import timeparams = {"index": "ndvi", "from": "2026-05-01", "to": "2026-05-31"}while True:r = session.get(f"{API}/fields/{field_id}/statistics", params=params, timeout=60)if r.status_code != 202:breaktime.sleep(int(r.headers.get("Retry-After", "30")))body = r.json()if not body["success"]:raise RuntimeError(body["error"])print("units used:", r.headers["X-AgroSat-Units"])for row in body["data"]["data"]:print(row["date"], row["mean"], row["valid_fraction"])const url = new URL(`${API}/fields/${fieldId}/statistics`);url.search = new URLSearchParams({ index: "ndvi", from: "2026-05-01", to: "2026-05-31" });let res = await fetch(url, { headers });while (res.status === 202) {const wait = Number(res.headers.get("Retry-After") ?? 30);await new Promise((resolve) => setTimeout(resolve, wait * 1000));res = await fetch(url, { headers });}const body = await res.json();if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);console.log("units used:", res.headers.get("X-AgroSat-Units"));for (const row of body.data.data) console.log(row.date, row.mean, row.valid_fraction);Once the data is ready you get one entry per date with imagery, oldest first:
{"success": true,"data": {"field_id": "cmgz7k2qv0004l8p3a9d1x6rt","index": "ndvi","data": [{"date": "2026-05-04","mean": 0.5871, "median": 0.6012, "min": 0.1544, "max": 0.7785,"std": 0.0962, "p10": 0.4518, "p90": 0.6973,"valid_fraction": 0.9934, "cloud_fraction": 0.0066, "cloud_mask": "scl_cld"},{"date": "2026-05-09","mean": null, "median": null, "min": null, "max": null,"std": null, "p10": null, "p90": null,"valid_fraction": 0, "cloud_fraction": 1, "cloud_mask": "scl_cld"},{"date": "2026-05-14","mean": 0.6124, "median": 0.6347, "min": 0.1812, "max": 0.8021,"std": 0.0938, "p10": 0.4783, "p90": 0.7214,"valid_fraction": 0.9871, "cloud_fraction": 0.0129, "cloud_mask": "scl_cld"}],"unavailable_dates": []}}On 9 May the field was fully under cloud, so its values are
null. This response usedceil(41.53 × 3 / 10) = 13units, reported inX-AgroSat-Units.
