1333 lines
43 KiB
Markdown
1333 lines
43 KiB
Markdown
# 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
|
||
|
||
<!-- ### 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
|
||
|
||
```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
|
||
|