Developer API Documentation
Reference for the Every Farm Developer API, covering authentication with API tokens, rate limits, scopes, and every endpoint for farms, blocks, block headers, datasets, folders, collectors, datapoints, and GeoJSON.
Documentation for the Every Farm Developer API. This API allows 3rd-party developers to access farm and dataset data programmatically using API tokens.
#Overview
The Developer API provides authenticated, rate-limited access to farm data, datasets, and GeoJSON files. Developers generate API tokens through the application UI, then use those tokens to make HTTP requests from their own servers or scripts.
Base URL
https://api.every.farm#API Discovery
GET /v1 returns a self-describing JSON manifest of the entire API — every endpoint with a one-line summary and required scope, the authentication scheme, conventions (signed-URL TTLs, rate limits, timestamp shape), and a link back to this documentation. It requires no authentication and exposes no farm data.
This is the recommended first call for an AI agent: hand the agent a token and the base URL, and it can discover everything else from GET https://api.every.farm/v1.
#Authentication
#API Tokens
All data-access endpoints require a Bearer token in the Authorization header:
Authorization: Bearer efv_abc12345_...Tokens are generated from the API Tokens panel in the app (open your account menu → API Tokens). Each token:
- Has a prefix
efv_for easy identification - Is shown once at creation — it cannot be retrieved again
- Can be scoped to
read,write, or both - Can optionally be restricted to specific farms
- Can be set to expire after 30, 90, 180, or 365 days (or never)
- Maximum of 10 active tokens per user
#Token Management Endpoints
These three endpoints use a Firebase ID token (not an API token) for authentication. They are used by the application UI to manage tokens.
| Method | Endpoint | Description |
|---|---|---|
POST |
/v1/tokens |
Generate a new API token |
GET |
/v1/tokens |
List your tokens |
DELETE |
/v1/tokens/:token_id |
Revoke a token |
#Rate Limits
Each API token is subject to rate limiting:
| Limit | Default | Notes |
|---|---|---|
| Per minute | 60 requests | Best-effort, per Cloud Function instance |
| Per day | 10,000 requests | Firestore-backed, authoritative |
When rate-limited, the API returns a 429 Too Many Requests response:
{
"error": "Rate limit exceeded. Try again shortly.",
"retryAfterSeconds": 60
}You can check your current usage at any time via GET /v1/usage.
#Permissions & Scopes
#Token Scopes
| Scope | Access |
|---|---|
read |
List farms, datasets, blocks, folders; fetch GeoJSON data |
write |
Push new datasets; update, move, and trash datasets; create blocks; edit and delete datapoints; manage block headers and their values; create and organize folders |
#Farm Roles
The API respects the same role-based permissions as the application:
| Role | Read | Write |
|---|---|---|
| Owner | Yes | Yes |
| Administrator | Yes | Yes |
| Editor | Yes | Yes |
| Read-only | Yes | No |
| Permission-only | Yes | No |
If a token is restricted to specific farms, it can only access those farms regardless of the user's broader permissions.
#Endpoints
#List Farms
GET /v1/farms
Returns all farms the token owner has access to.
Response:
{
"farms": [
{
"id": "farm_abc123",
"name": "My Vineyard",
"role": "Owner",
"geolocation": { "latitude": 38.5, "longitude": -121.7 }
}
]
}#Get Farm Details
GET /v1/farms/:farm_id
Returns farm metadata including a signed URL for the blocks GeoJSON file (valid 15 minutes).
Response:
{
"id": "farm_abc123",
"name": "My Vineyard",
"role": "Owner",
"geolocation": { "latitude": 38.5, "longitude": -121.7 },
"blockFields": [],
"vizSettings": {},
"blocksGeoJsonURL": "https://storage.googleapis.com/..."
}#List Blocks
GET /v1/farms/:farm_id/blocks
Returns blocks (vineyard blocks, field boundaries, etc.) for a farm.
Query parameters:
| Param | Default | Description |
|---|---|---|
limit |
100 | Max blocks to return |
Response:
{
"blocks": [
{
"id": "block_xyz",
"name": "Block A",
"description": "",
"area": 2.5,
"footprint": { "type": "FeatureCollection", "features": [...] },
"properties": {}
}
]
}#Create Blocks
POST /v1/farms/:farm_id/blocks
Creates one or more blocks (polygon boundaries with optional field values). Requires the write scope and Owner/Editor/Administrator access to the farm. After creation the farm's blocks are automatically recompiled so the new blocks appear on the map and become available to analyses within a few seconds.
Body — single block:
{
"name": "Block A",
"description": "optional",
"geometry": { "type": "Polygon", "coordinates": [[[-122.1,38.5],[-122.0,38.5],[-122.0,38.6],[-122.1,38.6],[-122.1,38.5]]] },
"fields": { "type": "Variety", "variety": "Cabernet" }
}Body — multiple blocks: wrap an array under blocks (max 500 per request):
{ "blocks": [ { "name": "Block A", "geometry": { } }, { "name": "Block B", "geometry": { } } ] }| Field | Required | Description |
|---|---|---|
name |
yes | Block name |
geometry |
yes | Polygon or MultiPolygon — accepted as a raw geometry, a GeoJSON Feature, or a FeatureCollection. Geometry must be polygonal. |
description |
no | Free-text description |
fields |
no | Object of block-field values keyed by the field's machine name or label. Keys must match a field defined on the farm; unknown keys are rejected. Use these typed fields (e.g. a type Select with values Block/Variety) to drive the farm's block filters and tiering. |
Response (201):
{
"created": 2,
"blockIds": ["block_abc", "block_def"],
"farmId": "farm_xyz",
"compileRequestId": "compile_123",
"message": "2 block(s) created. The farm's blocks are recompiling..."
}Validation is all-or-nothing: if any block in the request is invalid, nothing is created and the response is 400 with a validationErrors array identifying each failing block by index.
{
"error": "One or more blocks failed validation. Nothing was created.",
"validationErrors": [
{ "index": 0, "error": "geometry must be a Polygon/MultiPolygon, Feature, or FeatureCollection." }
]
}#Block Headers
Typed, optionally time-aware attributes attached to blocks — e.g. Yield (bu/acre, by year), Leaf Ca (%, by year), Rootstock (Select), Year Planted (static). Use these for whole-block, non-spatial history (yield-by-year, leaf analyses, planting metadata) instead of duplicating geometry across GeoJSON datasets. Definitions are a per-farm schema registry; values are stored per block (and per period for temporal headers). Header types reuse the collector field-type enum (Text | Number | Select | Date and Time | Image).
#Create Block Header
POST /v1/farms/:farm_id/block-headers · scope write
Body:
{
"label": "Yield",
"type": "Number",
"unit": "bu/acre",
"category": "Production",
"timeDimension": "year",
"selectOptions": null,
"range": { "min": 0, "max": 3000 },
"description": "Estimated bushels per acre"
}| Field | Required | Description |
|---|---|---|
label |
yes | Unique per farm (case-insensitive) |
type |
yes | Text | Number | Select | Date and Time | Image |
timeDimension |
no | null (static, one value per block) | "year" | "season" |
selectOptions |
if Select | Non-empty array of allowed values |
unit / category / range / description |
no | Display + soft-validation. range violations warn, they don't reject. |
Returns 201 with the header definition (includes its id).
#List Block Headers
GET /v1/farms/:farm_id/block-headers · scope read — returns { "blockHeaders": [ … ] } (trashed excluded).
#Update Block Header
PATCH /v1/block-headers/:header_id · scope write — send any subset of the definition fields. Renaming re-checks uniqueness.
#Delete Block Header
DELETE /v1/block-headers/:header_id · scope write — soft-trash. Values already stored on blocks are retained but stop being surfaced on reads.
#Upsert Values (one block)
PUT /v1/blocks/:block_id/values · scope write
{ "values": [
{ "header": "Yield", "period": "2025", "value": 1005.3 },
{ "header": "Rootstock", "value": "M.9" }
] }header is a header id or label. period is required iff the header is temporal (YYYY for year, YYYY-Sxx for season) and must be omitted for static headers. Values are coerced to the header type. Returns per-row results + the block's updated values.
#Bulk Upsert Values (many blocks) — the spreadsheet path
POST /v1/farms/:farm_id/block-values:bulkUpsert · scope write
{
"blockKey": "name",
"createMissingHeaders": false,
"values": [
{ "block": "H22", "header": "Yield", "period": "2025", "value": 1005.3 },
{ "block": "H22", "header": "Yield", "period": "2024", "value": 1172 },
{ "block": "H22", "header": "Rootstock", "value": "M.9" }
]
}blockKey (name | id, default name) controls how each row's block resolves to a block on the farm (exact, trimmed, case-insensitive). createMissingHeaders: true auto-creates unknown headers (type inferred: Number if the value is numeric, else Text; year if a YYYY period is present). Every row gets a status so partial spreadsheets fail gracefully:
{
"blocksUpdated": 2,
"summary": { "applied": 3, "skipped": 1, "error": 1 },
"results": [
{ "index": 0, "block": "H22", "header": "Yield", "period": "2025", "status": "applied" },
{ "index": 3, "block": "H99", "status": "skipped", "reason": "no block named \"H99\" on this farm." }
]
}Compound labels (
H43-53,H1,2,3) and crosswalk aliasing are a later phase — today each row resolves to a single block by exact name/id, and unmatched rows come back asskipped.
#Read values on blocks
Header values are label-keyed and attach to block reads on request:
GET /v1/blocks/:block_id/values— all values for one block.GET /v1/blocks/:block_id?include=headerValues— a single block with values.GET /v1/farms/:farm_id/blocks?include=headerValues— all blocks with values.
Add ?period=2025 to collapse temporal headers to that period (static headers are omitted when filtering by period).
"headerValues": {
"Yield": { "2025": 1005.3, "2024": 1172, "2023": 1878 },
"Leaf Ca": { "2024": 1.31 },
"Rootstock": "M.9",
"Year Planted": 2014
}#List Datasets
GET /v1/farms/:farm_id/datasets
Returns datasets for a farm. Trashed datasets are excluded.
Query parameters:
| Param | Default | Description |
|---|---|---|
limit |
50 | Max datasets to return |
folder |
(all) | Filter by folder ID |
order |
updatedOn |
Sort field |
direction |
desc |
Sort direction (asc or desc) |
Response:
{
"datasets": [
{
"id": "dataset_123",
"name": "Yield 2025",
"description": "",
"dynamic": false,
"collectorId": null,
"headers": ["yield", "brix", "weight"],
"folderId": "root",
"createdOn": "2025-10-01T...",
"updatedOn": "2025-10-15T...",
"hasGeoJson": true,
"hasRaster": false
}
],
"count": 1
}collectorId is non-null on dynamic datasets — the link to a data collector you can push datapoints into. See Data Collectors & Datapoints below.
#List Folders
GET /v1/farms/:farm_id/folders
Query parameters:
| Param | Default | Description |
|---|---|---|
folder |
root |
Parent folder ID — lists one level of folders under it |
all |
false |
true returns every folder for the farm (flat), so you can map the whole tree in one request. Trash is excluded. |
Response:
{
"folders": [
{ "id": "folder_abc", "name": "2025 Season", "description": "", "folderId": "root" }
]
}Each folder's folderId is its parent (root, trash, or another folder id) — folders can be nested.
#Create Folder
POST /v1/farms/:farm_id/folders
Creates a folder. Requires the write scope and Owner/Editor/Administrator access.
Body:
{
"name": "2026 Season",
"description": "optional",
"parentFolderId": "folder_abc"
}| Field | Required | Description |
|---|---|---|
name |
yes | Folder name |
description |
no | Free-text description |
parentFolderId |
no | Parent folder id (default root). Must be an existing folder on the same farm. |
Response (201):
{ "id": "folder_def", "name": "2026 Season", "folderId": "folder_abc", "farmId": "farm_xyz" }To place a new dataset directly into a folder, pass that folder's id as folderId when creating the dataset (POST /v1/farms/:farm_id/datasets), or move an existing one with PATCH /v1/datasets/:dataset_id.
#Update / Move Folder
PATCH /v1/folders/:folder_id
Rename or re-describe a folder, and/or move it. Send any subset of the fields below.
| Field | Description |
|---|---|
name |
New name |
description |
New description |
folderId |
Move the folder: a folder id, root, or trash (all on the same farm). A folder cannot be moved into itself or one of its own subfolders. |
Example — move a folder into the trash:
{ "folderId": "trash" }Response (200):
{ "id": "folder_def", "name": "2026 Season", "folderId": "root", "message": "Folder updated." }#Delete Folder
DELETE /v1/folders/:folder_id
Hard-deletes a folder, but only when it is empty (no datasets and no subfolders). A non-empty folder returns 409; empty it first (move/trash its contents) or trash the folder itself with PATCH { "folderId": "trash" }. Cascade deletion (which also deletes dataset files) is intentionally not available over the API — use the web app for that.
Response (200):
{ "id": "folder_def", "message": "Folder deleted." }#Get Dataset Details
GET /v1/datasets/:dataset_id
Returns dataset metadata. When the dataset has GeoJSON or a raster (or both), short-lived signed URLs are included inline so the caller can fetch the actual data without a second round-trip.
Response:
{
"id": "dataset_123",
"name": "NAIP — 2024-09-12",
"description": "",
"dynamic": false,
"collectorId": null,
"headers": [],
"farmId": "farm_abc123",
"folderId": "root",
"createdOn": "2025-10-01T...",
"updatedOn": "2025-10-15T...",
"vizSettings": {},
"hasGeoJson": false,
"hasRaster": true,
"geoJsonURL": "https://storage.googleapis.com/...",
"rasterUrl": "https://storage.googleapis.com/.../data.tif?Signature=...",
"rasterPreviewUrl": "https://storage.googleapis.com/.../preview.png?Signature=...",
"tileUrlTemplate": "https://titiler-.../cog/tiles/WebMercatorQuad/{z}/{x}/{y}.png?url=<signed>",
"rasterUrlExpiresAt":"2026-05-09T16:00:00Z",
"bbox": [-78.47, 42.66, -78.46, 42.67]
}URL TTLs:
geoJsonURL— 15 min (small file, agents typically fetch immediately)rasterUrl/rasterPreviewUrl— 60 min (COG bytes are larger; allow time for slow downloads)tileUrlTemplate— valid untilrasterUrlExpiresAt; re-fetch the dataset to refresh
If you need a download rather than a URL, use the convenience endpoint below — it 302-redirects to a fresh signed URL, ideal for curl -L -o.
#Download Raster (302 redirect)
GET /v1/datasets/:dataset_id/raster
Scope: read
Issues a 302 Found redirect to a freshly-signed GeoTIFF download URL (60 min TTL). Convenience endpoint for curl -L, wget, rasterio.open() with a redirecting client, or any tool that downloads through Location: headers without needing JSON.
curl -L -H "Authorization: Bearer $TOKEN" \
https://api.every.farm/v1/datasets/$DATASET_ID/raster \
-o raster.tifReturns 404 when the dataset isn't backed by a tileable raster.
#Download Raster Preview
GET /v1/datasets/:dataset_id/raster/preview
Scope: read
302 redirect to the small PNG preview (when one exists). Useful for agents that want a fast thumbnail without pulling the full COG. Returns 404 if the dataset has no preview image.
#Get Dataset GeoJSON
GET /v1/datasets/:dataset_id/geojson
Returns the full GeoJSON data inline in the response body.
Response:
{
"datasetId": "dataset_123",
"datasetName": "Yield 2025",
"featureCount": 1250,
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "yield": 4.2, "brix": 24.1 },
"geometry": { "type": "Point", "coordinates": [-121.7, 38.5] }
}
]
}
}#Push a Dataset (Create)
POST /v1/farms/:farm_id/datasets
Scope required: write
Creates a new dataset in a farm by uploading GeoJSON data. The entire request is sent as a JSON body via Content-Type: application/json. Maximum body size is 10 MB.
Request body:
{
"name": "Yield 2026",
"description": "Optional description",
"folderId": "root",
"vizSettings": {},
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "yield": 4.2, "brix": 24.1 },
"geometry": { "type": "Point", "coordinates": [-121.7, 38.5] }
}
]
}
}Validation rules:
name— Required. String.geojson— Required. Must be a valid GeoJSON FeatureCollection withtypeandfeaturesproperties.geojson.features— Must be a non-empty array.description— Optional. String.folderId— Optional. Defaults to"root".vizSettings— Optional. Object for visualization configuration.
Headers are automatically extracted from the first feature's properties keys.
Response (201):
{
"id": "new_dataset_id",
"name": "Yield 2026",
"farmId": "farm_abc123",
"headers": ["yield", "brix"],
"featureCount": 1,
"folderId": "root",
"message": "Dataset created successfully."
}#Update Dataset GeoJSON
PUT /v1/datasets/:dataset_id/geojson
Scope required: write
Replaces the GeoJSON data of an existing dataset. Same body format and validation as the push endpoint, but only the geojson field is required.
Request body:
{
"geojson": {
"type": "FeatureCollection",
"features": [ ... ]
}
}Response (200):
{
"id": "dataset_123",
"name": "Yield 2025",
"headers": ["yield", "brix", "weight"],
"featureCount": 1300,
"message": "Dataset updated successfully."
}#Move a Dataset Between Folders
PATCH /v1/datasets/:dataset_id
Scope required: write
Moves a dataset into a different folder. Mirrors the app's Move action.
Request body:
{ "folderId": "folder_abc123" }folderId— Required. A folder id belonging to the dataset's farm, or the sentinels"root"(top level) or"trash".- Moving to
"root"also restores a trashed dataset. - Moving to
"trash"is equivalent to the Trash endpoint below. - For a dynamic dataset, the linked collector's
trashedflag is kept in sync (set when moving to trash, cleared otherwise).
Validation: the target folder must exist and belong to the same farm (404 / 400 otherwise).
Response (200):
{
"id": "dataset_123",
"folderId": "folder_abc123",
"message": "Dataset moved to folder folder_abc123."
}#Trash a Dataset
DELETE /v1/datasets/:dataset_id
Scope required: write
Soft-deletes a dataset by moving it into the trash folder (mirrors the app's Trash action). For a dynamic dataset, the linked collector is also flagged trashed. Recoverable — restore from the UI under Datasets → Trash, or move it back with PATCH /v1/datasets/:dataset_id and { "folderId": "root" }.
There is intentionally no hard-delete over the API. Use the app's "Destroy Permanently" action from the Trash folder when you really mean it.
Response (200):
{
"id": "dataset_123",
"trashed": true,
"collectorId": null,
"message": "Dataset trashed. Restore from the UI under Datasets → Trash..."
}If the dataset is already trashed, returns 200 with alreadyTrashed: true.
#Data Collectors & Datapoints
A data collector backs a dynamic dataset. Rather than uploading one static GeoJSON, a dynamic dataset is built from datapoints accumulated in its collector and then compiled into a fresh GeoJSON snapshot on demand. Sensors, mobile collectors, and AI agents are all natural callers for these endpoints.
Discovery flow for an agent:
GET /v1/farms/:farm_id/datasets— dynamic datasets carry acollectorIdfield.GET /v1/collectors/:collector_id— read the field schema.POST /v1/collectors/:collector_id/datapoints— push one or many datapoints.POST /v1/collectors/:collector_id/recompile— refresh the linked dataset.GET /v1/datasets/:dataset_id/geojson— fetch the freshly-compiled output.
Autonomous-agent flow (create + configure + populate from scratch):
POST /v1/farms/:farm_id/collectorswith{ name, fields }— creates collector + linked dynamic dataset in one call.- (Optional)
PATCH /v1/collectors/:collector_idto evolve the schema later. POST /v1/collectors/:collector_id/datapointsto push observations.POST /v1/collectors/:collector_id/recompileafter each batch.- (Optional)
DELETE /v1/collectors/:collector_idto soft-trash.
#List Collectors
GET /v1/farms/:farm_id/collectors
Scope: read
Lists the farm's collectors with their public field schemas. Trashed collectors are excluded.
Response:
{
"count": 2,
"collectors": [
{
"id": "col_abc123",
"name": "Soil samples 2026",
"description": "",
"farmId": "farm_xyz",
"datasetId": "ds_qrs456",
"fields": [
{ "label": "Brix", "machine_name": "brix", "type": "Number", "required": false, "options": [] },
{ "label": "Variety", "machine_name": "variety", "type": "Select", "required": true, "options": ["Cab","Merlot"] }
],
"reCompile": false,
"createdOn": "2026-04-01T...",
"updatedOn": "2026-05-08T..."
}
]
}#Create Collector
POST /v1/farms/:farm_id/collectors
Scope: write
Creates a collector AND the linked dynamic dataset in one atomic operation. Returns both ids so the caller can immediately push datapoints.
Request body:
{
"name": "Soil samples 2026",
"description": "Spring soil pH + moisture sweep across the south block",
"folderId": "root",
"geometryType": "Point",
"fields": [
{ "label": "pH", "type": "Number", "required": true, "min": 0, "max": 14, "step": 0.1 },
{ "label": "Moisture", "type": "Number", "required": false, "suffix": "%" },
{ "label": "Variety", "type": "Select", "options": ["Cab", "Merlot", "Riesling"] },
{ "label": "Notes", "type": "Text" }
]
}Field validation rules:
namerequired;fieldsmust be non-empty (1–50 fields).typemust be one of:Text,Number,Select,Date and Time,Image.Selectfields must include a non-emptyoptionsarray of non-empty strings.machine_nameis auto-generated fromlabel(snake_case, deduped with_Nsuffix on collision). Caller can pass an explicitmachine_name; reserved namedataPointIdis rewritten.geometryTypedefaults toPoint. Other valid values:LineString,Polygon.
Validation failure (400):
{
"error": "One or more fields failed validation. Nothing was created.",
"validationErrors": [
{ "index": 2, "errors": ["Select fields must include a non-empty options array"] }
]
}Response (201):
{
"collectorId": "col_new123",
"datasetId": "ds_new456",
"farmId": "farm_xyz",
"name": "Soil samples 2026",
"fields": [
{ "label": "pH", "machine_name": "p_h", "type": "Number", "required": true, "min": 0, "max": 14, "step": 0.1, ... }
],
"message": "Collector + linked dynamic dataset created. Push datapoints via POST /v1/collectors/col_new123/datapoints, then call /recompile."
}#Update Collector
PATCH /v1/collectors/:collector_id
Scope: write
Update any subset of name, description, geometryType, fields. Dataset metadata (name, description, headers) is kept in sync in the same batch.
Field-update semantics:
- Pass an existing field's
machine_nameto preserve its identity — datapoints keyed by that machine_name keep their values. - Omit
machine_nameto have a new field generated. - Omit a field from the array to remove it from the schema. Existing datapoint values for that key are not deleted (they just become unreferenced columns).
- To rename the display label only, keep the
machine_nameand change thelabel.
Request body (every field optional):
{
"name": "Soil samples 2026 (revised)",
"fields": [
{ "label": "pH", "machine_name": "p_h", "type": "Number", "required": true },
{ "label": "Moisture", "machine_name": "moisture", "type": "Number" },
{ "label": "Lab notes", "type": "Text" }
]
}A field-schema change automatically sets reCompile: true on the collector. The caller should follow up with POST /v1/collectors/:collector_id/recompile to refresh the dataset's GeoJSON.
Response (200):
{
"collectorId": "col_new123",
"datasetId": "ds_new456",
"updated": ["name", "fields", "reCompile"],
"message": "Collector updated. Schema changes mark the dataset for recompile — call POST /v1/collectors/col_new123/recompile to refresh the GeoJSON."
}#Delete (Trash) Collector
DELETE /v1/collectors/:collector_id
Scope: write
Soft-deletes the collector and its dataset (mirrors the app's Trash action). Recoverable via the UI under Datasets → Trash.
Response (200):
{
"collectorId": "col_new123",
"datasetId": "ds_new456",
"trashed": true,
"message": "Collector and linked dataset trashed. Restore from the UI under Datasets → Trash."
}There is intentionally no hard-delete endpoint over the API. Use the app's "Destroy Permanently" action from the Trash folder when you really mean it.
#Get Collector
GET /v1/collectors/:collector_id
Scope: read
Returns the collector's full schema plus dataPointCount. Use this before pushing datapoints to know what keys + types your payload must include.
Response:
{
"id": "col_abc123",
"name": "Soil samples 2026",
"description": "",
"farmId": "farm_xyz",
"datasetId": "ds_qrs456",
"fields": [
{ "label": "Brix", "machine_name": "brix", "type": "Number",
"required": false, "options": [],
"min": 0, "max": 30, "step": 0.1, "suffix": "°Bx" },
{ "label": "Notes", "machine_name": "notes", "type": "Text", "required": false, "options": [] }
],
"reCompile": false,
"dataPointCount": 142,
"createdOn": "2026-04-01T...",
"updatedOn": "2026-05-08T..."
}Field type values: Number, Text, Select, Date and Time. Select fields publish their options array; others may publish min/max/step/suffix if defined.
#List Datapoints
GET /v1/collectors/:collector_id/datapoints
Scope: read
Paginated list of datapoints in a collector, newest first.
Query params:
| Param | Default | Description |
|---|---|---|
limit |
50 | Max datapoints per page (max 200) |
start_after |
— | Datapoint id to resume from (use nextCursor from a previous response) |
resolve_images |
true |
When true, every Image-typed field includes signed download URLs. |
Response:
{
"collectorId": "col_abc",
"count": 50,
"nextCursor": "dp_zzz789",
"nextPageHint": "Pass ?start_after=dp_zzz789 to fetch the next page.",
"datapoints": [
{
"id": "dp_aaa111",
"lat": 42.66,
"lng": -77.46,
"accuracy": 0.5,
"createdOn": { ... },
"updatedOn": { ... },
"createdBy": "user_uid",
"source": "api",
"fields": {
"brix": 24.1,
"variety": "Cab",
"sample_photo": [{
"path": "datapointImages/...",
"large": "datapointImages/large/...",
"caption": "south block, row 3",
"url": "https://storage.googleapis.com/...&Signature=...",
"largeUrl": "https://storage.googleapis.com/...&Signature=..."
}]
}
}
]
}Image URLs are signed for 15 minutes — fetch them promptly. Setting resolve_images=false returns just the raw {path, large, caption} shape (use this when you only need metadata and want to skip the per-image sign cost).
#Get Datapoint
GET /v1/collectors/:collector_id/datapoints/:datapoint_id
Scope: read
Returns a single datapoint with the same shape as the list above. Same resolve_images query knob applies. Useful when you've seen a dataPointId in a compiled GeoJSON feature and want the original record (including any images).
#Edit Datapoint
PATCH /v1/collectors/:collector_id/datapoints/:datapoint_id
Scope: write
Partial update of a single datapoint. Supply any of lat, lng, accuracy, or any collector field by machine_name; omitted keys are left untouched. Values are coerced and validated the same way the push endpoint does. A required field can be changed but not blanked, and unknown keys are rejected (typo guard).
Geolocation note: lat and lng together define the point. If you send only one, the other is taken from the existing datapoint — so you can nudge a single coordinate without re-sending both.
After editing, call /recompile to refresh the linked dataset's GeoJSON.
Request body (every field optional):
{
"lat": 42.6620,
"brix": 23.9,
"variety": "Merlot"
}Response (200): the updated datapoint, same shape as Get Datapoint (image fields resolved unless ?resolve_images=false):
{
"collectorId": "col_abc123",
"datapoint": {
"id": "dp_aaa111",
"lat": 42.662,
"lng": -77.4651,
"accuracy": 0.5,
"createdOn": { ... },
"updatedOn": { ... },
"source": "api",
"fields": { "brix": 23.9, "variety": "Merlot" }
},
"message": "Datapoint updated. Call POST /v1/collectors/col_abc123/recompile to refresh the linked dataset's GeoJSON."
}Returns 404 if the datapoint doesn't exist, or 400 with validationErrors if a value is invalid or a required field would be cleared.
#Delete Datapoint
DELETE /v1/collectors/:collector_id/datapoints/:datapoint_id
Scope: write
Permanently removes a single datapoint from the collector. After deleting, call /recompile to refresh the linked dataset's GeoJSON.
Response (200):
{
"collectorId": "col_abc123",
"deleted": "dp_aaa111",
"message": "Datapoint deleted. Call POST /v1/collectors/col_abc123/recompile to refresh the linked dataset's GeoJSON."
}Returns 404 if the datapoint doesn't exist.
#Push Datapoints
POST /v1/collectors/:collector_id/datapoints
Scope: write
Create one or many datapoints in a single call. Capped at 500 datapoints per request (Firestore batch limit). Chunk larger imports across multiple calls.
Request body:
{
"datapoints": [
{
"lat": 42.6618,
"lng": -77.4651,
"accuracy": 0.5,
"brix": 24.1,
"variety": "Cab"
},
{
"lat": 42.6620,
"lng": -77.4648,
"brix": 23.8,
"variety": "Merlot"
}
]
}Single-datapoint convenience: if the body has top-level lat/lng and no datapoints array, it's treated as a single datapoint:
{ "lat": 42.66, "lng": -77.46, "brix": 24.1, "variety": "Cab" }Per-datapoint fields:
| Field | Required | Description |
|---|---|---|
lat |
Yes | Latitude in [-90, 90] |
lng |
Yes | Longitude in [-180, 180] |
accuracy |
No | Positive number; defaults to 1 |
<machine_name> |
Required if the collector field is required | Value for any header on the collector. Unknown keys are rejected (typo guard). |
Validation rules:
- Every datapoint must validate; if any fails, none are written (atomic batch).
- Unknown keys (not present in the collector's
fields) cause a 400 withvalidationErrorsdescribing which key and which datapoint index failed. - Number fields accept numeric strings (lenient coercion).
- Recompile is not automatic. Push datapoints, then call
/recompilewhen done batching.
Response (201):
{
"collectorId": "col_abc123",
"created": 2,
"datapointIds": ["dp_aaa111", "dp_bbb222"],
"message": "2 datapoint(s) created. Call POST /v1/collectors/col_abc123/recompile to refresh the linked dataset's GeoJSON."
}Validation failure (400):
{
"error": "One or more datapoints failed validation. No datapoints were written.",
"validationErrors": [
{ "index": 0, "errors": ["unknown field 'sugar' — collector has no header with that machine_name"] },
{ "index": 1, "errors": ["required field 'variety' is missing or empty"] }
]
}#Recompile Dataset
POST /v1/collectors/:collector_id/recompile
Scope: write
Triggers a recompile of the collector's linked dataset — every datapoint is read, a fresh GeoJSON is written to Cloud Storage, and the dataset's headers field is regenerated. Returns immediately with a compile-request id; the work runs asynchronously in a Cloud Function (typically a few seconds).
Response (202):
{
"collectorId": "col_abc123",
"datasetId": "ds_qrs456",
"compileRequestId": "cr_def789",
"status": "queued",
"pollPath": "compileRequests/cr_def789",
"message": "Compile queued. The dataset GeoJSON will be regenerated shortly; poll the request doc for completion or fetch /v1/datasets/:dataset_id/geojson after a few seconds."
}The simplest "is it done?" pattern is to wait a few seconds and refetch GET /v1/datasets/:dataset_id/geojson. Power users with Firestore access can subscribe to compileRequests/{compileRequestId}.complete === true.
#End-to-end agent workflow (curl)
A. Push to an existing collector
# 1. Find the dynamic dataset's collectorId
curl https://api.every.farm/v1/farms/$FARM/datasets \
-H "Authorization: Bearer $TOKEN" | jq '.datasets[] | select(.dynamic) | {id,name,collectorId}'
# 2. Read the collector schema
curl https://api.every.farm/v1/collectors/$COL -H "Authorization: Bearer $TOKEN"
# 3. Push datapoints
curl -X POST https://api.every.farm/v1/collectors/$COL/datapoints \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"datapoints":[{"lat":42.66,"lng":-77.46,"brix":24.1,"variety":"Cab"}]}'
# 4. Recompile
curl -X POST https://api.every.farm/v1/collectors/$COL/recompile -H "Authorization: Bearer $TOKEN"
# 5. Fetch the updated GeoJSON (wait a moment first)
sleep 3
curl https://api.every.farm/v1/datasets/$DATASET/geojson -H "Authorization: Bearer $TOKEN"B. Autonomous agent — create from scratch and start collecting
# 1. Create the collector + linked dynamic dataset
COL=$(curl -sX POST https://api.every.farm/v1/farms/$FARM/collectors \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Soil samples 2026",
"fields": [
{"label":"pH","type":"Number","required":true},
{"label":"Variety","type":"Select","options":["Cab","Merlot"]}
]
}' | jq -r .collectorId)
# 2. (Optional) Evolve the schema later
curl -X PATCH https://api.every.farm/v1/collectors/$COL \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"fields":[
{"label":"pH","machine_name":"p_h","type":"Number","required":true},
{"label":"Variety","machine_name":"variety","type":"Select","options":["Cab","Merlot"]},
{"label":"Moisture","type":"Number","suffix":"%"}
]}'
# 3. Push observations
curl -X POST https://api.every.farm/v1/collectors/$COL/datapoints \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"datapoints":[
{"lat":42.66,"lng":-77.46,"p_h":6.4,"variety":"Cab","moisture":18},
{"lat":42.67,"lng":-77.45,"p_h":6.7,"variety":"Merlot","moisture":22}
]}'
# 4. Recompile
curl -X POST https://api.every.farm/v1/collectors/$COL/recompile -H "Authorization: Bearer $TOKEN"#Check Usage
GET /v1/usage
Returns current rate limit usage for the token.
Response:
{
"today": {
"requestCount": 142,
"limit": 10000
},
"rateLimitPerMinute": 60,
"lastUsedAt": "2026-04-09T..."
}#Satellite Imagery & Analysis
The /v1/satellite/* endpoints expose Every.Farm's remote-sensing pipeline to API consumers — including AI agent systems. With these endpoints you can:
- Discover what imagery sources, indices, and analysis recipes are available.
- Search open satellite archives (Sentinel-2, Sentinel-1, Landsat, HLS, etc.) for scenes that overlap a block.
- Run an analysis (e.g. compute NDVI from a single scene, or aggregate a season of NDVI into a temporal mean) — vector-first by default.
- Poll for completion and retrieve the resulting dataset, which lands in the destination farm under
datasets/{id}like any other Every.Farm dataset.
All satellite endpoints respect the same authentication, scopes, rate limits, and farm-role permissions as the rest of the v1 API. read scope is sufficient for discovery and scene search; write scope is required to submit analyses (since the result is written into a farm).
#List Imagery Collections
GET /v1/satellite/collections
Scope: read
Lists open remote-sensing collections the platform can ingest. The id returned here is what you pass as collection everywhere else.
Response:
{
"collections": [
{
"id": "sentinel-2-l2a",
"label": "Sentinel-2 L2A",
"provider": "ESA Copernicus",
"kind": "optical",
"nativeResolutionM": 10,
"cloudCover": true,
"description": "Multispectral optical surface reflectance...",
"stacEndpoint": "earthSearch"
},
{
"id": "sentinel-1-rtc",
"label": "Sentinel-1 RTC (SAR)",
"provider": "ESA Copernicus / Microsoft Planetary Computer",
"kind": "sar",
"nativeResolutionM": 10,
"cloudCover": false,
"stacEndpoint": "planetaryComputer"
}
]
}Currently supported collections (12) — every collection the platform's frontend ingestion drawer offers is reachable from the API:
| Collection id | Kind | Native res | Cloud cover | Analysis paths |
|---|---|---|---|---|
sentinel-2-l2a |
optical | 10 m | yes | single_scene, temporal_mean |
sentinel-1-rtc |
sar | 10 m | n/a | single_scene, temporal_mean, raster_ingest |
landsat-c2-l2 |
optical | 30 m | yes | single_scene, raster_ingest |
hls2-l30 |
optical | 30 m | yes | single_scene, raster_ingest |
hls2-s30 |
optical | 30 m | yes | single_scene, raster_ingest |
naip |
optical (RGB+NIR aerial) | 0.6 m | n/a | raster_ingest |
modis-13Q1-061 |
optical (precomputed NDVI/EVI) | 250 m | n/a | raster_ingest |
3dep-seamless |
terrain (DEM) | 10 m | n/a | raster_ingest |
gnatsgo-rasters |
soil | 10 m | n/a | raster_ingest |
daymet-daily-na |
climate (daily) | 1 km | n/a | raster_ingest |
io-lulc-9-class |
land cover (categorical) | 10 m | n/a | raster_ingest |
esa-worldcover |
land cover (categorical) | 10 m | n/a | raster_ingest |
Every entry's analysisPaths is also surfaced on each collection object in the API response, so an agent can introspect programmatically which recipes apply rather than relying on this table.
#List Analysis Recipes
GET /v1/satellite/recipes
Scope: read
Lists named analyses you can submit via POST /v1/satellite/analyses. Each recipe describes its expected inputs shape.
Response:
{
"recipes": [
{
"id": "single_scene",
"label": "Single-scene index extraction",
"description": "Compute one or more indices from a single satellite scene...",
"inputs": {
"sceneId": { "type": "string", "required": true },
"collection": { "type": "string", "required": true },
"indices": { "type": "string[]", "required": true },
"includeRasterPreview": { "type": "boolean", "default": true },
"includeRasterDownload": { "type": "boolean", "default": false }
},
"output": "One vector dataset per requested index, written to the destination farm."
},
{
"id": "temporal_mean",
"label": "Temporal aggregation",
"description": "Aggregate a time series of scenes into a single vector grid...",
"inputs": {
"source": { "type": "string", "enum": ["s2-ndvi", "s1-vh"], "required": true },
"dateRange": { "type": "{start,end}", "required": true },
"aggregation": { "type": "string", "enum": ["mean","median","min","max","stddev","count"], "required": true },
"cloudCoverMax": { "type": "number", "default": 30 }
}
},
{
"id": "raster_ingest",
"label": "Raster ingest (native product)",
"description": "Ingest a single STAC scene as a raster dataset (GeoTIFF + PNG preview)...",
"inputs": {
"sceneId": { "type": "string", "required": true },
"collection": { "type": "string", "required": true },
"kind": { "type": "string", "enum": ["native","trueColor"], "default": "native" }
},
"output": "One raster dataset (GeoTIFF + PNG preview). Eligible as a custom base layer."
}
]
}Recipe / collection compatibility: check each collection's analysisPaths for the authoritative list. As a quick reference:
single_scene/temporal_mean— optical + SAR (S2, S1, Landsat, HLS) that have band data for indices.raster_ingest— every collection except S2 on Earth Search (NAIP, MODIS, DEM, soil, climate, land cover, plus the MPC-hosted optical + SAR for true-color composites). Produces a raster dataset that is also eligible as the farm's custom base layer.
Example — ingest a NAIP aerial scene as a raster dataset:
curl -X POST https://api.every.farm/v1/satellite/analyses \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"recipe": "raster_ingest",
"farm_id": "'$FARM'",
"block_id": "'$BLOCK'",
"inputs": {
"sceneId": "'$SCENE_ID'",
"collection": "naip"
}
}'Returns a job_id. Poll GET /v1/satellite/analyses/:job_id until status: complete, then the resulting result.datasetId references a raster dataset visible alongside any other dataset on the farm. Because file.raster: true is set on the dataset, it's also selectable in the UI under Farm Settings → Farm Base Map as a custom basemap.
#List Vector-Supported Indices
GET /v1/satellite/indices
Scope: read
Lists every analytical index that can be ingested as a vector grid, along with which collections support it. Use this when an agent needs to know which indices it can pass to the single_scene recipe for a given collection.
Response (excerpt):
{
"indices": [
{
"id": "ndvi",
"label": "NDVI",
"description": "Normalized Difference Vegetation Index",
"appliesTo": ["sentinel-2-l2a","sentinel-2-l1c","landsat-c2-l2","hls2-l30","hls2-s30"],
"expectedRange": [-1, 1],
"suggestedColormap": "rdylgn"
},
{
"id": "ndre",
"appliesTo": ["sentinel-2-l2a","sentinel-2-l1c","hls2-s30"],
"expectedRange": [-1, 1]
}
]
}Currently supported index ids: ndvi, ndre, savi, evi, gndvi, ndwi, msi, ndsi (optical) and vv_db, vh_db, rvi, vv_vh_ratio (SAR).
#Search Scenes
GET /v1/satellite/scenes
Scope: read
Search open STAC catalogs (Element 84 Earth Search, Microsoft Planetary Computer) for scenes that overlap a block, in a date range, optionally filtered by cloud cover. Unknown query parameters are rejected with a 400 (so a typo like collection instead of collections fails loudly instead of being silently ignored).
Query parameters:
| Param | Required | Description |
|---|---|---|
farm_id |
Yes | Farm the block belongs to (caller must have read access) |
block_id |
Yes | Block whose footprint defines the search bounding box |
start |
Yes | ISO date — start of search window |
end |
Yes | ISO date — end of search window |
collections |
No | Comma-separated collection ids; defaults to S2 + S1 + Landsat |
cloud_cover_max |
No | 0–100 — applies to optical collections only |
limit |
No | Max scenes per collection (default 25, max 100) |
Response:
{
"bbox": [-77.4, 42.4, -77.3, 42.5],
"start": "2025-06-01T00:00:00Z",
"end": "2025-09-30T00:00:00Z",
"results": [
{
"collection": "sentinel-2-l2a",
"count": 18,
"scenes": [
{
"id": "S2A_MSIL2A_20250812T154851_R068_T18TVN_20250812T215012",
"collection": "sentinel-2-l2a",
"datetime": "2025-08-12T15:48:51Z",
"cloudCover": 4.2,
"bbox": [-77.5, 42.3, -77.2, 42.6],
"assetKeys": ["red","green","blue","nir","rededge1","swir16","..."]
}
]
}
]
}The id field of each scene is what you pass as inputs.sceneId when submitting an analysis.
#Submit an Analysis
POST /v1/satellite/analyses
Scope: write
Kicks off an analysis recipe. The endpoint validates the request, persists it, and returns immediately with a job id. The actual work runs asynchronously in a background worker (4 GB / up to 540 s) — poll GET /v1/satellite/analyses/:job_id for progress and the final dataset id.
Request body:
{
"recipe": "single_scene",
"farm_id": "farm_abc123",
"block_id": "block_xyz",
"inputs": {
"sceneId": "S2A_MSIL2A_20250812T154851_R068_T18TVN_20250812T215012",
"collection": "sentinel-2-l2a",
"indices": ["ndvi", "ndre"],
"includeRasterPreview": true,
"includeRasterDownload": false
},
"folder_id": "root",
"dataset_name": "NDVI 2025-08-12",
"dataset_description": "Submitted via developer API"
}Body fields:
| Field | Required | Description |
|---|---|---|
recipe |
Yes | One of the recipe ids from /v1/satellite/recipes |
farm_id |
Yes | Destination farm — must be writable by the token's user |
block_id |
Yes | Block geometry to clip / sample over |
inputs |
Yes | Recipe-specific input bag (see /v1/satellite/recipes) |
cell_size |
No | Grid cell size in meters — one of 10, 20, 30, 100 (400 on anything else); default = source native. Use this for large blocks: a 100 m grid makes the output ~100× smaller than 10 m. Vector output is capped at 250,000 cells; an oversized default grid is auto-coarsened to the next size that fits (the poll response then carries cellSizeAutoCoarsened: true and the actual cellSize), while an explicitly requested size that exceeds the cap fails with guidance on which size to pass. |
folder_id |
No | Destination folder; default "root" |
dataset_name |
No | Override default dataset name |
dataset_description |
No | Optional description prepended to the auto-generated description |
Response (202 Accepted):
{
"job_id": "api_1717012345678_a1b2c3d4",
"status": "queued",
"poll_url": "/v1/satellite/analyses/api_1717012345678_a1b2c3d4"
}Concurrency limit: each user may have at most 3 analysis jobs queued or running at once. Submitting past the cap returns 429 with retryAfterSeconds — wait for a job to finish (poll it), then resubmit.
Upstream resilience: transient upstream failures (catalog 429s / 5xx) are retried automatically inside the job (3 attempts with backoff). If the budget is exhausted the job fails with retryable: true — simply resubmit the same request.
#Poll an Analysis
GET /v1/satellite/analyses/:job_id
Scope: read
Returns the current status of an analysis job. While running, stage, stage_label, and percent reflect live progress. When complete, result contains the dataset id(s) the analysis wrote — fetch them via the regular dataset endpoints.
Response (in flight):
{
"job_id": "api_1717012345678_a1b2c3d4",
"recipe": "single_scene",
"farm_id": "farm_abc123",
"block_id": "block_xyz",
"status": "running",
"stage": "fetching",
"stage_label": "Fetching NDVI",
"detail": "Index 1 of 2",
"percent": 45,
"error": null,
"result": null
}Response (complete):
{
"job_id": "api_1717012345678_a1b2c3d4",
"recipe": "single_scene",
"status": "complete",
"stage": "complete",
"percent": 100,
"result": {
"success": true,
"datasetId": "dataset_new_a1",
"datasetIds": ["dataset_new_a1", "dataset_new_a2"],
"meta": {
"indexCount": 2,
"cellCount": 1280,
"cellSize": 10,
"utmZone": 18
}
}
}With the dataset id in hand, the agent can fetch the resulting GeoJSON via GET /v1/datasets/:dataset_id/geojson exactly like any other Every.Farm dataset.
Failed jobs return status: "failed" with a human-readable error field, plus retryable: true when the failure was a transient upstream condition (e.g. a satellite catalog 429/5xx that survived in-job retries) — resubmitting the same request is expected to succeed.
#End-to-End Agent Workflow
A typical AI-agent loop using these endpoints looks like:
# 1. Discover what's available
curl https://api.every.farm/v1/satellite/collections -H "Authorization: Bearer $TOKEN"
curl https://api.every.farm/v1/satellite/recipes -H "Authorization: Bearer $TOKEN"
# 2. Find scenes over a block
curl "https://api.every.farm/v1/satellite/scenes?farm_id=$FARM&block_id=$BLOCK&start=2025-06-01&end=2025-09-30&collections=sentinel-2-l2a&cloud_cover_max=20" \
-H "Authorization: Bearer $TOKEN"
# 3. Submit an analysis (returns a job id)
curl -X POST https://api.every.farm/v1/satellite/analyses \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"recipe":"single_scene",
"farm_id":"'$FARM'","block_id":"'$BLOCK'",
"inputs":{"sceneId":"'$SCENE_ID'","collection":"sentinel-2-l2a","indices":["ndvi","ndre"]}
}'
# 4. Poll until done
curl https://api.every.farm/v1/satellite/analyses/$JOB_ID -H "Authorization: Bearer $TOKEN"
# 5. Fetch the resulting vector dataset
curl https://api.every.farm/v1/datasets/$DATASET_ID/geojson -H "Authorization: Bearer $TOKEN"#Public Dataset Endpoint
Datasets can be marked as Public in the app (Dataset Details > Downloads tab). It previously exposed the following read-only endpoint without authentication (now retired — see the note below):
GET https://api.every.farm/dataset/:dataset_id/geojsonThis is the legacy endpoint shown in the app. It returns:
{
"geojson": { ... },
"datasetId": "dataset_123",
"datasetName": "Yield 2025"
}Datasets are private by default.
Update — this endpoint has been retired. As part of a security hardening pass, the entire legacy unauthenticated API (including
GET /dataset/:dataset_id/geojsonand theapi.efficientvineyard.comhost) was removed. Those URLs now return404. All dataset access — public or private — now goes through the token-authenticated/v1API. Read a dataset withGET /v1/datasets/:dataset_id/geojsonand a Bearer token (see the Endpoints section above); the token's farm-role permissions govern what it can read.
#Error Responses
All errors follow a consistent format:
{ "error": "Description of what went wrong." }| Status | Meaning |
|---|---|
400 |
Bad request — missing or invalid parameters |
401 |
Unauthorized — missing, invalid, or expired token |
403 |
Forbidden — token lacks scope or farm access |
404 |
Not found — resource doesn't exist |
429 |
Rate limited — try again later |
500 |
Server error |
#Quick Start Example
1. Generate a token in the app: account menu → API Tokens → New token
2. List your farms:
curl https://api.every.farm/v1/farms \
-H "Authorization: Bearer efv_your_token_here"3. Get datasets for a farm:
curl https://api.every.farm/v1/farms/FARM_ID/datasets \
-H "Authorization: Bearer efv_your_token_here"4. Download GeoJSON:
curl https://api.every.farm/v1/datasets/DATASET_ID/geojson \
-H "Authorization: Bearer efv_your_token_here"5. Push a dataset:
curl -X POST https://api.every.farm/v1/farms/FARM_ID/datasets \
-H "Authorization: Bearer efv_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"name": "My New Dataset",
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "value": 42 },
"geometry": { "type": "Point", "coordinates": [-121.7, 38.5] }
}
]
}
}'#GeoJSON Format Reference
All dataset data uses the GeoJSON specification (RFC 7946). A valid dataset must be a FeatureCollection:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"field_name": "value"
},
"geometry": {
"type": "Point",
"coordinates": [longitude, latitude]
}
}
]
}Supported geometry types: Point, Polygon, LineString, MultiLineString, MultiPolygon
Coordinates use [longitude, latitude] order (WGS 84 / EPSG:4326).
Properties can contain any key-value pairs. The keys from the first feature become the dataset's column headers in the application.
Updated
Something missing or out of date? Tell us — the docs are updated with every release.