43 KiB
AgMission Data Export API — Customer Integration Guide
Audience: Technical Integrators, BI Teams, Data Warehouse Engineers
Version: 1.0
Last Updated: May 2026
Table of Contents
- Overview
- Quick Start
- Authentication
- API Endpoints
- Rate Limiting
- Data Formats
- Use Cases
- Error Handling
- Support & SLAs
Overview
The AgMission Data Export API provides programmatic access to spray application data for integration with business intelligence tools, data warehouses, and custom systems.
Capabilities
- Real-time session summaries — Coverage, timing, pilot, aircraft info (GET
/api/v1/jobs/:jobId/sessions) - Raw GPS trace records — Point-by-point telemetry with cursor pagination (GET
/api/v1/jobs/:jobId/sessions/:fileId/records) - Spray area polygons — GeoJSON boundaries for mapping (GET
/api/v1/jobs/:jobId/areas) - Async bulk export — CSV or JSON for full data lake ingestion (POST/GET
/api/v1/jobs/:jobId/export)
Who Should Use This API
| Role | Use Case |
|---|---|
| BI Engineer | Power BI incremental refresh, Tableau connectors |
| Data Warehouse | Nightly batch loads, transformation pipelines |
| GIS Analyst | ArcGIS layer ingestion, spatial analysis |
| Compliance Officer | Audit trails, proof-of-application records |
| Agronomist | Yield correlation, efficacy analysis |
Architecture
graph TD
ext[Your System]
gw[AgMission API Gateway - Auth and Rate Limiting]
sess[GET /api/v1/jobs/:id/sessions]
recs[GET /api/v1/jobs/:id/sessions/:fid/records]
areas[GET /api/v1/jobs/:id/areas]
exp[POST /api/v1/jobs/:id/export]
stat[GET /api/v1/exports/:id]
dl[GET /api/v1/exports/:id/download]
data[(AgMission Data Services)]
ext -->|X-API-Key over HTTPS| gw
gw --> sess
gw --> recs
gw --> areas
gw --> exp
gw --> stat
gw --> dl
sess --> data
recs --> data
areas --> data
exp --> data
stat --> data
dl --> data
style ext fill:#e3f2fd
style gw fill:#f3e5f5
style data fill:#e8f5e9
Quick Start
1. Get an API Key
Contact your AgMission account manager or self-serve using a Master Account at https://agmission.agnav.com/api-keys:
Test Export: 3v8x2j9kL4m5nQ6... (test key)
Live Export: 7p2r9w4tY3h8k1... (production key)
...
2. List sessions for a job
JOB_ID=12345
API_KEY="3v8x2j9kL4m5nQ6..."
curl -X GET "https://api.agmission.com/api/v1/jobs/${JOB_ID}/sessions" \
-H "X-API-Key: ${API_KEY}"
Response:
{
"jobId": 12345,
"clientId": "507f1f77bcf86cd799439055",
"clientName": "Fazenda São Paulo Ltda",
"assignedPilotId": "507f1f77bcf86cd799439033",
"assignedPilotName": "John Smith",
"assignedAircraftId": "507f1f77bcf86cd799439044",
"assignedAircraftName": "AT-802F",
"assignedAircraftTailNumber": "N1234AT",
"planAircraftName": "AT-802F",
"planAircraftTailNumber": "N1234AT",
"assignedDate": "2026-04-21T18:00:00Z",
"mappedArea_ha": 48.5,
"reportConfirmed": false,
"areaSize_ha": 48.5,
"coverage_ha": 45.2,
"overSprayed_pct": -6.80,
"appRate": 50,
"appRateUnit": "lit/ha",
"appRateConfirmed": null,
"sprayVolume": 2260,
"volumeUnit": "lit",
"useConfirmedVolume": false,
"actualSprayVolume": 2260,
"confirmedActualVolume": null,
"effectiveVolume": 2260,
"useCustomWeather": false,
"weather": null,
"data": [
{
"sessionId": "507f1f77bcf86cd799439011",
"fileName": "flight_20260422_001.log",
"startDateTime": "2026-04-22T09:00:00Z",
"endDateTime": "2026-04-22T11:30:00Z",
"totalFlightTime_s": 9000,
"totalSprayTime_s": 7200,
"totalTurnTime_s": 1800,
"totalSprayed_ha": 45.2,
"totalSprayMat": 2260,
"totalSprayMatUnit": "lit",
"avgSpraySpeed_ms": 39.5,
"sprayZoneName": "Field A North",
"sprayZoneArea_ha": 25.0,
"appRate": 50,
"appRateUnit": "lit/ha",
"flowController": "Ag-Flow UFC",
"sprayOnLag_s": 0.2,
"sprayOffLag_s": 0.15,
"pulsesPerLiter": 1800,
"files": [
{ "fileId": "507f1f77bcf86cd799439022", "name": "n5021813.t44" }
],
"sessionPilotName": "John Smith"
}
]
}
3. Export to CSV
JOB_ID=12345
API_KEY="3v8x2j9kL4m5nQ6..."
# Trigger export (async)
EXPORT_ID=$(curl -s -X POST "https://api.agmission.com/api/v1/jobs/${JOB_ID}/export" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"format":"csv","units":"metric"}' \
| jq -r '.exportId')
echo "Export ID: $EXPORT_ID"
# Poll for completion
while true; do
STATUS=$(curl -s -X GET "https://api.agmission.com/api/v1/exports/${EXPORT_ID}" \
-H "X-API-Key: ${API_KEY}" \
| jq -r '.status')
echo "Status: $STATUS"
if [ "$STATUS" = "ready" ]; then
break
fi
sleep 5
done
# Download
curl -X GET "https://api.agmission.com/api/v1/exports/${EXPORT_ID}/download" \
-H "X-API-Key: ${API_KEY}" \
-o "export_job${JOB_ID}.csv"
echo "Downloaded: export_job${JOB_ID}.csv"
Authentication
API Key Format
API keys are Bearer tokens supplied via the X-API-Key header (NOT Authorization header).
sequenceDiagram
participant Client as Your System
participant API as AgMission API
participant Auth as Auth Middleware
participant KeyStore as API Key Service
Client->>API: GET /api/v1/jobs/123/sessions
Note over Client,API: Header: X-API-Key: 3v8x2j9kL4m5nQ6...
API->>Auth: Verify key
Auth->>KeyStore: Validate API key
KeyStore-->>Auth: Key valid for account
Auth-->>API: req.uid set to account owner
API-->>Client: 200 JSON response
DO NOT use Authorization: Bearer 3v8x2j9... — This will fail!
# ✅ CORRECT
curl -H "X-API-Key: 3v8x2j9kL4m5nQ6..." \
https://api.agmission.com/api/v1/jobs/12345/sessions
# ❌ WRONG
curl -H "Authorization: Bearer 3v8x2j9kL4m5nQ6..." \
https://api.agmission.com/api/v1/jobs/12345/sessions
Key Management
- Create new keys at
https://agmission.agnav.com/api-keys - Rotate keys by creating new ones and disabling old ones
- Scope keys by each Master account
- Revoke immediately if compromised
Security Best Practices
-
Never commit keys to version control — Use environment variables or secrets manager
export AGMISSION_API_KEY="3v8x2j9kL4m5nQ6..." curl -H "X-API-Key: $AGMISSION_API_KEY" https://api.agmission.com/... -
Use HTTPS only — All API endpoints require HTTPS.
-
Rotate keys quarterly — Implement key rotation in your automation
-
Monitor key usage — Check activity logs for suspicious patterns
API Endpoints
NOTE: mappedArea_ha is the mapped/planned area value for the job and is not derived by summing the /areas GeoJSON polygons.
1. List Sessions
Endpoint: GET /api/v1/jobs/:jobId/sessions
Returns one summary per uploaded flight log file.
Parameters:
jobId(path) — Job ID (integer)
Response (200 OK):
{
"jobId": 12345,
"clientId": "507f1f77bcf86cd799439055",
"clientName": "Fazenda São Paulo Ltda",
"assignedPilotId": "507f1f77bcf86cd799439033",
"assignedPilotName": "John Smith",
"assignedAircraftId": "507f1f77bcf86cd799439044",
"assignedAircraftName": "AT-802F",
"assignedAircraftTailNumber": "N1234AT",
"planAircraftName": "AT-802F",
"planAircraftTailNumber": "N1234AT",
"assignedDate": "2026-04-21T18:00:00Z",
"mappedArea_ha": 48.5,
"reportConfirmed": false,
"areaSize_ha": 48.5,
"coverage_ha": 45.2,
"overSprayed_pct": -6.80,
"appRate": 50,
"appRateUnit": "lit/ha",
"appRateConfirmed": null,
"sprayVolume": 2260,
"volumeUnit": "lit",
"useConfirmedVolume": false,
"actualSprayVolume": 2260,
"confirmedActualVolume": null,
"effectiveVolume": 2260,
"useCustomWeather": false,
"weather": null,
"data": [
{
"sessionId": "507f1f77bcf86cd799439011",
"fileName": "flight_20260422_001.log",
"startDateTime": "2026-04-22T09:00:00Z",
"endDateTime": "2026-04-22T11:30:00Z",
"totalFlightTime_s": 9000,
"totalSprayTime_s": 7200,
"totalTurnTime_s": 1800,
"totalSprayed_ha": 45.2,
"totalSprayMat": 2260,
"totalSprayMatUnit": "lit",
"avgSpraySpeed_ms": 39.5,
"sprayZoneName": "Field A North",
"sprayZoneArea_ha": 25.0,
"appRate": 50,
"appRateUnit": "lit/ha",
"flowController": "Ag-Flow UFC",
"sprayOnLag_s": 0.2,
"sprayOffLag_s": 0.15,
"pulsesPerLiter": 1800,
"files": [
{ "fileId": "507f1f77bcf86cd799439022", "name": "n5021813.t44" }
],
"sessionPilotName": "John Smith"
}
]
}
Confirmed vs Fallback Values:
When reportConfirmed: true, the applicator has manually confirmed spray records in Report Settings:
areaSize_ha,coverage_ha,appRate,confirmedActualVolume,weathercome from the report- Otherwise, system-calculated fallbacks are used
Volume derivation for /sessions envelope:
sprayVolume: planned estimate based on total spray area and app rate (coverage_ha × appRate)actualSprayVolume: total calculated from recorded application sessionsconfirmedActualVolume: value entered in Report Settings when confirmedeffectiveVolume:confirmedActualVolumewhenuseConfirmedVolume=true, otherwiseactualSprayVolumevolumeUnit: follows job unit system and material type (lit/galfor liquid,kg/lbfor solid/dry)
2. Get Records (Paginated GPS Trace)
Endpoint: GET /api/v1/jobs/:jobId/sessions/:fileId/records
Streams raw GPS points with cursor-based pagination.
Parameters:
jobId(path) — Job IDfileId(path) — Session/file IDstartingAfter(query) — Cursor for paginationlimit(query) — Records per page (default 500, max 2000)interval(query) — GPS thinning interval in seconds (float). Spray-state changes are always included.interval=0(query) — Explicitly disable thinning (same as omittinginterval).
Interval Decision Table (/records):
Assume interval=5 seconds and records are processed in returned order.
Previous Kept gpsTime |
Current gpsTime |
sprayStat Changed? |
Keep Current Record? | Reason |
|---|---|---|---|---|
| none | 100 | N/A | Yes | First record is always kept |
| 100 | 103 | No | No | Inside 5-second window |
| 100 | 103 | Yes | Yes | Spray-state transition is always preserved |
| 100 | 106 | No | Yes | Outside interval window (106 - 100 >= 5) |
| 106 | 109 | No | No | Inside 5-second window |
| any | any | any | Yes (all) | If interval=0 (or omitted), thinning is bypassed |
Example: Fetch 500 records, every 5 seconds
curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?limit=500&interval=5" \
-H "X-API-Key: 3v8x2j9..."
Response (200 OK):
{
"data": [
{
"timeUtc": "2026-04-22T09:00:15Z",
"gpsTime": 1745312415,
"lat": 40.7128,
"lon": -74.0060,
"alt": 150.5,
"grSpeed": 39.8,
"heading": 180,
"sprayStat": 1,
"flowRateApplied": 48.5,
"appRateApplied": 49.3,
"windSpeed_kt": 6.22,
"windDir_deg": 225,
"temp_c": 22.5,
"humidity_pct": 65
}
],
"hasMore": true,
"startingAfter": "507f191e810c19729de8605f",
"endingBefore": "507f1f77bcf86cd799439011"
}
Cursor field meanings:
startingAfter— pass as query param to get the next pageendingBefore— pass as query param to get the previous pagehasMore: false+ nostartingAftermeans you have reached the last page
API Response Field Descriptions (/records):
Envelope fields:
| Field | Type | Description |
|---|---|---|
data |
array | Per-point telemetry records for the page (after optional interval thinning). |
hasMore |
boolean | true when additional pages exist. Pass startingAfter to fetch the next page. |
startingAfter |
string | undefined | Last record ID of this page — pass as ?startingAfter= to get the next page. Present whenever data is non-empty. |
endingBefore |
string | undefined | First record ID of this page — pass as ?endingBefore= to get the previous page. Present whenever data is non-empty. |
GPS fields (per record in data[]):
| Field | Type | Unit | Description |
|---|---|---|---|
timeUtc |
string | null | ISO 8601 UTC | GPS timestamp converted from gpsTime. |
gpsTime |
number | null | epoch seconds | Raw GPS time as seconds since epoch. |
lat |
number | null | decimal degrees | Latitude (WGS84). 7 decimal places. |
lon |
number | null | decimal degrees | Longitude (WGS84). 7 decimal places. |
utmX |
number | null | meters | UTM easting coordinate. 1 decimal place. |
utmY |
number | null | meters | UTM northing coordinate. 1 decimal place. |
alt |
number | null | meters | Altitude above sea level. 2 decimal places. |
grSpeed |
number | null | m/s | Aircraft ground speed. 2 decimal places. |
heading |
number | null | degrees | Aircraft heading (0–360°). 2 decimal places. |
xTrack |
number | null | meters | Cross-track error from the guidance line. 2 decimal places. |
lockedLine |
number | null | — | Guidance line number locked by the autopilot. |
hdop |
number | null | — | Horizontal dilution of precision — lower is better GPS geometry. 2 decimal places. |
satsIn |
number | null | — | Encoded satellite/inside-area value. 0..99 = outside area (value is satellite count). 100..199 = inside area (satellites = value - 100). Example: 112 = inside area with 12 satellites; 17 = outside area with 17 satellites. |
tslu |
number | null | seconds | Time since last GPS differential correction update. Measures staleness of DGPS correction signal. |
calcodeFreq |
number | null | — | Raw calibration/frequency field from the spray controller. 30,000–60,000: frequency/RPM mode — true RPM = value − 30,000. < 20,000: positive spray offset in decimeters. > 60,000: negative spray offset — stored value = 65,536 − abs(offset). |
sprayStat |
number | null | — | Spray state. 0 = spray OFF. 1 = spray ON, inside spray area (continuing record). 3 = spray ON, first point of a new spray line (start-of-line marker; boom IS active). 10 = spray ON, outside spray area. Any non-zero value = boom open. |
Application data fields:
| Field | Type | Unit | Description |
|---|---|---|---|
flowRateApplied |
number | null | L/min | Actual spray system flow rate as measured. Raw value from controller (lminApp). |
flowRateRequired |
number | null | L/min | Target flow rate set by the spray controller (lminReq). |
appRateRequired |
number | null | L/ha or kg/ha | Planned/target application rate, computed with this priority: (1) file metadata app rate, (2) controller-reported required rate (lhaReq), (3) job plan app rate. Always in metric. |
appRateApplied |
number | null | L/ha or kg/ha | Computed actual application rate. null when sprayStat is 0 (spray off). Non-null for all spray-on states: sprayStat = 1 (on, inside area), 3 (start of line), or 10 (on, outside area). For liquid material: derived from flow rate, swath, and ground speed. For dry material: flow reading used directly. When no flow controller is fitted or no flow reading is present, the file metadata app rate is used. Always in metric. |
swathWidth |
number | null | meters | Effective boom/swath width at this point. |
boomPressure_psi |
number | null | PSI | Boom pressure reading. |
flowController |
string | — | Flow controller name from the session. Normalised to 'No FC' when absent or set to "none" in the source file. Session constant — same value repeated on every record. |
sprayOnLag_s |
number | null | seconds | Spray-on lag configured in the session. Session constant repeated per record. |
sprayOffLag_s |
number | null | seconds | Spray-off lag configured in the session. Session constant repeated per record. |
pulsesPerLiter |
number | null | — | Flow meter calibration constant from the session. Session constant repeated per record. |
rpm |
array | null | — | RPM array from the spray controller. Interpretation differs between liquid and dry material types. |
MET (weather) fields:
| Field | Type | Unit | Description |
|---|---|---|---|
windSpeed_kt |
number | null | knots | Wind speed (converted from m/s at output). 2 decimal places. |
windDir_deg |
number | null | degrees | Wind direction (0–360°). 1 decimal place. |
temp_c |
number | null | °C | Air temperature. 1 decimal place. |
humidity_pct |
number | null | % | Relative humidity. 1 decimal place. |
FM fields (only when ?fm=true is included in the request — FM-enabled equipment only):
| Field | Type | Unit | Description |
|---|---|---|---|
sprayHeight_m |
number | null | meters | Target spray height (AgDisp). |
driftX_m |
number | null | meters | Lateral drift offset X (AgDisp). |
driftY_m |
number | null | meters | Lateral drift offset Y (AgDisp). |
depositX_m |
number | null | meters | Deposit offset X (AgDisp). |
depositY_m |
number | null | meters | Deposit offset Y (AgDisp). |
radarAlt_m |
number | null | meters | Radar altimeter reading. |
laserAlt_m |
number | null | meters | Laser altimeter reading. |
appRateAppliedis null when spray is off (sprayStat = 0). All other values (1, 3, 10) are spray-on states and will carry a computed rate.
sprayStat = 3is the first sprayed point of a new spray line — spray IS active at this record. It is written when the boom transitions from OFF to ON. IncludesprayStat = 3records when computing spray coverage; excludesprayStat = 0records only.
satsIndecoding: inside-area is encoded as+100, not bitmask. Parse with:inside = (satsIn >= 100)andsatellites = inside ? (satsIn - 100) : satsIn.
Pagination:
# Get next page
curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?startingAfter=507f191e810c19729de8605f" \
-H "X-API-Key: 3v8x2j9kL4m5nQ6..."
Use Cases:
- Power BI incremental refresh: Use
startingAfterto fetch only new records since last sync - Lightweight queries: Use
interval=5to reduce data volume by 5x - Parity testing: Use
interval=0for full-fidelity page comparisons
3. Get Spray Areas
Endpoint: GET /api/v1/jobs/:jobId/areas
Returns GeoJSON FeatureCollection of planned spray zones.
Response (200 OK):
{
"type": "FeatureCollection",
"jobId": 12345,
"features": [
{
"type": "Feature",
"properties": {
"name": "North Field",
"type": "area",
"area_ha": 48.5,
"appRate": 50,
"appRateUnit": "lit/ha"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-74.0060, 40.7128],
[-74.0050, 40.7128],
[-74.0050, 40.7118],
[-74.0060, 40.7118]
]]
}
},
{
"type": "Feature",
"properties": {
"name": "Exclude - Power Lines",
"type": "xcl"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-74.0055, 40.7125],
[-74.0053, 40.7125],
[-74.0053, 40.7120]
]]
}
}
]
}
Field Meanings:
type: "area"— Planned spray zone (will have appRate/unit)type: "xcl"— Exclusion zone (no-spray boundary, skipped fields)area_ha— Polygon area in hectaresappRateUnit— Material unit string ('lit/ha','oz/ac', etc.)
Import to ArcGIS:
// JavaScript + ArcGIS JS API
const response = await fetch('https://api.agmission.com/api/v1/jobs/12345/areas', {
headers: { 'X-API-Key': apiKey }
});
const featureCollection = await response.json();
const layer = new FeatureLayer({
source: featureCollection.features,
objectIdField: 'OBJECTID',
fields: [...],
renderer: {...}
});
map.add(layer);
4. Trigger Export (Async)
Endpoint: POST /api/v1/jobs/:jobId/export
Initiates async generation of a bulk export.
stateDiagram-v2
[*] --> pending: POST /export returns 202
pending --> processing: async generation starts
processing --> ready: file written to disk
processing --> error: generation failed
ready --> [*]: 24h TTL expires
error --> [*]: TTL expires
Poll GET /exports/:exportId until status: "ready", then call the download endpoint.
Request Body:
{
"format": "csv",
"units": "metric",
"interval": null
}
Parameters:
format(string) —"csv"or"json"units(string, optional) —"metric"(default) or"us"interval(number, optional) — GPS point thinning in seconds (float). Spray-state changes are always included.fm(boolean, optional) —trueto include Flight Master/AgDisp FM fields (sprayHeight_m,driftX_m,driftY_m,depositX_m,depositY_m,radarAlt_m,laserAlt_m). Defaultfalse. Only applicable for customers with FM-enabled equipment.
Bulk export interval behavior:
- Records are exported in a stable, deterministic order per file.
- If
intervalis omitted ornull, all points are exported. - If
intervalis provided, points inside the same window are thinned out. - Records where
sprayStatchanges are never removed by thinning. - Thinning is applied independently per file stream (not across one global timeline).
- Set
interval=0(or omitinterval) for full-fidelity export on bulk endpoints.
Interval Decision Table (Bulk Export):
Bulk export uses the same keep/skip rule as /records, but runs per file stream.
Previous Kept gpsTime (same file) |
Current gpsTime |
sprayStat Changed? |
Keep Current Record? | Reason |
|---|---|---|---|---|
| none | 500 | N/A | Yes | First record in file is kept |
| 500 | 503 | No | No | Inside interval window |
| 500 | 503 | Yes | Yes | Spray transition event is preserved |
| 500 | 506 | No | Yes | Outside interval window |
API and Field Descriptions (Bulk Export Endpoints):
POST /api/v1/jobs/:jobId/export response fields:
| Field | Type | Description |
|---|---|---|
exportId |
string | Export tracker ID used for polling and download. |
status |
string | Initial status (pending) or reused status when deduplicated. |
format |
string | csv or json. |
units |
string | metric or us. |
createdAt |
string | ISO UTC creation timestamp. |
reused |
boolean | Present when request deduplicates to an existing export. |
downloadUrl |
string | Present immediately if reused export is already ready. |
GET /api/v1/exports/:exportId response fields:
| Field | Type | Description |
|---|---|---|
exportId |
string | Export tracker ID. |
status |
string | pending, processing, ready, or error. |
format |
string | Export format. |
units |
string | Unit system used for exported values. |
createdAt |
string | ISO UTC creation timestamp. |
expiresAt |
string | null | Expiration time for cleanup. |
error |
string | null | Error detail when status is error. |
downloadUrl |
string | Present only when status is ready. |
GET /api/v1/exports/:exportId/download output:
| Item | Description |
|---|---|
| Body | Streamed CSV or JSON file bytes |
Content-Type |
text/csv or application/geo+json |
Content-Disposition |
Attachment filename with extension |
End-to-End Example (Trigger -> Poll -> Download):
# 1) Trigger export
TRIGGER=$(curl -sS -X POST "https://api.agmission.com/api/v1/jobs/12345/export" \
-H "X-API-Key: 3v8x2j9..." \
-H "Content-Type: application/json" \
-d '{"format":"csv","units":"metric","interval":5}')
echo "$TRIGGER"
# 2) Extract exportId (jq recommended)
EXPORT_ID=$(echo "$TRIGGER" | jq -r '.exportId')
# 3) Poll until ready
while true; do
STATUS_JSON=$(curl -sS "https://api.agmission.com/api/v1/exports/${EXPORT_ID}" \
-H "X-API-Key: 3v8x2j9...")
STATUS=$(echo "$STATUS_JSON" | jq -r '.status')
echo "status=${STATUS}"
if [ "$STATUS" = "ready" ]; then
break
fi
if [ "$STATUS" = "error" ]; then
echo "$STATUS_JSON"
exit 1
fi
sleep 5
done
# 4) Download file
curl -L "https://api.agmission.com/api/v1/exports/${EXPORT_ID}/download" \
-H "X-API-Key: 3v8x2j9..." \
-o export_job_12345.csv
Dedup Shortcut:
- If trigger response returns
reused: trueandstatus: "ready", skip polling and download immediately using returneddownloadUrl.
Operational Notes:
- Export files are temporary and expire by TTL (default 24h).
- Re-running the same request in dedup window may return the existing export instead of creating a new one.
- For full-fidelity parity checks, omit
intervalin export; use/records?interval=0for page-level comparisons.
Response (202 Accepted):
{
"exportId": "66f4a8c1...",
"status": "pending",
"format": "csv",
"units": "metric",
"createdAt": "2026-04-22T14:00:00Z"
}
Status Codes:
202— Export created and queued200— Existing export reused (deduplication — same job/format/units within 5 minutes):{ "exportId": "66f4a8c1...", "status": "ready", "format": "csv", "units": "metric", "createdAt": "2026-04-22T14:00:00Z", "reused": true, "downloadUrl": "/api/v1/exports/66f4a8c1.../download" }429— Rate limit exceeded (checkRetry-Afterheader)409— Invalid parameters
Deduplication: If you POST the same
jobId + format + unitswithin 5 minutes, the server returns the existing export (HTTP 200) instead of creating a new one. Whenreused: trueandstatus: "ready",downloadUrlis included immediately — skip polling.
5. Poll Export Status
Endpoint: GET /api/v1/exports/:exportId
Check generation progress.
Response (200 OK — Pending):
{
"exportId": "66f4a8c1...",
"status": "pending",
"format": "csv",
"units": "metric",
"createdAt": "2026-04-22T14:00:00Z",
"expiresAt": null
}
Response (200 OK — Ready):
{
"exportId": "66f4a8c1...",
"status": "ready",
"format": "csv",
"units": "metric",
"createdAt": "2026-04-22T14:00:00Z",
"expiresAt": "2026-04-23T14:00:00Z",
"downloadUrl": "/api/v1/exports/66f4a8c1.../download"
}
Response (200 OK — Error):
{
"exportId": "66f4a8c1...",
"status": "error",
"error": "Job has no app data to export",
"createdAt": "2026-04-22T14:00:00Z"
}
Polling Best Practice:
import time
import requests
def poll_export(export_id, api_key, max_wait_seconds=600):
start = time.time()
while time.time() - start < max_wait_seconds:
response = requests.get(
f'https://api.agmission.com/api/v1/exports/{export_id}',
headers={'X-API-Key': api_key}
)
data = response.json()
if data['status'] == 'ready':
return data['downloadUrl']
if data['status'] == 'error':
raise Exception(f"Export failed: {data.get('error')}")
# Exponential backoff: 1s, 2s, 4s, ...
time.sleep(min(2 ** (time.time() - start) / 10, 30))
raise TimeoutError('Export generation timeout')
6. Download Export
Endpoint: GET /api/v1/exports/:exportId/download
Stream the ready file.
Response (200 OK):
Content-Type: text/csv (or application/geo+json)
Content-Disposition: attachment; filename="export_job12345_66f4a8c1.csv"
[Binary file stream]
Examples:
# Download as file
curl -X GET "https://api.agmission.com/api/v1/exports/66f4a8c1.../download" \
-H "X-API-Key: 3v8x2j9kL4m5nQ6..." \
-o "export_$(date +%Y%m%d).csv"
# Python with requests
import requests
response = requests.get(
'https://api.agmission.com/api/v1/exports/66f4a8c1.../download',
headers={'X-API-Key': api_key},
stream=True
)
with open('export.csv', 'wb') as f:
for chunk in response.iter_content(8192):
f.write(chunk)
// JavaScript / Node.js
fetch('https://api.agmission.com/api/v1/exports/66f4a8c1.../download', {
headers: { 'X-API-Key': apiKey }
})
.then(r => r.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'export.csv';
a.click();
});
Rate Limiting
Rate limits are enforced per customer account to keep the API stable and fair for all users.
Current Limit
- 20 requests per minute per account (all Data Export API endpoints combined)
Response Headers
| Header | Meaning |
|---|---|
RateLimit-Limit: 20 |
Max requests per account per window |
RateLimit-Remaining: 18 |
Requests left in current window |
RateLimit-Reset: 1745353200 |
Unix timestamp of window reset |
Retry-After: 45 |
Seconds to wait before retrying (on 429) |
Behavior on Limit Exceeded
- HTTP status:
429 Too Many Requests - Respect
Retry-Afterbefore sending new requests - Keep retry logic idempotent (safe to re-run requests after wait)
Customer Integration Recommendations
- Use one shared request queue per account/API key to avoid bursts.
- Add exponential backoff with jitter for retries (especially on
429and transient5xx). - For bulk data, prefer async exports (
POST /export+ poll status) over large volumes of/recordscalls. - During polling, use a conservative interval (for example every 5-10 seconds).
- Cache stable responses (for example
/areas) where possible.
Practical Retry Flow
- If response is
200/202: continue normally. - If response is
429: waitRetry-Afterseconds, then retry. - If response is transient
5xx: retry with capped exponential backoff. - If response is
4xx(except429): treat as request issue and fix parameters/auth first.
Data Formats
CSV Export Columns
All CSV exports include one row per GPS point. Job/session metadata is repeated on every row so the file can be loaded directly into Power BI, Snowflake, or any data warehouse without a join.
Column headers include a unit suffix when units='us' (e.g. groundSpeed_mph instead of groundSpeed_ms).
Job/Session Metadata (repeated on every row, no join required):
| Metric column | US column | Description |
|---|---|---|
jobId |
same | Numeric job identifier. |
orderNumber |
same | Customer purchase order number. |
jobName |
same | Job name as entered by the applicator. |
clientId |
same | Client account ID (the applicator's customer). |
clientName |
same | Client account name. |
sessionId |
same | Flight session/file ID. |
fileName |
same | Original log file name. |
pilotName |
same | Pilot name as recorded in the data file. |
GPS columns:
| Metric column | US column | Unit (metric / US) | Description |
|---|---|---|---|
timeUtc |
same | ISO 8601 UTC | GPS timestamp. |
gpsTime |
same | epoch seconds | Raw GPS epoch time. |
lat |
same | decimal degrees | Latitude (WGS84). |
lon |
same | decimal degrees | Longitude (WGS84). |
utmX |
same | meters | UTM easting. |
utmY |
same | meters | UTM northing. |
alt_m |
alt_ft |
m / ft | Altitude. |
groundSpeed_ms |
groundSpeed_mph |
m/s / mph | Ground speed. |
heading |
same | degrees | Aircraft heading (0–360°). |
crossTrackError_m |
crossTrackError_ft |
m / ft | Cross-track deviation from guidance line. |
lockedLine |
same | — | Guidance line number. |
hdop |
same | — | Horizontal dilution of precision. |
satsIn |
same | — | Encoded satellite count with inside-area offset. 0..99 = outside area, satellite count is raw value. 100..199 = inside area, satellite count = value - 100. |
tslu |
same | seconds | Time since last GPS differential correction. |
calcodeFreq |
same | — | Raw calibration/frequency field. 30,000–60,000 = RPM (true RPM = value − 30,000). < 20,000 = positive spray offset (dm). > 60,000 = negative offset (65,536 − abs). |
sprayStat |
same | — | Spray state: 0 = off. 1 = on (inside area). 3 = on, first point of new spray line. 10 = on (outside area). Any non-zero value = boom open. |
Application data columns:
| Metric column | US column | Unit (metric / US) | Description |
|---|---|---|---|
flowRateApplied_Lmin |
flowRateApplied_galMin |
L/min / gal/min | Actual spray flow rate. |
flowRateRequired_Lmin |
flowRateRequired_galMin |
L/min / gal/min | Controller target flow rate. |
appRateRequired_Lha |
appRateRequired_galAc |
L/ha / gal/ac | Planned application rate. |
appRateApplied_Lha |
appRateApplied_galAc |
L/ha / gal/ac | Computed applied rate. Empty/null when spray is off (sprayStat = 0). |
swathWidth_m |
swathWidth_ft |
m / ft | Effective boom/swath width. |
boomPressure_psi |
same | PSI | Boom pressure (same in both unit systems). |
flowController |
same | — | Flow controller name; 'No FC' when absent. |
sprayOnLag_s |
same | seconds | Spray-on delay. Session constant. |
sprayOffLag_s |
same | seconds | Spray-off delay. Session constant. |
pulsesPerLiter |
same | — | Flow meter calibration constant. Session constant. |
rpm |
same | — | RPM array (JSON-serialised). Interpretation depends on material type. |
MET (weather) columns:
| Metric column | US column | Unit (metric / US) | Description |
|---|---|---|---|
windSpeed_kt |
windSpeed_mph |
knots / mph | Wind speed. |
windDir_deg |
same | degrees | Wind direction (0–360°). |
temp_c |
temp_f |
°C / °F | Air temperature. |
humidity_pct |
same | % | Relative humidity. |
FM columns (only when fm=true was set on the export trigger request):
| Column | Unit | Description |
|---|---|---|
sprayHeight_m |
meters | Target spray height (AgDisp). |
driftX_m |
meters | Lateral drift offset X (AgDisp). |
driftY_m |
meters | Lateral drift offset Y (AgDisp). |
depositX_m |
meters | Deposit offset X (AgDisp). |
depositY_m |
meters | Deposit offset Y (AgDisp). |
radarAlt_m |
meters | Radar altimeter reading. |
laserAlt_m |
meters | Laser altimeter reading. |
US unit conversion factors (applied at export time from canonical metric values):
| Metric field | US field | Conversion |
|---|---|---|
alt_m |
alt_ft |
× 3.28084 |
groundSpeed_ms |
groundSpeed_mph |
× 2.23694 |
crossTrackError_m |
crossTrackError_ft |
× 3.28084 |
swathWidth_m |
swathWidth_ft |
× 3.28084 |
flowRateApplied_Lmin |
flowRateApplied_galMin |
× 0.264172 |
flowRateRequired_Lmin |
flowRateRequired_galMin |
× 0.264172 |
appRateRequired_Lha |
appRateRequired_galAc |
× 0.10694 |
appRateApplied_Lha |
appRateApplied_galAc |
× 0.10694 |
windSpeed_kt |
windSpeed_mph |
× 1.15078 |
temp_c |
temp_f |
× 9/5 + 32 |
boomPressure_psi |
same | no conversion (already PSI) |
JSON Export Format
Array of record objects, one per GPS point. Each record includes all fields (job metadata, GPS, rates, MET, etc.) with appropriate unit conversions:
[
{
"jobId": 12345,
"sessionId": "507f1f77bcf86cd799439011",
"lat": 40.7128,
"lon": -74.0060,
"alt_m": 150.5,
"timeUtc": "2026-04-22T09:00:15Z",
"sprayStat": 1,
"grSpeed": 39.8,
"appRateApplied": 48.7,
"appRateRequired": 50,
"flowRateApplied": 45.3,
"flowRateRequired": 45.0,
"windSpeed_kt": 8.2,
"temp_c": 22.5
},
{
"jobId": 12345,
"sessionId": "507f1f77bcf86cd799439012",
"lat": 40.7129,
"lon": -74.0061,
"alt_m": 150.6,
"timeUtc": "2026-04-22T09:00:25Z",
"sprayStat": 1,
"grSpeed": 39.9,
"appRateApplied": 48.8,
"appRateRequired": 50,
"flowRateApplied": 45.4,
"flowRateRequired": 45.0,
"windSpeed_kt": 8.3,
"temp_c": 22.6
}
]
Use Cases
Use Case 1: Power BI Incremental Refresh
Goal: Update a Power BI dataset nightly with new GPS records.
Solution:
import requests
from datetime import datetime, timedelta
def sync_to_powerbi(job_id, api_key):
# Get sessions
sessions = requests.get(
f'https://api.agmission.com/api/v1/jobs/{job_id}/sessions',
headers={'X-API-Key': api_key}
).json()
for session in sessions['data']:
file_id = session['sessionId']
# Paginate records
cursor = None
records = []
while True:
params = {'limit': 2000}
if cursor:
params['startingAfter'] = cursor
page = requests.get(
f'https://api.agmission.com/api/v1/jobs/{job_id}/sessions/{file_id}/records',
params=params,
headers={'X-API-Key': api_key}
).json()
records.extend(page['data'])
if not page.get('hasMore'):
break
cursor = page.get('startingAfter')
# Push to Power BI (REST API or XMLA endpoint)
# ...
Use Case 2: ArcGIS Map Automation
Goal: Update ArcGIS Online layer with spray area boundaries.
const job_id = 12345;
const api_key = '3v8x2j9kL4m5nQ6...';
// Fetch areas
const areaResponse = await fetch(
`https://api.agmission.com/api/v1/jobs/${job_id}/areas`,
{ headers: { 'X-API-Key': api_key } }
);
const areas = await areaResponse.json();
// Convert to Feature Service format
const features = areas.features.map(feature => ({
geometry: feature.geometry,
attributes: {
name: feature.properties.name,
type: feature.properties.type,
area_ha: feature.properties.area_ha
}
}));
// Add to ArcGIS layer via REST API
const updateResponse = await fetch(
'https://services.arcgis.com/.../updates',
{
method: 'POST',
body: new URLSearchParams({ features: JSON.stringify(features), token: agolToken })
}
);
Use Case 3: Nightly Data Warehouse Load
Goal: Daily batch load all jobs' data into a data lake (S3, Snowflake, etc.).
#!/bin/bash
API_KEY="3v8x2j9kL4m5nQ6..."
JOBS=(12345 12346 12347)
S3_BUCKET="s3://company-spray-data"
DATE=$(date +%Y%m%d)
for job_id in "${JOBS[@]}"; do
echo "Exporting job $job_id..."
# Trigger export
export_id=$(curl -s -X POST "https://api.agmission.com/api/v1/jobs/${job_id}/export" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"format":"csv","units":"metric"}' \
| jq -r '.exportId')
# Poll until ready
while true; do
status=$(curl -s -X GET "https://api.agmission.com/api/v1/exports/${export_id}" \
-H "X-API-Key: ${API_KEY}" \
| jq -r '.status')
[ "$status" = "ready" ] && break
sleep 5
done
# Download and upload to S3
curl -s -X GET "https://api.agmission.com/api/v1/exports/${export_id}/download" \
-H "X-API-Key: ${API_KEY}" \
| aws s3 cp - "${S3_BUCKET}/spray_data/job${job_id}/data_${DATE}.csv"
echo "Completed: job $job_id → ${S3_BUCKET}/spray_data/job${job_id}/data_${DATE}.csv"
done
Error Handling
Error Response Format
All errors follow this structure:
{
"error": {
".tag": "error_constant"
}
}
Common HTTP Status Codes
| Code | Condition | Solution |
|---|---|---|
| 200 | Success | — |
| 202 | Export accepted (async) | Poll /exports/:exportId for completion |
| 400 | Bad request (invalid params) | Check endpoint docs for required fields |
| 401 | Invalid/missing API key | Verify X-API-Key header is present and valid |
| 404 | Resource not found | Check jobId, exportId, fileId exist and belong to your account |
| 409 | Conflict (e.g., invalid format) | Check format is "csv" or "json" |
| 429 | Rate limit exceeded | Wait Retry-After seconds, then retry with backoff |
| 500 | Server error | Retry with exponential backoff; contact support if persists |
Example: Handling 429 Rate Limit
import time
import requests
def request_with_backoff(url, api_key, max_retries=3):
for attempt in range(max_retries):
response = requests.get(
url,
headers={'X-API-Key': api_key}
)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
raise Exception("Max retries exceeded")
Support & SLAs
Support Channels
| Channel | Notes |
|---|---|
Email: support@agnav.com |
8:30am-4:30pm ET, Toronto, CA |
| Phone: 1-800-AGNAV-11 | 8:30am-4:30pm ET, Toronto, CA |
API SLA
- Availability: 99.5% monthly uptime
- Rate limit quota: 20 requests/min per account
- Export timeout: 1 hour max generation time
- File retention: 24 hours after ready
- Data accuracy: ±0.5% for area/volume calculations
API Versioning
Current version: v1
- Breaking changes will be announced 90 days in advance
- Deprecation warnings via response headers:
Deprecation: true - Version support policy: At least 3 versions maintained simultaneously
Appendix: Code Examples
cURL Examples
# List sessions
curl -X GET https://api.agmission.com/api/v1/jobs/12345/sessions \
-H "X-API-Key: 3v8x2j9kL4m5nQ6..." \
-H "Accept: application/json"
# Get records with thinning
curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?interval=5&limit=1000" \
-H "X-API-Key: 3v8x2j9kL4m5nQ6..."
# Get records without thinning (full-fidelity troubleshooting)
curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?limit=1000&interval=0" \
-H "X-API-Key: 3v8x2j9kL4m5nQ6..."
# Trigger CSV export
curl -X POST https://api.agmission.com/api/v1/jobs/12345/export \
-H "X-API-Key: 3v8x2j9kL4m5nQ6..." \
-H "Content-Type: application/json" \
-d '{"format":"csv","units":"metric"}'
JavaScript / Node.js
const apiKey = '3v8x2j9kL4m5nQ6...';
async function fetchSessions(jobId) {
const response = await fetch(`https://api.agmission.com/api/v1/jobs/${jobId}/sessions`, {
headers: { 'X-API-Key': apiKey }
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json();
}
async function exportAndDownload(jobId) {
// Trigger export
const exportRes = await fetch(`https://api.agmission.com/api/v1/jobs/${jobId}/export`, {
method: 'POST',
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({ format: 'csv', units: 'metric' })
});
const { exportId } = await exportRes.json();
// Poll for ready
let status = 'pending';
while (status !== 'ready') {
const statusRes = await fetch(`https://api.agmission.com/api/v1/exports/${exportId}`, {
headers: { 'X-API-Key': apiKey }
});
({ status } = await statusRes.json());
if (status !== 'ready') await new Promise(r => setTimeout(r, 5000));
}
// Download
return fetch(`https://api.agmission.com/api/v1/exports/${exportId}/download`, {
headers: { 'X-API-Key': apiKey }
});
}
Contact: AgMission Team - AG-NAV Inc.
Email: agm_admin@agnav.com or support@agnav.com
Last Updated: May 11, 2026
Next Review: December, 2026