# 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](#overview) 2. [Quick Start](#quick-start) 3. [Authentication](#authentication) 4. [API Endpoints](#api-endpoints) 5. [Rate Limiting](#rate-limiting) 6. [Data Formats](#data-formats) 7. [Use Cases](#use-cases) 8. [Error Handling](#error-handling) 9. [Support & SLAs](#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 ```mermaid 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 ```bash 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**: ```json { "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 ```bash 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). ```mermaid 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! ```bash # ✅ 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 ```bash 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): ```json { "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 ```bash curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?limit=500&interval=5" \ -H "X-API-Key: 3v8x2j9..." ``` **Response** (200 OK): ```json { "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 (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. | > **`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**: ```bash # 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): ```json { "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 // 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. ```mermaid 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**: ```json { "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)**: ```bash # 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): ```json { "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): ```json { "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): ```json { "exportId": "66f4a8c1...", "status": "pending", "format": "csv", "units": "metric", "createdAt": "2026-04-22T14:00:00Z", "expiresAt": null } ``` **Response** (200 OK — Ready): ```json { "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): ```json { "exportId": "66f4a8c1...", "status": "error", "error": "Job has no app data to export", "createdAt": "2026-04-22T14:00:00Z" } ``` **Polling Best Practice**: ```python 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**: ```bash # 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 # 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 // 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 (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: ```json [ { "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**: ```python 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. ```javascript 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.). ```bash #!/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: ```json { "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 ```python 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 ```bash # 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 ```javascript 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