agmission/server/docs/DATA_EXPORT_CUSTOMER_INTEGRATION_GUIDE.md

43 KiB
Raw Permalink Blame History

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

  1. Overview
  2. Quick Start
  3. Authentication
  4. API Endpoints
  5. Rate Limiting
  6. Data Formats
  7. Use Cases
  8. Error Handling
  9. 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

  1. 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/...
    
  2. Use HTTPS only — All API endpoints require HTTPS.

  3. Rotate keys quarterly — Implement key rotation in your automation

  4. 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, weather come 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 sessions
  • confirmedActualVolume: value entered in Report Settings when confirmed
  • effectiveVolume: confirmedActualVolume when useConfirmedVolume=true, otherwise actualSprayVolume
  • volumeUnit: follows job unit system and material type (lit/gal for liquid, kg/lb for 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 ID
  • fileId (path) — Session/file ID
  • startingAfter (query) — Cursor for pagination
  • limit (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 omitting interval).

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 page
  • endingBefore — pass as query param to get the previous page
  • hasMore: false + no startingAfter means 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 (0360°). 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,00060,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 (0360°). 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.

appRateApplied is null when spray is off (sprayStat = 0). All other values (1, 3, 10) are spray-on states and will carry a computed rate.

sprayStat = 3 is 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. Include sprayStat = 3 records when computing spray coverage; exclude sprayStat = 0 records only.

satsIn decoding: inside-area is encoded as +100, not bitmask. Parse with: inside = (satsIn >= 100) and satellites = 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 startingAfter to fetch only new records since last sync
  • Lightweight queries: Use interval=5 to reduce data volume by 5x
  • Parity testing: Use interval=0 for 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 hectares
  • appRateUnit — 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) — true to include Flight Master/AgDisp FM fields (sprayHeight_m, driftX_m, driftY_m, depositX_m, depositY_m, radarAlt_m, laserAlt_m). Default false. Only applicable for customers with FM-enabled equipment.

Bulk export interval behavior:

  • Records are exported in a stable, deterministic order per file.
  • If interval is omitted or null, all points are exported.
  • If interval is provided, points inside the same window are thinned out.
  • Records where sprayStat changes are never removed by thinning.
  • Thinning is applied independently per file stream (not across one global timeline).
  • Set interval=0 (or omit interval) 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: true and status: "ready", skip polling and download immediately using returned downloadUrl.

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 interval in export; use /records?interval=0 for 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 queued
  • 200 — 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 (check Retry-After header)
  • 409 — Invalid parameters

Deduplication: If you POST the same jobId + format + units within 5 minutes, the server returns the existing export (HTTP 200) instead of creating a new one. When reused: true and status: "ready", downloadUrl is 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-After before sending new requests
  • Keep retry logic idempotent (safe to re-run requests after wait)

Customer Integration Recommendations

  1. Use one shared request queue per account/API key to avoid bursts.
  2. Add exponential backoff with jitter for retries (especially on 429 and transient 5xx).
  3. For bulk data, prefer async exports (POST /export + poll status) over large volumes of /records calls.
  4. During polling, use a conservative interval (for example every 5-10 seconds).
  5. Cache stable responses (for example /areas) where possible.

Practical Retry Flow

  • If response is 200/202: continue normally.
  • If response is 429: wait Retry-After seconds, then retry.
  • If response is transient 5xx: retry with capped exponential backoff.
  • If response is 4xx (except 429): 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 (0360°).
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,00060,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 (0360°).
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