agmission/Development/server/docs/DATA_EXPORT_CUSTOMER_INTEGRATION_GUIDE.md
Devin Major df31b2080d
All checks were successful
Server Tests / Mocha – Unit & Utility Tests (push) Successful in 42s
-(#3013) Data Export - Implement Data Export API BE (Cont.)
+ Added public data export API enhancements, tests, and customer documentation
  + Extended /api/v1 data export endpoints with richer session, records, area, and async export output
  + Added confirmed/fallback report values, client metadata, mapped area, over-spray, volume/apprate (string) units, and weather blocks
  + Normalized flowController to "No FC" and align record field names with playback output
  + Converted record wind speed output to knots, add Fligh Mater only record/export fields behind fm=true, and persist fm on export jobs
  + Added export status/area constants, HTTP 202 support, route-level API docs, and per-account export rate limiting support
  + Added comprehensive endpoint, format, and verification test coverage plus test-suite README
  + Added customer-facing data export design, integration, rate-limit, and documentation index guides
  + Updated README/DLQ docs and related documentation links to current HTTPS dashboard paths
2026-04-24 09:05:55 -04:00

26 KiB

AgMission Data Export API — Customer Integration Guide

Audience: Technical integrators, BI teams, data warehouse engineers
Version: 1.0
Last Updated: April 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 GeoJSON 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]
    db[(MongoDB - Applications and GPS Trace)]

    ext -->|X-API-Key over HTTPS| gw
    gw --> sess
    gw --> recs
    gw --> areas
    gw --> exp
    gw --> stat
    gw --> dl
    sess --> db
    recs --> db
    areas --> db
    exp --> db
    stat --> db
    dl --> db

    style ext fill:#e3f2fd
    style gw fill:#f3e5f5
    style db fill:#e8f5e9

Quick Start

1. Get an API Key

Contact your AgMission account manager or self-serve at https://agmission.agnav.com/api-keys:

ak_test_3v8x2j9kL4m5nQ6...  (test key)
ak_live_7p2r9w4tY3h8k1...  (production key)

2. List sessions for a job

JOB_ID=12345
API_KEY="ak_test_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",
  "mappedArea_ha": 48.5,
  "reportConfirmed": false,
  "areaSize_ha": 48.5,
  "coverage_ha": 45.2,
  "overSprayedPct": -6.8,
  "appRate": 50,
  "appRateUnit": "lit/ha",
  "appRateConfirmed": null,
  "sprayVolume": 2260,
  "volumeUnit": "lit",
  "useActualVolume": false,
  "actualVolume": 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,
      "appRate": 50,
      "appRateUnit": "lit/ha",
      "matType": "wet",
      "flowController": "SatLoc G4",
      "sprayOnLag_s": 0.2,
      "sprayOffLag_s": 0.15,
      "pulsesPerLiter": 1800,
      "sprayZoneName": "Field A North",
      "sprayZoneArea_ha": 25.0,
      "files": [
        { "fileId": "507f1f77bcf86cd799439022", "name": "flight_20260422_001.log" }
      ],
      "sessionPilotName": "John Smith",
      "pilotId": "507f1f77bcf86cd799439033",
      "pilotName": "John Smith",
      "aircraftName": "AT-802F",
      "aircraftTailNumber": "N1234AT",
      "assignedDate": "2026-04-21T18:00:00Z",
      "reportConfirmed": false,
      "areaSize_ha": 48.5,
      "coverage_ha": 45.2,
      "appRateConfirmed": null,
      "sprayVolume": 2260,
      "volumeUnit": "lit",
      "useActualVolume": false,
      "actualVolume": null,
      "effectiveVolume": 2260
    }
  ]
}

3. Export to CSV

JOB_ID=12345
API_KEY="ak_test_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 DB as ApiKey Store

    Client->>API: GET /api/v1/jobs/123/sessions
    Note over Client,API: Header: X-API-Key: ak_live_abc123...
    API->>Auth: Verify key
    Auth->>DB: Lookup by prefix first 8 chars
    DB-->>Auth: Key candidate
    Auth->>Auth: bcrypt.compare key vs stored hash
    Auth-->>API: req.uid set to account owner
    API-->>Client: 200 JSON response

DO NOT use Authorization: Bearer ak_test_... — This will fail!

# ✅ CORRECT
curl -H "X-API-Key: ak_test_3v8x2j9kL4m5nQ6..." \
  https://api.agmission.com/api/v1/jobs/12345/sessions

# ❌ WRONG
curl -H "Authorization: Bearer ak_test_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 job or account (coming soon)
  • Revoke immediately if compromised

Security Best Practices

  1. Never commit keys to version control — Use environment variables or secrets manager

    export AGMISSION_API_KEY="ak_test_..."
    curl -H "X-API-Key: $AGMISSION_API_KEY" https://api.agmission.com/...
    
  2. Use HTTPS only — API endpoints enforce TLS 1.3+

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

  4. Monitor key usage — Check activity logs for suspicious patterns


API Endpoints

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,
  "mappedArea_ha": 48.5,
  "reportConfirmed": false,
  "areaSize_ha": 48.5,
  "coverage_ha": 45.2,
  "overSprayedPct": -6.8,
  "appRate": 50,
  "appRateUnit": "lit/ha",
  "appRateConfirmed": null,
  "sprayVolume": 2260,
  "volumeUnit": "lit",
  "useActualVolume": false,
  "actualVolume": 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,
      "appRate": 50,
      "appRateUnit": "lit/ha",
      "matType": "wet",
      "flowController": "SatLoc G4",
      "sprayOnLag_s": 0.2,
      "sprayOffLag_s": 0.15,
      "pulsesPerLiter": 1800,
      "sprayZoneName": "Field A North",
      "sprayZoneArea_ha": 25.0,
      "files": [
        { "fileId": "507f1f77bcf86cd799439022", "name": "flight_20260422_001.log" }
      ],
      "sessionPilotName": "John Smith",
      "pilotId": "507f1f77bcf86cd799439033",
      "pilotName": "John Smith",
      "aircraftName": "AT-802F",
      "aircraftTailNumber": "N1234AT",
      "assignedDate": "2026-04-21T18:00:00Z",
      "reportConfirmed": false,
      "areaSize_ha": 48.5,
      "coverage_ha": 45.2,
      "appRateConfirmed": null,
      "sprayVolume": 2260,
      "volumeUnit": "lit",
      "useActualVolume": false,
      "actualVolume": null,
      "effectiveVolume": 2260
    }
  ]
}

Confirmed vs Fallback Values:

When reportConfirmed: true, the applicator has manually confirmed spray records in Report Settings:

  • areaSize_ha, coverage_ha, appRate, actualVolume, weather come from the report
  • Otherwise, system-calculated fallbacks are used

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)

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: ak_test_..."

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

Pagination:

# Get next page
curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?startingAfter=507f191e810c19729de8605f" \
  -H "X-API-Key: ak_test_..."

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
  • Real-time dashboards: Long-poll this endpoint every 10 seconds

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 "geojson"
  • units (string, optional) — "metric" (default) or "us"
  • interval (number, optional) — GPS point thinning in seconds (float)
  • 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.

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: ak_test_..." \
  -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

See DATA_EXPORT_API_RATE_LIMITING.md for comprehensive rate limit documentation including examples and best practices.

Quick Reference:

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)

Data Formats

CSV Export Columns

All CSV exports include these columns (order may vary by unit system):

Job/Session Metadata (repeated per record):

  • jobId — Job ID
  • orderNumber — Customer PO number
  • jobName — Job name
  • clientId — Client account ID (the applicator's customer this job was done for)
  • clientName — Client account name
  • sessionId — Flight file ID
  • fileName — Log file name
  • pilotName — Pilot name

GPS Data:

  • timeUtc — ISO 8601 timestamp
  • lat — Latitude (decimal degrees)
  • lon — Longitude (decimal degrees)
  • alt_m (metric) / alt_ft (US) — Altitude
  • grSpeed_ms (metric) / groundSpeed_mph (US) — Ground speed

Application Data:

  • appRateApplied_Lha (metric) / appRateApplied_galAc (US) — Actual application rate
  • flowRateApplied_Lmin (metric) / flowRateApplied_galMin (US) — Spray system flow rate
  • swathWidth_m (metric) / swathWidth_ft (US) — Boom width

Environment:

  • windSpeed_kt (metric) / windSpeed_mph (US) — Wind speed (knots / mph)
  • windDir_deg — Wind direction (0-360°)
  • temp_c (metric) / temp_f (US) — Temperature
  • humidity_pct — Relative humidity

GeoJSON Export Format

Each point becomes a Feature with Point geometry:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [-74.0060, 40.7128, 150.5]
  },
  "properties": {
    "timeUtc": "2026-04-22T09:00:15Z",
    "sprayStat": 1,
    "grSpeed": 39.8
  }
}

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 = 'ak_test_...';

// 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="ak_live_..."
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",
    "message": "Human-readable details (dev mode only)"
  }
}

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 "geojson"
429 Rate limit exceeded Wait Retry-After seconds, see rate limit docs
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 Response Time
Email: support@agnav.com 4 hours (business hours)
Phone: 1-800-AGNAV-11 1 hour (9am-5pm ET)
Slack (Enterprise): Dedicated channel 1 hour

API SLA

  • Availability: 99.5% monthly uptime
  • Rate limit quota: 20 requests/min per account (configurable)
  • Export timeout: 1 hour max generation time
  • File retention: 24 hours after ready
  • Data accuracy: ±0.5% for area/volume calculations

Status & Maintenance

  • Status Page: https://status.agmission.com
  • Maintenance windows: Tuesdays 2-4 AM ET (announced 7 days prior)
  • Incident response: PagerDuty escalation, max 15-min response

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: ak_test_..." \
  -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: ak_test_..."

# Trigger CSV export
curl -X POST https://api.agmission.com/api/v1/jobs/12345/export \
  -H "X-API-Key: ak_test_..." \
  -H "Content-Type: application/json" \
  -d '{"format":"csv","units":"metric"}'

JavaScript / Node.js

const apiKey = 'ak_test_...';

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: technical-support@agnav.com
Last Updated: April 22, 2026
Next Review: October 22, 2026