# Alert Authentication Source: https://docs.withleaf.io/alerts/authentication Verify that incoming webhook requests are genuinely from Leaf by validating the HMAC SHA-256 signature in the X-Leaf-Signature header. Every webhook request from Leaf includes an `X-Leaf-Signature` header. This header contains a base64-encoded HMAC SHA-256 digest of the request body, signed with the secret you provided when creating the webhook. Verify this signature before processing any payload. ## How signature verification works 1. Read the raw request body as bytes. Do not parse or reformat it first. 2. Compute an HMAC SHA-256 digest of those bytes using your webhook secret as the key. 3. Base64-decode the value from the `X-Leaf-Signature` header. 4. Compare the two digests using a constant-time comparison function. The signed content is a compact JSON string without extra line breaks or spaces (other than spaces after `:` and `,`). ## Code examples ```python Python theme={null} import hmac import base64 import json def verify_leaf_signature(raw_body: bytes, secret: str, signature_header: str) -> bool: expected_sig = hmac.digest( msg=raw_body, key=secret.encode("utf-8"), digest="sha256" ) request_sig = base64.b64decode(signature_header) return hmac.compare_digest(expected_sig, request_sig) ``` ```java Java theme={null} import java.util.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; public boolean verifyLeafSignature(byte[] rawBody, String secret, byte[] signatureHeader) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256")); byte[] expected = mac.doFinal(rawBody); return MessageDigest.isEqual(expected, signatureHeader); } ``` ```javascript JavaScript theme={null} const crypto = require("crypto"); function verifyLeafSignature(rawBody, secret, signatureHeader) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest(); const received = Buffer.from(signatureHeader, "base64"); return crypto.timingSafeEqual(expected, received); } ``` ## Example payload If you receive a `fieldCreated` event, the signed body looks like: ```json theme={null} {"source": "REST", "leafUserId": "uuid", "fieldId": "uuid", "timestamp": "2024-06-15T14:30:00.000000Z", "type": "fieldCreated"} ``` Compute the HMAC of this exact byte string with your secret to get the expected signature. ## IP addresses Leaf uses cloud infrastructure and does not have a fixed range of IP addresses for webhook delivery. If your network architecture requires allow-listing, consider placing a load balancer or reverse proxy in a DMZ to receive webhook traffic and forward it to your internal systems. Using an `X-CompanyName-Signature` header for webhook verification is an industry-standard pattern also used by Twilio, Slack, and Stripe. ## What to do next * [Alerts Overview](/alerts/overview) for webhook setup and retry policy. * [Events reference](/alerts/events) for all event types and payload schemas. * [Alerts API Reference](/api-reference/alerts) for endpoint details. # Alert Events Source: https://docs.withleaf.io/alerts/events Reference for current Leaf webhook event names, with representative JSON payload examples for common event families. Every webhook payload includes a `type` field identifying the event and a `timestamp` in ISO-8601 format. This page is comprehensive for current event names from Leaf's `EventDto` enum. The payload bodies shown below are representative examples for each event family unless a producer-specific schema is separately verified. ## Event summary | Service | Events | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Credentials](#credentials-events) | `credentialsLimitedPermission`, `credentialsUnauthenticated` | | [Fields](#field-events) | `fieldCreated`, `fieldUpdated`, `mergedFieldCreated`, `mergedFieldUpdated` | | [Field Boundaries](#field-boundary-events) | `fieldBoundaryCreated`, `fieldBoundaryUpdated`, `fieldBoundaryDeleted` | | [Manual File Upload](#manual-file-upload-events) | `batchUploadProcessingFinished`, `batchUploadProcessingFailed`, `uploadedFileProcessingFinished`, `uploadedFileProcessingFailed` | | [Machine File Conversion](#machine-file-conversion-events) | `providerFileProcessingFinished`, `providerFileProcessingFailed` | | [Field Operations](#field-operation-events) | `mergedFileProcessingFinished`, `mergedFileProcessingFailed`, `automergedFileProcessingFinished`, `automergedFileProcessingFailed`, `operationCreated`, `operationUpdated`, `operationProcessingFinished`, `operationProcessingFailed` | | [Workflow](#workflow-events) | `workflowProcessingFinished`, `workflowProcessingFailed` | | [Crop Monitoring](#crop-monitoring-events) | `newSatelliteImage`, `satelliteSubscriptionFailed` | | [Irrigation](#irrigation-events) | `newIrrigationActivity`, `newFieldIrrigationActivity` | | [Assets (Beta)](#asset-events) | `machineCreated`, `machineUpdated`, `machineDeleted`, `implementCreated`, `implementUpdated`, `operatorCreated`, `operatorUpdated` | | [Provider Organizations](#provider-organization-events) | `providerOrganizationCreated`, `providerOrganizationBlocked`, `providerOrganizationRemoved` | *** ## Credentials events Current event names in this family: `credentialsLimitedPermission`, `credentialsUnauthenticated`. ### credentialsLimitedPermission Fired when provider credentials lack sufficient permissions for the requested actions. ```json theme={null} { "credential": "the client identification", "provider": "the provider name", "credentialId": "the credential id", "message": "message from the alert", "status": "the new status of the credential", "type": "credentialsLimitedPermission", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### credentialsUnauthenticated Fired when credentials are no longer valid (expired or revoked). ```json theme={null} { "credential": "the client identification", "provider": "the provider name", "credentialId": "the credential id", "message": "message from the alert", "status": "the new status of the credential", "type": "credentialsUnauthenticated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` *** ## Field events ### fieldCreated Fired when a new field is created in a connected provider account or directly in Leaf. ```json theme={null} { "source": "SYNC", "leafUserId": "uuid", "fieldId": "uuid", "type": "fieldCreated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### fieldUpdated Fired when a field is updated. ```json theme={null} { "source": "SYNC", "leafUserId": "uuid", "fieldId": "uuid", "type": "fieldUpdated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### mergedFieldCreated Fired when a new merged field is created. Requires `fieldsAutoSync` and `fieldsMergeIntersection` to be configured. ```json theme={null} { "source": "REST", "leafUserId": "uuid", "fieldId": "uuid", "type": "mergedFieldCreated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### mergedFieldUpdated Fired when a merged field is updated because one of its source field geometries changed. ```json theme={null} { "source": "REST", "leafUserId": "uuid", "fieldId": "uuid", "type": "mergedFieldUpdated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` *** ## Field boundary events ### fieldBoundaryCreated ```json theme={null} { "boundaryId": "uuid", "leafUserId": "uuid", "fieldId": "uuid", "type": "fieldBoundaryCreated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### fieldBoundaryUpdated ```json theme={null} { "boundaryId": "uuid", "leafUserId": "uuid", "fieldId": "uuid", "type": "fieldBoundaryUpdated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### fieldBoundaryDeleted Current event name in this family. Payloads follow the same general field-boundary pattern. *** ## Manual file upload events ### batchUploadProcessingFinished Fired when all machine files in a batch have been processed (converted or failed). Use the `batchId` to query the batch or batch status endpoints for results. ```json theme={null} { "batchId": "uuid", "leafUserId": "uuid", "type": "batchUploadProcessingFinished", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### batchUploadProcessingFailed Fired when a batch upload fails before any files are processed, most commonly when the uploaded archive contains no recognizable file formats. The payload includes a `message` field describing the failure reason. ```json theme={null} { "batchId": "uuid", "leafUserId": "uuid", "type": "batchUploadProcessingFailed", "message": "No operation discovered. Check file format before re-trying or contact support", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### uploadedFileProcessingFinished Fired when a manually uploaded machine file finishes all processing steps successfully. ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "type": "uploadedFileProcessingFinished", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### uploadedFileProcessingFailed Fired when a manually uploaded machine file finishes processing but failed one or more steps. ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "message": "details of what happened. May be empty", "type": "uploadedFileProcessingFailed", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` *** ## Machine file conversion events Current event names in this family: `providerFileProcessingFinished`, `providerFileProcessingFailed`. ### providerFileProcessingFinished Fired when a machine file pulled from a provider finishes processing successfully. ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "type": "providerFileProcessingFinished", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### providerFileProcessingFailed Fired when a provider machine file finishes processing but failed one or more steps. ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "message": "details of what happened. May be empty", "type": "providerFileProcessingFailed", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` *** ## Field operation events ### mergedFileProcessingFinished Fired when a merged field operation file finishes processing successfully. ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "type": "mergedFileProcessingFinished", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### mergedFileProcessingFailed ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "message": "details of what happened. May be empty", "type": "mergedFileProcessingFailed", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### automergedFileProcessingFinished Fired when an auto-merged field operation file finishes processing successfully. ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "type": "automergedFileProcessingFinished", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### automergedFileProcessingFailed ```json theme={null} { "fileId": "uuid", "leafUserId": "uuid", "message": "details of what happened. May be empty", "type": "automergedFileProcessingFailed", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### operationCreated Fired when a field operation is created. At this point, summary, images, and units may not yet be available. ```json theme={null} { "operationId": "uuid", "leafUserId": "uuid", "type": "operationCreated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### operationUpdated Fired when an existing field operation changes (e.g., new files merged in). ```json theme={null} { "operationId": "uuid", "leafUserId": "uuid", "type": "operationUpdated", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### operationProcessingFinished Fired when a field operation has fully finished processing. At this point, images, summary, and units are available. ```json theme={null} { "operationId": "uuid", "leafUserId": "uuid", "type": "operationProcessingFinished", "timestamp": "2024-06-15T14:30:00.000000Z" } ``` ### operationProcessingFailed Current event name in this family. Payloads follow the same general operation-processing pattern. *** ## Workflow events Current event names in this family: `workflowProcessingFinished`, `workflowProcessingFailed`. ### workflowProcessingFinished Current event name in this family. Payloads are not expanded here beyond enum coverage. ### workflowProcessingFailed Current event name in this family. Payloads are not expanded here beyond enum coverage. *** ## Crop monitoring events ### newSatelliteImage Fired when a new satellite image finishes processing for a monitored field. ```json theme={null} { "externalId": "the external id of the monitored field", "processId": "uuid", "type": "newSatelliteImage", "timestamp": "2024-06-15T14:30:00.000Z" } ``` ### satelliteSubscriptionFailed Fired when a field subscription fails on the satellite provider side. ```json theme={null} { "fieldId": "the external id of the monitored field", "message": "error message", "type": "satelliteSubscriptionFailed", "timestamp": "2024-06-15T14:30:00.000Z" } ``` *** ## Irrigation events ### newIrrigationActivity Fired when new as-applied irrigation data is available from a supported provider. ```json theme={null} { "irrigationId": "uuid", "leafUserId": "uuid", "type": "newIrrigationActivity", "timestamp": "2024-06-15T14:30:00.000Z" } ``` ### newFieldIrrigationActivity Fired when new irrigation data is available for a specific field. ```json theme={null} { "fieldIrrigationId": "uuid", "fieldId": "uuid", "leafUserId": "uuid", "type": "newFieldIrrigationActivity", "timestamp": "2024-06-15T14:30:00.000Z" } ``` *** ## Asset events Asset events are in beta. ### machineCreated ```json theme={null} { "type": "machineCreated", "machineId": "uuid", "leafUserId": "uuid", "timestamp": "2024-06-15T14:30:00.000Z" } ``` ### machineUpdated ```json theme={null} { "type": "machineUpdated", "machineId": "uuid", "leafUserId": "uuid", "timestamp": "2024-06-15T14:30:00.000Z" } ``` ### machineDeleted ```json theme={null} { "type": "machineDeleted", "machineId": "uuid", "leafUserId": "uuid", "timestamp": "2024-06-15T14:30:00.000Z" } ``` ### implementCreated ```json theme={null} { "type": "implementCreated", "implementId": "uuid", "leafUserId": "uuid", "timestamp": "2024-06-15T14:30:00.000Z" } ``` ### implementUpdated ```json theme={null} { "type": "implementUpdated", "implementId": "uuid", "leafUserId": "uuid", "timestamp": "2024-06-15T14:30:00.000Z" } ``` ### operatorCreated Current event name in this family. Payloads are not expanded here beyond enum coverage. ### operatorUpdated Current event name in this family. Payloads are not expanded here beyond enum coverage. *** ## Provider organization events Current event names in this family: `providerOrganizationCreated`, `providerOrganizationBlocked`, `providerOrganizationRemoved`. ### providerOrganizationCreated Current event name in this family. Payloads are not expanded here beyond enum coverage. ### providerOrganizationBlocked Current event name in this family. Payloads are not expanded here beyond enum coverage. ### providerOrganizationRemoved Current event name in this family. Payloads are not expanded here beyond enum coverage. ## What to do next * [Alerts Overview](/alerts/overview) for webhook setup and retry policy. * [Authentication](/alerts/authentication) for verifying webhook signatures. * [Alerts API Reference](/api-reference/alerts) for endpoint details. # Alerts Overview Source: https://docs.withleaf.io/alerts/overview Set up webhook-based alerts to receive real-time notifications when Leaf processes new data, credentials change, or satellite images become available. Leaf Alerts notify you via webhooks when something changes, so you don't have to poll the API for updates. Alerts cover events from both Leaf's own processing pipeline and supported third-party providers. ## How it works When you create a webhook, you provide three things: 1. A URL where Leaf sends HTTP POST requests. 2. A secret key Leaf uses to sign each request (HMAC SHA-256). 3. A list of event types you want to receive. When a matching event occurs, Leaf sends a JSON payload to your URL. Your server should return a 2xx status code to acknowledge receipt. After registration, Leaf immediately sends a confirmation message to your URL: ```json theme={null} { "message": "confirmation of webhook upon registration" } ``` If your server doesn't respond with a 2xx, Leaf retries at 1, 30, 60, and 240 minutes after the initial failure. You can check for missed deliveries using the failed calls endpoint. ## Testing your endpoint Once a webhook is registered, call `POST /webhooks/{id}/test` to have Leaf fire a signed sample message at your URL on demand, so you can exercise your receiving and signature-verification code without waiting for a real event. The sample payload has `"type": "test"` and is meant for transport and signature checks, not event-schema validation. It is signed with the same `X-Leaf-Signature` scheme as real alerts, so verification code you write against the test works unchanged for live events. See [Authentication](/alerts/authentication) for verification steps and [Test a webhook](/api-reference/alerts#test-a-webhook) for the endpoint details. You cannot update a webhook in place. To change the URL or events, delete the existing webhook and create a new one. Keep the old URL running until the new webhook is confirmed. ## Recommended events At a minimum, set up webhooks for: * **Field events** (`fieldCreated`, `fieldBoundaryCreated`, `fieldBoundaryUpdated`) to track boundary changes. * **Machine file events** (`providerFileProcessingFinished`, `uploadedFileProcessingFinished`) to know when new data is ready. * **Operation events** (`operationCreated`, `operationProcessingFinished`) to react when field operations are available. * **Credential events** (`credentialsUnauthenticated`, `credentialsLimitedPermission`) to catch broken provider connections early. The full list of events and their payload schemas is on the [Events](/alerts/events) page. ## Retry policy | Attempt | Delay after failure | | ------- | ------------------- | | 1 | 1 minute | | 2 | 30 minutes | | 3 | 60 minutes | | 4 | 240 minutes | After 4 failed attempts, the event is logged as a failed call. You can retrieve failed calls via `GET /webhooks/failed-calls`. ## Security Every request includes an `X-Leaf-Signature` header containing a base64-encoded HMAC SHA-256 digest of the request body, signed with your secret. Always verify this signature before processing the payload. See [Authentication](/alerts/authentication) for implementation details. Leaf runs on cloud infrastructure and does not have a fixed set of IP addresses for webhook delivery. If your network requires allow-listing, consider placing a reverse proxy or load balancer in a DMZ to receive webhooks and forward them internally. ## Common use cases * **React to new field operations**: Subscribe to `operationProcessingFinished` to trigger downstream workflows (yield analysis, report generation) as soon as data is ready. * **Monitor provider credentials**: Subscribe to `credentialsUnauthenticated` and `credentialsLimitedPermission` to alert your support team when a grower's connection breaks. * **Track boundary changes**: Subscribe to `fieldBoundaryCreated` and `fieldBoundaryUpdated` to keep your system in sync with provider-side field edits. * **Automate satellite ingestion**: Subscribe to `newSatelliteImage` to process NDVI or NDRE imagery as soon as each satellite pass is clipped. ## What to do next * [Events reference](/alerts/events) for all event types and payloads. * [Authentication](/alerts/authentication) for signature verification code examples. * [Alerts API Reference](/api-reference/alerts) for endpoint details. # Alerts API Source: https://docs.withleaf.io/api-reference/alerts Manage webhooks for real-time notifications about field boundary changes, file processing, field operations, credential expiration, and satellite images. Use these endpoints to create and manage webhooks that notify your application when field boundaries change, machine files finish processing, field operations are created, provider credentials expire, or satellite images are ready. For conceptual background, see [Alerts Overview](/alerts/overview). ## Base URL ``` https://api.withleaf.io/services/alerts/api/alerts ``` ## Endpoints | Description | Method | Path | | ------------------------------------- | ------------------- | ------------------------ | | [Create a webhook](#create-a-webhook) | POST | `/webhooks` | | [Test a webhook](#test-a-webhook) | POST | `/webhooks/{id}/test` | | [Get a webhook](#get-a-webhook) | GET | `/webhooks/{id}` | | [Get all webhooks](#get-all-webhooks) | GET | `/webhooks` | | [Get failed calls](#get-failed-calls) | GET | `/webhooks/failed-calls` | | [Delete a webhook](#delete-a-webhook) | DELETE | `/webhooks/{id}` | You cannot update a webhook. To change an existing webhook, delete it and create a new one. Keep the previous URL running until the new webhook is confirmed. On delivery failure, Leaf retries at **1, 30, 60, and 240 minutes** after the initial attempt. *** ## Available events You can subscribe to any combination of these events: **Credentials** | Event | Description | | ------------------------------ | ------------------------------------------------- | | `credentialsLimitedPermission` | Provider credentials have limited permissions. | | `credentialsUnauthenticated` | Provider credentials are no longer authenticated. | **Fields and boundaries** | Event | Description | | ---------------------- | ---------------------------- | | `fieldCreated` | A field is created. | | `fieldUpdated` | A field is updated. | | `fieldBoundaryCreated` | A field boundary is created. | | `fieldBoundaryUpdated` | A field boundary is updated. | | `fieldBoundaryDeleted` | A field boundary is deleted. | | `mergedFieldCreated` | A merged field is created. | | `mergedFieldUpdated` | A merged field is updated. | **Machine files** | Event | Description | | ---------------------------------- | ------------------------------------------------------------------------------- | | `uploadedFileProcessingFinished` | An uploaded machine file finishes processing. | | `uploadedFileProcessingFailed` | An uploaded machine file fails processing. | | `providerFileProcessingFinished` | A provider-synced machine file finishes processing. | | `providerFileProcessingFailed` | A provider-synced machine file fails processing. | | `mergedFileProcessingFinished` | A merged file finishes processing. | | `mergedFileProcessingFailed` | A merged file fails processing. | | `automergedFileProcessingFinished` | An auto-merged field operation file finishes processing. | | `automergedFileProcessingFailed` | An auto-merged field operation file fails processing. | | `batchUploadProcessingFinished` | All files in a batch upload have finished processing. | | `batchUploadProcessingFailed` | A batch upload fails before any files are processed (e.g., unsupported format). | **Field operations** | Event | Description | | ----------------------------- | -------------------------------------- | | `operationCreated` | A field operation is created. | | `operationUpdated` | A field operation is updated. | | `operationProcessingFinished` | A field operation finishes processing. | | `operationProcessingFailed` | A field operation fails processing. | **Satellite imagery** | Event | Description | | ----------------------------- | ----------------------------------- | | `newSatelliteImage` | A new satellite image is available. | | `satelliteSubscriptionFailed` | A satellite subscription fails. | **Assets (Beta)** | Event | Description | | ------------------ | ------------------------ | | `machineCreated` | A machine is created. | | `machineUpdated` | A machine is updated. | | `machineDeleted` | A machine is deleted. | | `implementCreated` | An implement is created. | | `implementUpdated` | An implement is updated. | | `operatorCreated` | An operator is created. | | `operatorUpdated` | An operator is updated. | **Workflows (Beta)** | Event | Description | | ---------------------------- | ------------------------------- | | `workflowProcessingFinished` | A workflow finishes processing. | | `workflowProcessingFailed` | A workflow fails processing. | **Irrigation** | Event | Description | | ---------------------------- | --------------------------------------------- | | `newIrrigationActivity` | A new irrigation activity is available. | | `newFieldIrrigationActivity` | A new field irrigation activity is available. | **Provider organizations** | Event | Description | | ----------------------------- | ----------------------------------- | | `providerOrganizationCreated` | A provider organization is created. | | `providerOrganizationBlocked` | A provider organization is blocked. | | `providerOrganizationRemoved` | A provider organization is removed. | You cannot register two webhooks that listen to the same event. Attempting to do so returns a `400` response with error `eventRegisteredTwice`. *** ## Create a webhook POST `/webhooks` Creates a webhook and begins delivering matching events immediately. ### Request body | Parameter | Type | Required | Description | | --------- | --------- | -------- | ------------------------------------------------------------------------- | | events | string\[] | Yes | Array of event names from the [available events](#available-events) list. | | name | string | Yes | Display name for the webhook. | | secret | string | Yes | Secret used for HMAC signature verification of payloads. | | url | string | Yes | A valid HTTP(S) URL where Leaf delivers event payloads. | ### Request ```bash cURL theme={null} curl -X POST \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -d '{ "events": ["fieldCreated", "operationCreated"], "name": "Field and operation listener", "secret": "your-hmac-secret", "url": "https://example.com/webhooks/leaf" }' \ 'https://api.withleaf.io/services/alerts/api/alerts/webhooks' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} payload = { "events": ["fieldCreated", "operationCreated"], "name": "Field and operation listener", "secret": "your-hmac-secret", "url": "https://example.com/webhooks/leaf", } response = requests.post( "https://api.withleaf.io/services/alerts/api/alerts/webhooks", headers=headers, json=payload, ) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const payload = { events: ["fieldCreated", "operationCreated"], name: "Field and operation listener", secret: "your-hmac-secret", url: "https://example.com/webhooks/leaf", }; axios .post("https://api.withleaf.io/services/alerts/api/alerts/webhooks", payload, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ### Response ```json theme={null} { "id": "uuid", "events": ["fieldCreated", "operationCreated"], "name": "Field and operation listener", "secret": "your-hmac-secret", "url": "https://example.com/webhooks/leaf" } ``` *** ## Test a webhook POST `/webhooks/{id}/test` Immediately delivers a signed sample message to the webhook's registered URL. Use it to exercise your receiving and signature-verification code end to end, on demand, without waiting for a real event. ### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | -------------------- | | id | string | path | Yes | UUID of the webhook. | No request body is required. ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/alerts/api/alerts/webhooks/2bcfa549-8ab4-4440-b9b5-6a5e1a0b62a7/test' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" WEBHOOK_ID = "2bcfa549-8ab4-4440-b9b5-6a5e1a0b62a7" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.post( f"https://api.withleaf.io/services/alerts/api/alerts/webhooks/{WEBHOOK_ID}/test", headers=headers, ) print(response.status_code) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const WEBHOOK_ID = "2bcfa549-8ab4-4440-b9b5-6a5e1a0b62a7"; const headers = { Authorization: `Bearer ${TOKEN}` }; axios .post( `https://api.withleaf.io/services/alerts/api/alerts/webhooks/${WEBHOOK_ID}/test`, null, { headers }, ) .then((res) => console.log(res.status)) .catch(console.error); ``` ### Delivered payload Leaf sends the following `POST` to your registered URL, with `Content-Type: application/json` and an `X-Leaf-Signature` header: ```json theme={null} {"type": "test", "timestamp": "2026-07-09T18:02:59.438203996Z", "webhookId": "uuid"} ``` This payload is a fixed shape with `"type": "test"`. Real events use their own `type` (for example, `batchUploadProcessingFinished`) and fields, so `/test` validates transport and signature verification, not the event schema. The secret and `X-Leaf-Signature` scheme are identical to production alerts, so verification code written against `/test` works unchanged once live events arrive. See [Authentication](/alerts/authentication) for the HMAC verification steps. ### Response Returns `200 OK` if your endpoint accepted the sample message. If your endpoint returns an error, the call responds with a status that reflects what your endpoint returned, which is useful for debugging delivery and signature handling. *** ## Get a webhook GET `/webhooks/{id}` Returns a single webhook by its ID. ### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | -------------------- | | id | string | path | Yes | UUID of the webhook. | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/alerts/api/alerts/webhooks/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} webhook_id = "WEBHOOK_UUID" response = requests.get( f"https://api.withleaf.io/services/alerts/api/alerts/webhooks/{webhook_id}", headers=headers, ) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const webhookId = "WEBHOOK_UUID"; axios .get(`https://api.withleaf.io/services/alerts/api/alerts/webhooks/${webhookId}`, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ### Response ```json theme={null} { "id": "uuid", "events": ["fieldCreated", "operationCreated"], "name": "Field and operation listener", "secret": "your-hmac-secret", "url": "https://example.com/webhooks/leaf" } ``` *** ## Get all webhooks GET `/webhooks` Returns all webhooks registered for the API owner. ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/alerts/api/alerts/webhooks' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get( "https://api.withleaf.io/services/alerts/api/alerts/webhooks", headers=headers, ) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; axios .get("https://api.withleaf.io/services/alerts/api/alerts/webhooks", { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ### Response ```json theme={null} [ { "id": "uuid", "events": ["fieldCreated", "operationCreated"], "name": "Field and operation listener", "secret": "your-hmac-secret", "url": "https://example.com/webhooks/leaf" } ] ``` *** ## Get failed calls GET `/webhooks/failed-calls` Returns a paginated list of failed webhook delivery attempts when any exist. Use the `nextPageToken` value from a response to fetch the next page. If no failed calls are available for the API owner, this endpoint returns `404`. ### Parameters | Parameter | Type | Location | Required | Description | | ------------- | ------ | -------- | -------- | ------------------------------------------------------------------ | | nextPageToken | string | query | No | Token returned in the previous response to retrieve the next page. | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/alerts/api/alerts/webhooks/failed-calls' # To fetch the next page: curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/alerts/api/alerts/webhooks/failed-calls?nextPageToken=TOKEN_VALUE' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get( "https://api.withleaf.io/services/alerts/api/alerts/webhooks/failed-calls", headers=headers, ) data = response.json() print(data) # Fetch next page if available next_token = data.get("nextPageToken") if next_token: response = requests.get( "https://api.withleaf.io/services/alerts/api/alerts/webhooks/failed-calls", headers=headers, params={"nextPageToken": next_token}, ) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; axios .get("https://api.withleaf.io/services/alerts/api/alerts/webhooks/failed-calls", { headers }) .then((res) => { console.log(res.data); // Use res.data.nextPageToken for the next page }) .catch(console.error); ``` ### Response ```json theme={null} { "items": [ { "apiOwner": "your-api-owner", "createdAt": "2026-01-15T12:16:30Z", "url": "https://example.com/webhooks/leaf", "status": 502, "response": "Bad Gateway", "requestBody": "{\"leafUserId\":\"uuid\",\"fileId\":\"uuid\",\"type\":\"automergedFileProcessingFinished\",\"timestamp\":\"2026-01-15T12:16:27Z\"}" }, { "apiOwner": "your-api-owner", "createdAt": "2026-01-15T14:10:05Z", "url": "https://example.com/webhooks/leaf", "connectionError": "ConnectionError: Remote end closed connection without response" } ], "nextPageToken": "eyJsYXN0..." } ``` *** ## Delete a webhook DELETE `/webhooks/{id}` Deletes a webhook. Returns `204 No Content` on success. Leaf stops delivering events for this webhook immediately. ### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | -------------------- | | id | string | path | Yes | UUID of the webhook. | ### Request ```bash cURL theme={null} curl -X DELETE \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/alerts/api/alerts/webhooks/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} webhook_id = "WEBHOOK_UUID" response = requests.delete( f"https://api.withleaf.io/services/alerts/api/alerts/webhooks/{webhook_id}", headers=headers, ) print(response.status_code) # 204 ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const webhookId = "WEBHOOK_UUID"; axios .delete(`https://api.withleaf.io/services/alerts/api/alerts/webhooks/${webhookId}`, { headers }) .then((res) => console.log(res.status)) // 204 .catch(console.error); ``` *** # Authentication Source: https://docs.withleaf.io/api-reference/authentication Authenticate with the Leaf API by exchanging your API owner credentials for a JWT token, then include it as a Bearer token on all subsequent requests. Leaf uses JWT (JSON Web Token) authentication. You exchange your API owner credentials for a token, then pass that token in the `Authorization` header of every request. For conceptual background -- multiple environments, token usage patterns -- see [Authentication](/getting-started/authentication). ## Base URL ``` https://api.withleaf.io/api ``` ## Endpoints | Action | Method | Path | | ----------- | ----------------- | --------------- | | Get a token | POST | `/authenticate` | *** ## Get a token `POST /authenticate` Exchanges API owner credentials for a JWT token. ### Request body | Field | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------------------------------- | | `username` | string | Yes | Your API owner email address. | | `password` | string | Yes | Your API owner password. | | `rememberMe` | string | No | `"true"` for a 30-day token, `"false"` for a 24-hour token. Defaults to `"false"`. | ### Token duration | `rememberMe` | Token duration | | ------------ | -------------- | | `"true"` | 30 days | | `"false"` | 24 hours | When a token expires, request a new one from the same endpoint. There is no refresh token flow. ### Request ```bash cURL theme={null} curl -X POST \ -H 'Content-Type: application/json' \ -d '{"username":"your-email@example.com","password":"your-password","rememberMe":"true"}' \ 'https://api.withleaf.io/api/authenticate' ``` ```python Python theme={null} import requests response = requests.post( "https://api.withleaf.io/api/authenticate", headers={"Content-Type": "application/json"}, json={ "username": "your-email@example.com", "password": "your-password", "rememberMe": "true" } ) token = response.json()["id_token"] ``` ```javascript JavaScript theme={null} const axios = require("axios"); axios.post("https://api.withleaf.io/api/authenticate", { username: "your-email@example.com", password: "your-password", rememberMe: "true", }) .then(({ data }) => { const token = data.id_token; console.log(token); }) .catch(console.error); ``` ### Response ```json theme={null} { "id_token": "eyJhbGciOi..." } ``` ### Using the token Include the token in the `Authorization` header of every API request: ``` Authorization: Bearer eyJhbGciOi... ``` ### Error responses | Status | Meaning | | ------------------ | ------------------------------------------------------------------------ | | `401 Unauthorized` | Credentials are invalid, or the token is missing, expired, or malformed. | # Beta Assets Source: https://docs.withleaf.io/api-reference/beta-assets Manage machines, implements, and operators for a Leaf user, including provider-synced and manually created assets from John Deere, CNHi, Stara, and Trimble. Use these endpoints to manage machines, implements, and operators for a Leaf user — whether synced from John Deere, CNHi, Stara, and Trimble or created manually through the API. For conceptual background, see [Assets](/beta/assets). The Assets API is currently in **beta**. Endpoints, request/response schemas, and behavior may change without notice. ## Base URL ``` https://api.withleaf.io/services/beta/api ``` ## Endpoints | Endpoint | Method | Path | | ----------------------------------------- | ------------------- | ------------------------------------------------ | | [Get all machines](#get-all-machines) | GET | `/users/{leafUserId}/machines` | | [Get a machine](#get-a-machine) | GET | `/users/{leafUserId}/machines/{machineId}` | | [Get machine files](#get-machine-files) | GET | `/users/{leafUserId}/machines/{machineId}/files` | | [Create a machine](#create-a-machine) | POST | `/users/{leafUserId}/machines` | | [Update a machine](#update-a-machine) | PATCH | `/users/{leafUserId}/machines/{machineId}` | | [Delete a machine](#delete-a-machine) | DELETE | `/users/{leafUserId}/machines/{machineId}` | | [Get all implements](#get-all-implements) | GET | `/users/{leafUserId}/implements` | | [Get an implement](#get-an-implement) | GET | `/users/{leafUserId}/implements/{implementId}` | | [Get all operators](#get-all-operators) | GET | `/users/{leafUserId}/operators` | | [Get an operator](#get-an-operator) | GET | `/users/{leafUserId}/operators/{operatorId}` | *** ## Machines ### Get all machines GET `/users/{leafUserId}/machines` Returns a paginated list of machines for a Leaf user. #### Parameters | Parameter | Type | Description | | ------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | string | Filter by machine name. | | `provider` | string | Filter by provider (`JohnDeere`, `Stara`, `CNHi`). | | `providerOrganizationId` | string | Filter by provider-side organization ID. | | `providerMachineId` | string | Filter by provider-side machine ID. | | `serialNumber` | string | Filter by machine serial number. | | `originType` | string | Origin of the machine: `USER_CREATED`, `FILE_POOLED`, or `PROVIDER_POOLED`. | | `createdTime` | string (ISO 8601) | Must match exactly the creation timestamp. | | `beforeCreatedTime` | string (ISO 8601) | Records created before this timestamp. | | `afterCreatedTime` | string (ISO 8601) | Records created after this timestamp. | | `updatedAt` | string (ISO 8601) | Must match exactly the last-updated timestamp. | | `beforeUpdatedAt` | string (ISO 8601) | Records updated before this timestamp. | | `afterUpdatedAt` | string (ISO 8601) | Records updated after this timestamp. | | `vin` | string | Filter by Vehicle Identification Number. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Sorting order. Valid fields: `id`, `leafUserId`, `name`, `provider`, `providerOrganizationId`, `providerMachineId`, `serialNumber`, `vin`, `model`, `make`, `category`, `modelYear`. Append `,asc` or `,desc`. | The default page size is 20 when `page` and `size` are not set. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "e89b1861-bdbb-49b9-8e11-74840f7e1ea8", "leafUserId": "faa6691a-7bf7-49c2-8934-b5b4c823aef8", "name": "TestName", "provider": "Leaf", "providerMachineId": "08790ae9-d451-4158-9920-09d1ab1ba5e6", "providerOrganizationId": "123456", "originType": "PROVIDER_POOLED", "createdTime": "2022-02-22T20:06:25.411Z", "serialNumber": "123456", "vin": "1234567890ABC", "model": "ModelName", "make": "MakerOfMachine", "category": "Sprayer", "modelYear": 2000 }, { "id": "82725746-3150-490d-9f3f-a47151ac0669", "leafUserId": "325f5ac0-6c57-4b4a-bdea-490ccddb06c4", "name": "nameTest", "provider": "Leaf", "providerMachineId": "75f362b4-8f61-46f9-905b-a357fb239930", "providerOrganizationId": "654321", "originType": "FILE_POOLED", "createdTime": "2022-02-22T20:06:25.411Z", "serialNumber": "123456", "vin": "1234567890ABC", "model": "ModelName", "make": "MakerOfMachine", "category": "Harvester", "modelYear": 2020 } ] ``` *** ### Get a machine GET `/users/{leafUserId}/machines/{machineId}` Returns details for a single machine by its ID. #### Parameters | Parameter | Type | Description | | ------------ | ------------- | ------------------------------ | | `leafUserId` | string (UUID) | Path param — the Leaf user ID. | | `machineId` | string (UUID) | Path param — the machine ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "e89b1861-bdbb-49b9-8e11-74840f7e1ea8", "leafUserId": "faa6691a-7bf7-49c2-8934-b5b4c823aef8", "name": "TestName", "provider": "Leaf", "providerMachineId": "08790ae9-d451-4158-9920-09d1ab1ba5e6", "providerOrganizationId": "123456", "originType": "PROVIDER_POOLED", "createdTime": "2022-02-22T20:06:25.411Z", "serialNumber": "123456", "vin": "1234567890ABC", "model": "ModelName", "make": "MakerOfMachine", "category": "Sprayer", "modelYear": 2000 } ``` *** ### Get machine files GET `/users/{leafUserId}/machines/{machineId}/files` Returns operation files associated with a specific machine. #### Parameters | Parameter | Type | Description | | -------------------------- | ----------------- | --------------------------------------------------------------------------------------------------- | | `leafFileId` | string (UUID) | Filter by Leaf file ID. | | `originType` | string | `USER_CREATED`, `FILE_POOLED`, or `PROVIDER_POOLED`. | | `createdTime` | string (ISO 8601) | Must match exactly the creation timestamp. | | `beforeCreatedTime` | string (ISO 8601) | Records created before this timestamp. | | `afterCreatedTime` | string (ISO 8601) | Records created after this timestamp. | | `startTime` | string (ISO 8601) | Must match exactly the operation start time. | | `endTime` | string (ISO 8601) | Must match exactly the operation end time. | | `beforeStartTime` | string (ISO 8601) | Operations that started before this timestamp. | | `afterStartTime` | string (ISO 8601) | Operations that started after this timestamp. | | `beforeEndTime` | string (ISO 8601) | Operations that ended before this timestamp. | | `afterEndTime` | string (ISO 8601) | Operations that ended after this timestamp. | | `distanceValue` | double | Exact distance value. | | `greaterThanDistanceValue` | double | Distance greater than this value. | | `lessThanDistanceValue` | double | Distance less than this value. | | `distanceUnit` | string | `Mile`, `mile`, `Feet`, or `ft`. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Valid fields: `id`, `leafUserId`, `machineId`, `startTime`, `endTime`, `createdTime`, `leafFileId`. | The default page size is 20 when `page` and `size` are not set. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}/files' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}/files" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}/files' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "e7916d05-97ae-404a-a467-e2512c202a2f", "leafFileId": "e0e18a6f-4f88-4801-96e2-f39143f260e1", "machineId": "98b244fc-7b2d-4acf-a51a-58d20ae27355", "startTime": "2022-02-22T20:06:25.411Z", "endTime": "2022-02-22T20:07:25.411Z", "distance": { "value": 4152.255, "unit": "ft" }, "fuelConsumption": { "value": 28.89, "unit": "US gal" } } ] ``` *** ### Create a machine POST `/users/{leafUserId}/machines` Creates a new machine for a Leaf user. #### Request body | Field | Type | Description | | -------------- | ------- | ----------------------------------------------- | | `name` | string | Machine name. | | `serialNumber` | string | Serial number. | | `vin` | string | Vehicle Identification Number. | | `model` | string | Model name. | | `make` | string | Manufacturer. | | `category` | string | Machine category (e.g. `Harvester`, `Sprayer`). | | `modelYear` | integer | Model year. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "nameTest", "serialNumber": "123456", "vin": "1234567890ABC", "model": "ModelName", "make": "MakerOfMachine", "category": "Harvester", "modelYear": 2020 }' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "name": "nameTest", "serialNumber": "123456", "vin": "1234567890ABC", "model": "ModelName", "make": "MakerOfMachine", "category": "Harvester", "modelYear": 2020 } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'nameTest', serialNumber: '123456', vin: '1234567890ABC', model: 'ModelName', make: 'MakerOfMachine', category: 'Harvester', modelYear: 2020 } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "d5efe8a1-98be-40db-b2b2-2da332e8f69c", "name": "nameTest", "provider": "Leaf", "providerMachineId": "24de86ff-e6f0-4f8f-b429-0571c30a7ddf", "providerOrganizationId": "654321", "leafUserId": "9e081c9d-6185-49e1-8df7-7788d6aa1211", "originType": "USER_CREATED", "createdTime": "2023-06-12T17:38:09.148Z", "serialNumber": "123456", "vin": "1234567890ABC", "model": "ModelName", "make": "MakerOfMachine", "category": "Harvester", "modelYear": 2020 } ``` *** ### Update a machine PATCH `/users/{leafUserId}/machines/{machineId}` Updates an existing machine for a Leaf user. Only machines with `originType` of `USER_CREATED` can be updated. Machine data obtained from providers cannot be modified. #### Request body | Field | Type | Description | | -------------- | ------- | ---------------------- | | `name` | string | Updated machine name. | | `serialNumber` | string | Updated serial number. | | `vin` | string | Updated VIN. | | `model` | string | Updated model name. | | `make` | string | Updated manufacturer. | | `category` | string | Updated category. | | `modelYear` | integer | Updated model year. | #### Request ```bash cURL theme={null} curl -X PATCH \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "updatedName", "serialNumber": "000123", "vin": "1234567890EFR", "model": "ModelNameUpdated", "make": "MakerOfMachineUpdated", "category": "Planted", "modelYear": 2021 }' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "name": "updatedName", "serialNumber": "000123", "vin": "1234567890EFR", "model": "ModelNameUpdated", "make": "MakerOfMachineUpdated", "category": "Planted", "modelYear": 2021 } response = requests.patch(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'updatedName', serialNumber: '000123', vin: '1234567890EFR', model: 'ModelNameUpdated', make: 'MakerOfMachineUpdated', category: 'Planted', modelYear: 2021 } axios.patch(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "d5efe8a1-98be-40db-b2b2-2da332e8f69c", "name": "updatedName", "provider": "Leaf", "providerMachineId": "24de86ff-e6f0-4f8f-b429-0571c30a7ddf", "providerOrganizationId": "654321", "leafUserId": "9e081c9d-6185-49e1-8df7-7788d6aa1211", "originType": "USER_CREATED", "createdTime": "2023-06-12T17:38:09.148Z", "serialNumber": "000123", "vin": "1234567890EFR", "model": "ModelNameUpdated", "make": "MakerOfMachineUpdated", "category": "Planted", "modelYear": 2021 } ``` *** ### Delete a machine DELETE `/users/{leafUserId}/machines/{machineId}` Deletes a machine by its ID. Only machines with `originType` of `USER_CREATED` can be deleted. Machine data obtained from providers cannot be removed. #### Parameters | Parameter | Type | Description | | ------------ | ------------- | ------------------------------ | | `leafUserId` | string (UUID) | Path param — the Leaf user ID. | | `machineId` | string (UUID) | Path param — the machine ID. | #### Request ```bash cURL theme={null} curl -X DELETE \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.delete(endpoint, headers=headers) print(response.status_code) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/machines/{machineId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.delete(endpoint, { headers }) .then(res => console.log(res.status)) .catch(console.error) ``` *** ## Implements ### Get all implements GET `/users/{leafUserId}/implements` Returns a paginated list of implements for a Leaf user. Currently supports John Deere and Trimble implements. #### Parameters | Parameter | Type | Description | | ------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user ID. | | `provider` | string | Filter by provider (`JohnDeere`, `Trimble`). | | `providerImplementId` | string | Filter by provider-side implement ID. | | `providerOrganizationId` | string | Filter by provider-side organization ID. | | `name` | string | Filter by implement name. | | `createdTime` | string (ISO 8601) | Returns records from the given creation time onward. | | `beforeCreatedTime` | string (ISO 8601) | Records created before this timestamp. | | `afterCreatedTime` | string (ISO 8601) | Records created after this timestamp. | | `updatedTime` | string (ISO 8601) | Returns records from the given update time onward. | | `beforeUpdatedTime` | string (ISO 8601) | Records updated before this timestamp. | | `afterUpdatedTime` | string (ISO 8601) | Records updated after this timestamp. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Valid fields: `id`, `leafUserId`, `name`, `provider`, `providerOrganizationId`, `providerImplementId`, `serialNumber`, `model`, `make`, `category`. | The default page size is 20 when `page` and `size` are not set. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/implements' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/implements" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/implements' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "38d313fc-e4ce-442b-9147-f469b30aedab", "name": "c3po_implement", "provider": "JohnDeere", "providerImplementId": "110237", "providerOrganizationId": "296264", "leafUserId": "mbba54fb-3710-4f7d-9aaf-703107930193", "originType": "PROVIDER_POOLED", "serialNumber": "00000", "model": "StMax150", "make": "JOHN DEERE", "category": "Cotton Harvester Implement" } ] ``` *** ### Get an implement GET `/users/{leafUserId}/implements/{implementId}` Returns details for a single implement by its ID. #### Parameters | Parameter | Type | Description | | ------------- | ------------- | ------------------------------ | | `leafUserId` | string (UUID) | Path param — the Leaf user ID. | | `implementId` | string (UUID) | Path param — the implement ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/implements/{implementId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/implements/{implementId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/implements/{implementId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "38d313fc-e4ce-442b-9147-f469b30aedab", "name": "c3po_implement", "provider": "JohnDeere", "providerImplementId": "110237", "providerOrganizationId": "296264", "leafUserId": "mbba54fb-3710-4f7d-9aaf-703107930193", "originType": "PROVIDER_POOLED", "serialNumber": "00000", "model": "StMax150", "make": "JOHN DEERE", "category": "Cotton Harvester Implement" } ``` *** ## Operators ### Get all operators GET `/users/{leafUserId}/operators` Returns a paginated list of operators for a Leaf user. Currently supports John Deere operators. #### Parameters | Parameter | Type | Description | | --------- | ------- | ------------------------------------ | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | The default page size is 20 when `page` and `size` are not set. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/operators' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/operators" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/operators' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "aa8c917bc-7e9b-47bc-99b8-4a0df818ab07", "name": "Brian O.", "provider": "JohnDeere", "providerOperatorId": "bbd3a3e8-5ac3-4ab8-4619-d582da4568cc", "providerOrganizationId": "9999", "originType": "PROVIDER_POOLED", "license": null, "updatedTime": "2023-10-10T10:10:10.000Z", "status": "ACTIVE" } ] ``` *** ### Get an operator GET `/users/{leafUserId}/operators/{operatorId}` Returns details for a single operator by its ID. #### Parameters | Parameter | Type | Description | | ------------ | ------------- | ------------------------------ | | `leafUserId` | string (UUID) | Path param — the Leaf user ID. | | `operatorId` | string (UUID) | Path param — the operator ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/operators/{operatorId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/operators/{operatorId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/operators/{operatorId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "aa8c917bc-7e9b-47bc-99b8-4a0df818ab07", "name": "Brian O.", "provider": "JohnDeere", "providerOperatorId": "bbd3a3e8-5ac3-4ab8-4619-d582da4568cc", "providerOrganizationId": "9999", "originType": "PROVIDER_POOLED", "license": null, "updatedTime": "2023-10-10T10:10:10.000Z", "status": "ACTIVE" } ``` # Beta Input Source: https://docs.withleaf.io/api-reference/beta-input Look up products, varieties, and tank mixes from provider databases like John Deere, Agrian, and CDMS. Match and validate inputs for field operations. Use these endpoints to look up agricultural products, varieties, and tank mixes from standardized databases and to match product names from machine files against known products. For conceptual background, see [Input Validator](/beta/input-validator). The Input API is currently in **beta**. Endpoints, request/response schemas, and behavior may change without notice. ## Base URL ``` https://api.withleaf.io/services/beta/api ``` ## Endpoints ### Products | Endpoint | Method | Path | | ----------------------------------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------- | | [Get all products](#get-all-products) | GET | `/products` | | [Get summarized products](#get-summarized-products) | GET | `/users/{leafUserId}/products/summary` | | [Search for products](#search-for-products) | GET | `/products/search` | | [Get a product](#get-a-product) | GET | `/products/{id}` | | [Get matching products from an operation](#get-matching-products-from-an-operation) | GET | `/products/matching/operations/{operationId}` | | [Update product matches](#update-product-matches) | PATCH | `/products/matching/operations/{operationId}/matches/{matchId}` | | [Get product matches historical](#get-product-matches-historical) | GET | `/products/matching/operations/{operationId}/matches/{matchId}/historical` | ### Varieties | Endpoint | Method | Path | | ----------------------------------------------------- | ---------------- | --------------------------------------- | | [Get all varieties](#get-all-varieties) | GET | `/varieties` | | [Get a variety](#get-a-variety) | GET | `/varieties/{id}` | | [Get summarized varieties](#get-summarized-varieties) | GET | `/users/{leafUserId}/varieties/summary` | | [Search for varieties](#search-for-varieties) | GET | `/varieties/search` | ### Tank Mixes | Endpoint | Method | Path | | ----------------------------------------------- | ---------------- | ------------------- | | [Get all tank mixes](#get-all-tank-mixes) | GET | `/tankMixes` | | [Search for tank mixes](#search-for-tank-mixes) | GET | `/tankMixes/search` | *** ## Products ### Get all products GET `/products` Returns a paginated list of products from providers at the Leaf user level. Currently supports John Deere products. This endpoint does **not** include products from label databases such as Agrian and CDMS — use [Search for products](#search-for-products) for those. #### Parameters | Parameter | Type | Description | | ------------ | ------------- | ----------------------------------------------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user. | | `provider` | string | Filter by provider (`JohnDeere`). | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (max `100`). | | `sort` | string | Sorting order. Comma-separated fields with optional `,asc` or `,desc` suffix. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/products?leafUserId={leafUserId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/products" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "{leafUserId}"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/products' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { leafUserId: '{leafUserId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "07b3f270-8af8-11ee-b9d1-0242ac120002", "name": "Propiconazole", "registrant": "Tide International USA, Inc.", "productType": "Chemical", "labelProvider": "JohnDeere", "providerId": "020c55f6-8af8-11ee-b9d1-0242ac120002", "formulationType": "DRY", "leafUserId": "fb6fcda4-8af7-11ee-b9d1-0242ac120002", "registration": "0084229-00011-AA-0000000", "status": "ACTIVE", "carrier": true } ] ``` *** ### Get summarized products GET `/users/{leafUserId}/products/summary` Returns a summarized list of products extracted from machine files for a Leaf user. #### Parameters | Parameter | Type | Description | | --------- | ------- | ----------------------------------------------------------------------------- | | `name` | string | Filter by partial product name. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (max `100`). | | `sort` | string | Sorting order. Comma-separated fields with optional `,asc` or `,desc` suffix. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/products/summary' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/products/summary" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/products/summary' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Default product", "leafUserId": "8bbe50a0-992c-11ee-b9d1-0242ac120002" } ] ``` *** ### Search for products GET `/products/search` Searches for products by name (partial values supported). Includes results from Agrian and CDMS databases available to everyone, plus John Deere products at the Leaf user level. #### Parameters | Parameter | Type | Description | | ------------ | ------- | ---------------------------------------------------- | | `name` | string | **Required.** Partial product name to search. | | `maxResults` | integer | Maximum results to return (default `10`, max `100`). | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/products/search?name=Roundup' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/products/search" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"name": "Roundup"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/products/search' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { name: 'Roundup' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "7d85c556-0ac5-4f0b-b7cc-b35ac559af8f", "name": "CompostX", "registration": "00000-00-00000", "registrant": "Leaf Company, LLC", "productType": "Dry", "physicalState": "dry", "formulationType": "Dry Flowable", "labelProvider": "CDMS", "productPageUrl": "https://www.cdms.net/ldat/", "labels": [ { "name": "meE2000.pdf", "url": "https://www.cdms.net/ldat/meE2000.pdf" } ], "activeIngredient": [ "Urea", "Calcium" ] }, { "id": "21f4cb76-07fa-46ca-a8cf-cfbc2f161bdf", "name": "Product AB", "registration": "0434785-2911", "registrant": "Loveland Products, Inc.", "productType": "Insecticide Miticide", "formulationType": "Emulsifiable Concentrate", "labelProvider": "AGRIAN", "productPageUrl": "https://www.agrian.com/labelcenter/results.cfm?d=0000", "labels": [ { "name": "Label", "url": "https://www.agrian.com/pdfs/new/00000.pdf" } ], "activeIngredient": [ "13.1 - Lambda-cyhalothrin" ], "physicalState": "liquid", "density": { "value": "7.17", "unit": "lb/ga" }, "activeIngredients": [ { "name": "Lambda-cyhalothrin", "value": "13.1", "unit": "%" } ] } ] ``` *** ### Get a product GET `/products/{id}` Returns a single product by its ID. Data is sourced from multiple product databases. #### Parameters | Parameter | Type | Description | | --------- | ------------- | ---------------------------- | | `id` | string (UUID) | Path param — the product ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/products/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/products/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/products/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "81ffe756-1fd0-4d97-b2ec-e33b5232f507", "name": "GameOn", "registration": "62719-724", "registrant": "Corteva Agriscience", "productType": "Dry", "formulationType": "Dry Flowable", "labelProvider": "AGRIAN", "productPageUrl": "https://www.agrian.com/labelcenter/results.cfm?d=21666", "labels": [ { "name": "Label - 03-R0718", "url": "https://www.agrian.com/pdfs/current/Badge_X2_FungicideBactericide_Label1p.pdf" } ] } ``` *** ### Get matching products from an operation GET `/products/matching/operations/{operationId}` Returns the standard products that best match the products found in a field operation. Use the returned `productId` with the [Get a product](#get-a-product) endpoint to retrieve registration numbers and labels. #### Parameters | Parameter | Type | Description | | ------------- | ------------- | ------------------------------ | | `operationId` | string (UUID) | Path param — the operation ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "37159c45-4c1f-48e4-aa87-90b34cc6e789", "name": "ams", "productId": "e5b91778-0714-4e1f-850c-b458d1bdc7ed", "matchDetails": { "status": "PREDICTED", "score": 14.354036 } }, { "id": "7fb70242-498b-42c1-92c3-a7d2361d2125", "name": "counter", "productId": "a85c1d0d-b673-46aa-a3a3-31cb65f57598", "matchDetails": { "status": "VALIDATED" } } ] ``` *** ### Update product matches PATCH `/products/matching/operations/{operationId}/matches/{matchId}` Approves or updates a product match prediction. Send `"status": "VALIDATED"` to approve the current prediction, or send a new `productId` to change the matched product. #### Parameters | Parameter | Type | Description | | ------------- | ------------- | ------------------------------ | | `operationId` | string (UUID) | Path param — the operation ID. | | `matchId` | string (UUID) | Path param — the match ID. | #### Request body To approve the prediction: ```json theme={null} { "status": "VALIDATED" } ``` To change the matched product: ```json theme={null} { "productId": "expectedProductID" } ``` #### Request ```bash cURL theme={null} curl -X PATCH \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "status": "VALIDATED" }' \ 'https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}/matches/{matchId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}/matches/{matchId}" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"status": "VALIDATED"} response = requests.patch(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}/matches/{matchId}' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { status: 'VALIDATED' } axios.patch(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "uidd-match-0001", "name": "Ta35", "productId": "uidd-prd-1001", "matchDetails": { "score": 91, "status": "PREDICTED" } }, { "id": "uidd-match-0002", "name": "Talisman", "productId": "uidd-prd-1003", "matchDetails": { "status": "VALIDATED" } } ] ``` *** ### Get product matches historical GET `/products/matching/operations/{operationId}/matches/{matchId}/historical` Returns the change history for a product match. #### Parameters | Parameter | Type | Description | | ------------- | ------------- | ------------------------------ | | `operationId` | string (UUID) | Path param — the operation ID. | | `matchId` | string (UUID) | Path param — the match ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}/matches/{matchId}/historical' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}/matches/{matchId}/historical" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/products/matching/operations/{operationId}/matches/{matchId}/historical' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "matchId": "uidd-match-0002", "name": "Talisman", "productId": "uidd-prd-1003", "matchDetails": { "status": "PREDICTED", "score": 8.225217 }, "historicalTime": "2023-12-19T13:18:44.709Z" } ] ``` *** ## Varieties ### Get all varieties GET `/varieties` Returns a paginated list of varieties from providers. Currently supports John Deere. #### Parameters | Parameter | Type | Description | | ------------ | ------------- | ----------------------------------------------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user. | | `provider` | string | Filter by provider (`JohnDeere`). | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (max `100`). | | `sort` | string | Sorting order. Comma-separated fields with optional `,asc` or `,desc` suffix. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/varieties?leafUserId={leafUserId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/varieties" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "{leafUserId}"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/varieties' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { leafUserId: '{leafUserId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "crops": ["ALFALFA"], "name": "Master Piece", "companyName": "Simplot", "status": "ACTIVE", "leafUserId": "028c30fa-6d2a-11ee-b962-0242ac120002", "provider": "JohnDeere", "providerId": "8e1e0920-1265-4066-8067-8ce2ce5012b2", "organizationId": "9999" } ] ``` *** ### Get a variety GET `/varieties/{id}` Returns a single variety by its ID. #### Parameters | Parameter | Type | Description | | --------- | ------------- | ---------------------------- | | `id` | string (UUID) | Path param — the variety ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/varieties/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/varieties/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/varieties/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "111120cc-d0c5-40d3-a063-ca09903a0738", "crops": ["SOYBEANS"], "providerId": "f4c43d25-0000-1000-7fc2-e1e1e1193019", "name": "2105 2000 mix", "companyName": "CHANNEL", "status": "ACTIVE", "provider": "JohnDeere", "leafUserId": "90a7faf4-33d3-4e35-9f46-1894ae13955d", "organizationId": "9999" } ``` *** ### Get summarized varieties GET `/users/{leafUserId}/varieties/summary` Returns a summarized list of varieties extracted from machine files for a Leaf user. #### Parameters | Parameter | Type | Description | | --------- | ------- | ----------------------------------------------------------------------------- | | `name` | string | Filter by partial variety name. | | `crops` | string | Filter by crop name. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (max `100`). | | `sort` | string | Sorting order. Comma-separated fields with optional `,asc` or `,desc` suffix. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/varieties/summary' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/varieties/summary" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/varieties/summary' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "680ff073-18d0-4951-ba69-b2ca0b098bc3", "name": "corn variety 2", "leafUserId": "028c30fa-6d2a-11ee-b962-0242ac120002", "crops": ["corn"] } ] ``` *** ### Search for varieties GET `/varieties/search` Searches for varieties by name (partial values supported). John Deere Operation Center varieties are available at the Leaf user level. #### Parameters | Parameter | Type | Description | | ------------ | ------- | --------------------------------------------------- | | `name` | string | **Required.** Partial variety name to search. | | `maxResults` | integer | Maximum results to return (default `10`, max `20`). | | `crop` | string | Filter by crop name. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/varieties/search?name=Master' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/varieties/search" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"name": "Master"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/varieties/search' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { name: 'Master' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "crops": ["ALFALFA"], "name": "Master Piece", "companyName": "Simplot", "status": "ACTIVE", "leafUserId": "028c30fa-6d2a-11ee-b962-0242ac120002", "provider": "JohnDeere", "providerId": "8e1e0920-1265-4066-8067-8ce2ce5012b2" } ] ``` *** ## Tank Mixes ### Get all tank mixes GET `/tankMixes` Returns a paginated list of tank mixes from providers. Currently supports John Deere. #### Parameters | Parameter | Type | Description | | ------------ | ------------- | ----------------------------------------------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user. | | `provider` | string | Filter by provider (`JohnDeere`). | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (max `100`). | | `sort` | string | Sorting order. Comma-separated fields with optional `,asc` or `,desc` suffix. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/tankMixes?leafUserId={leafUserId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/tankMixes" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "{leafUserId}"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/tankMixes' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { leafUserId: '{leafUserId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "abc59ca6-937c-11ee-b9d1-0242ac120002", "name": "TankTest1", "providerId": "b74878dc-937c-11ee-b9d1-0242ac120002", "notes": null, "solutionRate": { "valueAsDouble": 5, "unit": "gal1ac-1", "vrDomainId": "vrSolutionRateLiquid" }, "formulationType": "LIQUID", "targetCrops": ["PINEAPPLE"], "carrier": { "id": "c0cb84d0-937c-11ee-b9d1-0242ac120002", "name": "Water", "labelProvider": "JohnDeere", "registrant": "GENERIC", "productType": "ADDITIVE", "formulationType": "LIQUID", "carrier": true, "status": "ACTIVE" }, "components": [ { "id": "d1fb3aac-937c-11ee-b9d1-0242ac120002", "name": "Brandt Big Foot SS", "labelProvider": "JohnDeere", "registrant": "Brandt Consolidated, Inc.", "productType": "ADDITIVE", "formulationType": "DRY", "carrier": false, "status": "ACTIVE" } ], "status": "ACTIVE", "provider": "JohnDeere", "leafUserId": "cd06377c-937c-11ee-b9d1-0242ac120002" } ] ``` *** ### Search for tank mixes GET `/tankMixes/search` Searches for tank mixes by name (partial values supported). John Deere Operation Center tank mixes are available at the Leaf user level. #### Parameters | Parameter | Type | Description | | ------------ | ------- | --------------------------------------------------- | | `name` | string | **Required.** Partial tank mix name to search. | | `maxResults` | integer | Maximum results to return (default `10`, max `20`). | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/tankMixes/search?name=Tank' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/tankMixes/search" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"name": "Tank"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/tankMixes/search' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { name: 'Tank' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "abc59ca6-937c-11ee-b9d1-0242ac120002", "name": "TankTest1", "providerId": "b74878dc-937c-11ee-b9d1-0242ac120002", "notes": null, "solutionRate": { "valueAsDouble": 5, "unit": "gal1ac-1", "vrDomainId": "vrSolutionRateLiquid" }, "formulationType": "LIQUID", "targetCrops": ["PINEAPPLE"], "carrier": { "id": "c0cb84d0-937c-11ee-b9d1-0242ac120002", "name": "Water", "labelProvider": "JohnDeere", "registrant": "GENERIC", "productType": "ADDITIVE", "formulationType": "LIQUID", "carrier": true, "status": "ACTIVE" }, "components": [ { "id": "d1fb3aac-937c-11ee-b9d1-0242ac120002", "name": "Brandt Big Foot SS", "labelProvider": "JohnDeere", "registrant": "Brandt Consolidated, Inc.", "productType": "ADDITIVE", "formulationType": "DRY", "carrier": false, "status": "ACTIVE" } ], "status": "ACTIVE", "provider": "JohnDeere", "leafUserId": "cd06377c-937c-11ee-b9d1-0242ac120002" } ] ``` # Beta Layers Source: https://docs.withleaf.io/api-reference/beta-layers Retrieve aerial imagery layers (tassel count, stand count, NDVI, RGB) from the Sentera integration and upload layers to Climate FieldView. Use these endpoints to retrieve aerial imagery layers (tassel count, stand count, NDVI, RGB) from Sentera and upload RGB layers to Climate FieldView. For conceptual background, see [Layers](/beta/layers). The Layers API is currently in **beta**. Endpoints, request/response schemas, and behavior may change without notice. ## Base URL ``` https://api.withleaf.io/services/beta/api ``` ## Endpoints | Endpoint | Method | Path | | --------------------------------------------------------------------------- | ----------------- | --------------------------------------------- | | [Get all layers for a Leaf user](#get-all-layers-for-a-leaf-user) | GET | `/users/{leafUserId}/layers` | | [Upload a layer to Climate FieldView](#upload-a-layer-to-climate-fieldview) | POST | `/users/{leafUserId}/layers/climateFieldView` | *** ### Get all layers for a Leaf user GET `/users/{leafUserId}/layers` Returns a paginated list of layers that belong to a Leaf user. Layers are sourced from the Sentera integration and include tassel count, stand count, NDVI, and RGB imagery. #### Parameters | Parameter | Type | Description | | --------- | ------- | ---------------------------------------------------------------------- | | `type` | string | Filter by layer type: `TASSEL_COUNT`, `STAND_COUNT`, `NVDI`, or `RGB`. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | The default page size is 20 when `page` and `size` are not set. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/layers' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/layers" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/layers' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "96a098e0-f1d0-47e8-968d-9d55d54da114", "leafUserId": "055c4d61-b1e2-4fa9-873c-23433a11c271", "apiOwnerUsername": "yourUsername", "type": "RGB", "origin": "PROVIDER_POOLED", "provider": "Sentera", "providerLayerId": "vnoyi6a_FI_edovSouthernM_CV_prod_82f9b3d6_211018_151052", "providerFieldId": "ycof8zg_AS_edovSouthernM_CV_prod_a025df2d_211015_200456", "name": "QuickTile RGB", "size": 159135298, "md5": "7ff746c6f5f06fc25b46420328402bed", "contentS3": "URL", "downloadContentS3": "URL", "createdTime": "2022-02-16T21:40:20.257Z", "leafFieldIds": [ "f43ca7cc-c73a-43b9-8685-070b03876475", "edcf7b8b-913e-4e53-a0b5-91aa16699dfc" ] } ] ``` *** ### Upload a layer to Climate FieldView POST `/users/{leafUserId}/layers/climateFieldView` Sends a layer file to Climate FieldView using the Leaf user's Climate FieldView credentials. Currently only true-color image (RGB) files are supported. The file must meet the following Climate FieldView requirements: * Multi-band GeoTIFF with 24-bit composite values (3 bands: Red, Green, Blue) * Coordinate Reference System must be UTM with WGS84 datum * Required GDAL\_METADATA entries embedded in the GeoTIFF: * `acquisitionStartDate` — ISO 8601 date * `acquisitionEndDate` — ISO 8601 date * `isCalibrated` — boolean Although Climate FieldView supports files up to 500 MB, this endpoint currently accepts files up to 5 MB. Uploaded layers are not stored on the Leaf side and are only available directly in Climate FieldView. #### Parameters | Parameter | Type | Description | | ------------ | ------ | ---------------------------- | | `uploadType` | string | **Required.** Must be `RGB`. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@rgb.tif' \ 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/layers/climateFieldView?uploadType=RGB' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/api/users/{leafUserId}/layers/climateFieldView" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"uploadType": "RGB"} files = {"file": open("rgb.tif", "rb")} response = requests.post(endpoint, headers=headers, files=files, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/api/users/{leafUserId}/layers/climateFieldView' const form = new FormData() form.append('file', fs.createReadStream('rgb.tif')) const headers = { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() } axios.post(endpoint, form, { headers, params: { uploadType: 'RGB' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "86fb8bea-1670-48ea-a85d-fbdf6feefb35", "name": "LayerName" } ``` # Beta Prescriptions Source: https://docs.withleaf.io/api-reference/beta-prescriptions Upload prescriptions across providers and use list or download endpoints where supported. Use these endpoints to upload prescription (Rx) maps to provider accounts. Depending on the provider, you can also list existing prescriptions or download them. For conceptual background, see [Prescriptions](/beta/prescriptions). The Prescriptions API is currently in **beta**. Endpoints, request/response schemas, and behavior may change without notice. ## Base URL ``` https://api.withleaf.io/services/beta/prescription/api ``` ## Endpoints | Endpoint | Method | Path | | ------------------------------------------------------------------------------------- | ----------------- | ---------------------------------------- | | [Upload prescription to Raven Slingshot](#upload-prescription-to-raven-slingshot) | POST | `/users/{leafUserId}/ravenSlingshot` | | [List prescriptions from Raven Slingshot](#list-prescriptions-from-raven-slingshot) | GET | `/users/{leafUserId}/ravenSlingshot` | | [Upload prescription to John Deere](#upload-prescription-to-john-deere) | POST | `/users/{leafUserId}/johnDeere` | | [List prescriptions from John Deere](#list-prescriptions-from-john-deere) | GET | `/users/{leafUserId}/johnDeere` | | [Download prescription from John Deere](#download-prescription-from-john-deere) | GET | `/users/{leafUserId}/johnDeere/download` | | [Upload prescription to CNHi](#upload-prescription-to-cnhi) | POST | `/users/{leafUserId}/cnhi` | | [List prescriptions from CNHi](#list-prescriptions-from-cnhi) | GET | `/users/{leafUserId}/cnhi` | | [Upload prescription to Climate FieldView](#upload-prescription-to-climate-fieldview) | POST | `/users/{leafUserId}/climateFieldView` | | [Upload prescription to Trimble](#upload-prescription-to-trimble) | POST | `/users/{leafUserId}/trimble` | | [Upload prescription to AgLeader](#upload-prescription-to-agleader) | POST | `/users/{leafUserId}/agleader` | *** ## Raven Slingshot ### Upload prescription to Raven Slingshot POST `/users/{leafUserId}/ravenSlingshot` Uploads a prescription file using the Raven Slingshot credentials of a Leaf user. The file must be a `.zip` containing one each of `.shp`, `.dbf`, and `.shx` — all sharing the same base name. The zip cannot contain subfolders. #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@prescription_rx_map.zip' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/ravenSlingshot' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/ravenSlingshot" headers = {"Authorization": f"Bearer {TOKEN}"} files = {"file": open("prescription_rx_map.zip", "rb")} response = requests.post(endpoint, headers=headers, files=files) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/ravenSlingshot' const form = new FormData() form.append('file', fs.createReadStream('prescription_rx_map.zip')) const headers = { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() } axios.post(endpoint, form, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "str", "name": "str" } ``` *** ### List prescriptions from Raven Slingshot GET `/users/{leafUserId}/ravenSlingshot` Lists existing prescriptions available in Raven Slingshot for the Leaf user. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/ravenSlingshot' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/ravenSlingshot" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/ravenSlingshot' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "str", "name": "str" } ] ``` *** ## John Deere ### Upload prescription to John Deere POST `/users/{leafUserId}/johnDeere` Uploads a prescription file using the John Deere credentials of a Leaf user. The `organizationId` query parameter is **required** and must be the organization ID from John Deere. The file must be a `.zip` containing a folder named `Rx/` with one each of `.shp`, `.dbf`, and `.shx` — all sharing the same base name. #### Parameters | Parameter | Type | Description | | ---------------- | ------ | --------------------------------------------- | | `organizationId` | string | **Required.** The John Deere organization ID. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@prescription_rx_map.zip' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere?organizationId={organizationId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"organizationId": "{organizationId}"} files = {"file": open("prescription_rx_map.zip", "rb")} response = requests.post(endpoint, headers=headers, files=files, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere' const form = new FormData() form.append('file', fs.createReadStream('prescription_rx_map.zip')) const headers = { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() } axios.post(endpoint, form, { headers, params: { organizationId: '{organizationId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "str", "name": "str" } ``` *** ### List prescriptions from John Deere GET `/users/{leafUserId}/johnDeere` Lists existing prescriptions available in John Deere for the Leaf user. #### Parameters | Parameter | Type | Description | | ---------------- | ------ | --------------------------------------------- | | `organizationId` | string | **Required.** The John Deere organization ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere?organizationId={organizationId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"organizationId": "{organizationId}"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { organizationId: '{organizationId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "str", "name": "str" } ] ``` *** ### Download prescription from John Deere GET `/users/{leafUserId}/johnDeere/download` Downloads a prescription by its John Deere file ID. The `fileId` parameter refers to the ID on the John Deere side. #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------ | | `fileId` | string | **Required.** The file ID from John Deere. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere/download?fileId={fileId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere/download" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"fileId": "{fileId}"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/johnDeere/download' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { fileId: '{fileId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "fileUrl": "url_to_download" } ``` *** ## CNHi ### Upload prescription to CNHi POST `/users/{leafUserId}/cnhi` Uploads a prescription file using the CNHi credentials of a Leaf user. The `companyId` query parameter is **required**. You can obtain this value from the grower endpoints using the `providerOrganizationId` property. The file must be a `.zip` containing one each of `.shp`, `.dbf`, and `.shx` — all sharing the same base name. No subfolders. #### Parameters | Parameter | Type | Description | | ----------- | ------ | ------------------------------------------------------------- | | `companyId` | string | **Required.** The CNHi company ID (`providerOrganizationId`). | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@prescription_map.zip' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/cnhi?companyId={companyId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/cnhi" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"companyId": "{companyId}"} files = {"file": open("prescription_map.zip", "rb")} response = requests.post(endpoint, headers=headers, files=files, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/cnhi' const form = new FormData() form.append('file', fs.createReadStream('prescription_map.zip')) const headers = { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() } axios.post(endpoint, form, { headers, params: { companyId: '{companyId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "str", "name": "str" } ``` *** ### List prescriptions from CNHi GET `/users/{leafUserId}/cnhi` Lists existing prescriptions available in CNHi for the Leaf user. #### Parameters | Parameter | Type | Description | | ----------- | ------ | ---------------------------------- | | `companyId` | string | **Required.** The CNHi company ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/cnhi?companyId={companyId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/cnhi" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"companyId": "{companyId}"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/cnhi' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { companyId: '{companyId}' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "str", "name": "str" } ] ``` *** ## Climate FieldView ### Upload prescription to Climate FieldView POST `/users/{leafUserId}/climateFieldView` Uploads a prescription file using the Climate FieldView credentials of a Leaf user. The file must be a `.zip` containing one each of `.shp`, `.dbf`, and `.shx` — all sharing the same base name. No subfolders. #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@prescription_rx_map.zip' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/climateFieldView' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/climateFieldView" headers = {"Authorization": f"Bearer {TOKEN}"} files = {"file": open("prescription_rx_map.zip", "rb")} response = requests.post(endpoint, headers=headers, files=files) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/climateFieldView' const form = new FormData() form.append('file', fs.createReadStream('prescription_rx_map.zip')) const headers = { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() } axios.post(endpoint, form, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "str", "name": "str" } ``` *** ## Trimble ### Upload prescription to Trimble POST `/users/{leafUserId}/trimble` Uploads a prescription file using the Trimble credentials of a Leaf user. The `organizationId`, `rateColumn`, and `rateUnit` query parameters are **required**. `rateColumn` must be the column name from the Shapefile. `rateUnit` must be one of the supported units listed below. The file must be a `.zip` containing one each of `.shp`, `.dbf`, and `.shx` — all sharing the same base name. No subfolders. #### Parameters | Parameter | Type | Description | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------- | | `organizationId` | string | **Required.** The Trimble organization ID. | | `rateColumn` | string | **Required.** Column name from the Shapefile. | | `rateUnit` | string | **Required.** One of: `gal/ac`, `l/ha`, `lbs/ac`, `ton/ac`, `kg/ha`, `t/ha`, `kS/ac`, `kS/ha`, `lbs(N)/ac`, `kg(N)/ha`, `S/ha`. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@prescription_rx_map.zip' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/trimble?organizationId={organizationId}&rateColumn=Rate&rateUnit=lbs/ac' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/trimble" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"organizationId": "{organizationId}", "rateColumn": "Rate", "rateUnit": "lbs/ac"} files = {"file": open("prescription_rx_map.zip", "rb")} response = requests.post(endpoint, headers=headers, files=files, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/trimble' const form = new FormData() form.append('file', fs.createReadStream('prescription_rx_map.zip')) const headers = { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() } axios.post(endpoint, form, { headers, params: { organizationId: '{organizationId}', rateColumn: 'Rate', rateUnit: 'lbs/ac' } }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "str", "name": "str" } ``` *** ## AgLeader ### Upload prescription to AgLeader POST `/users/{leafUserId}/agleader` Uploads a prescription file using the AgLeader credentials of a Leaf user. The file must be a `.zip` containing one each of `.shp`, `.dbf`, and `.shx` — all sharing the same base name. No subfolders. #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@prescription_rx_map.zip' \ 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/agleader' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/agleader" headers = {"Authorization": f"Bearer {TOKEN}"} files = {"file": open("prescription_rx_map.zip", "rb")} response = requests.post(endpoint, headers=headers, files=files) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/beta/prescription/api/users/{leafUserId}/agleader' const form = new FormData() form.append('file', fs.createReadStream('prescription_rx_map.zip')) const headers = { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() } axios.post(endpoint, form, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "str", "name": "str" } ``` # Usage Monitoring endpoints Source: https://docs.withleaf.io/api-reference/billing Monitor your usage with contracts and consumption endpoints. All HTTP methods should be prepended by this service's endpoint: ``` https://api.withleaf.io/services/billingapplication/api ``` ## Usage Monitoring Endpoints Monitor your usage with these endpoints: | Description | Endpoints | | ------------------------------------- | --------------------------------------------------------------------------- | | List your contracts | `GET /billing/contracts` | | Get contract details | `GET /billing/contracts/{contract_id}` | | Get daily usage summary | `GET /billing/contracts/{contract_id}/consumption` | | Get usage range for your organization | `GET /billing/contracts/{contract_id}/consumption/api-owner` | | Get usage range for specific user | `GET /billing/contracts/{contract_id}/consumption/leaf-user/{leaf_user_id}` | *** ## List your contracts GET `/billing/contracts` Get a list of all usage monitoring contracts available for your organization. Each contract represents a different service or feature you can monitor. ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/billingapplication/api/billing/contracts' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "id": "unique id", "product": "FIELDS_BOUNDARY", "startDate": "2023-01-01T00:00:00Z", "endDate": "2024-01-01T00:00:00Z", "region": null } ] ``` **Response fields:** * `id`: Unique identifier for this contract * `product`: Which service this tracks * `startDate`: When usage monitoring began for this contract * `endDate`: When usage monitoring ends for this contract * `region`: Geographic region, if applicable *** ## Get contract details GET `/billing/contracts/{contract_id}` Get detailed information about a specific usage monitoring contract. ### Parameters | Parameter | Type | Description | | ------------- | ---- | -------------------- | | `contract_id` | path | UUID of the contract | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "unique id", "product": "FIELDS_BOUNDARY", "startDate": "2023-01-01T00:00:00Z", "endDate": "2024-01-01T00:00:00Z", "region": null } ``` *** ## Get daily usage summary GET `/billing/contracts/{contract_id}/consumption` Get usage metrics for a specific contract for a single day. If you don't specify a date, it returns today's usage. ### Parameters | Parameter | Type | Description | | ------------- | ---------------- | -------------------------------------------------------------- | | `contract_id` | path | UUID of the contract | | `timestamp` | query (optional) | Date to get usage for in ISO format `YYYY-MM-DDTHH:MM:SS.sssZ` | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption?timestamp=2024-01-15T00:00:00.000Z' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers, params={ 'timestamp': '2024-01-15T00:00:00.000Z' }) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { timestamp: '2024-01-15T00:00:00.000Z' } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "areaUnit": "Acre", "date": "2024-01-15T00:00:00Z", "totalUniqueArea": 0.8, "leafUsersAreas": [ { "leafUserId": "uuid1", "totalArea": 0.6 }, { "leafUserId": "uuid2", "totalArea": 0.4 } ] } ``` **Response fields:** * `areaUnit`: Unit of measurement, `Acre` or `Hectare` * `date`: The date this usage data represents * `totalUniqueArea`: Total unique area processed, removing overlaps between users * `leafUsersAreas`: Breakdown of usage by individual users *** ## Get usage range for your organization GET `/billing/contracts/{contract_id}/consumption/api-owner` Get usage metrics for your entire organization over a date range. Shows daily breakdown of total and cumulative usage. ### Parameters | Parameter | Type | Description | | ------------- | ----- | --------------------------------------------------- | | `contract_id` | path | UUID of the contract | | `startTime` | query | Start date in ISO format `YYYY-MM-DDTHH:MM:SS.sssZ` | | `endTime` | query | End date in ISO format `YYYY-MM-DDTHH:MM:SS.sssZ` | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption/api-owner?startTime=2024-01-01T00:00:00.000Z&endTime=2024-01-31T00:00:00.000Z' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption/api-owner' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers, params={ 'startTime': '2024-01-01T00:00:00.000Z', 'endTime': '2024-01-31T00:00:00.000Z' }) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption/api-owner' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { startTime: '2024-01-01T00:00:00.000Z', endTime: '2024-01-31T00:00:00.000Z' } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "areaUnit": "Acre", "areaPerDay": [ { "date": "2024-01-02T00:00:00Z", "totalArea": 90.0, "dailyArea": 65.0 }, { "date": "2024-01-03T00:00:00Z", "totalArea": 90.0, "dailyArea": 0.0 } ] } ``` **Response fields:** * `areaUnit`: Unit of measurement, `Acre` or `Hectare` * `areaPerDay`: Array of daily usage data * `date`: The date for this data point * `totalArea`: Cumulative area processed up to this date * `dailyArea`: New area processed on this specific date *** ## Get usage range for specific user GET `/billing/contracts/{contract_id}/consumption/leaf-user/{leaf_user_id}` Get usage metrics for a specific Leaf user over a date range. Shows how much area this user has processed day by day. ### Parameters | Parameter | Type | Description | | -------------- | ----- | --------------------------------------------------- | | `contract_id` | path | UUID of the contract | | `leaf_user_id` | path | UUID of the Leaf user | | `startTime` | query | Start date in ISO format `YYYY-MM-DDTHH:MM:SS.sssZ` | | `endTime` | query | End date in ISO format `YYYY-MM-DDTHH:MM:SS.sssZ` | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption/leaf-user/{leaf_user_id}?startTime=2024-01-01T00:00:00.000Z&endTime=2024-01-31T00:00:00.000Z' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption/leaf-user/{leaf_user_id}' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers, params={ 'startTime': '2024-01-01T00:00:00.000Z', 'endTime': '2024-01-31T00:00:00.000Z' }) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/billingapplication/api/billing/contracts/{contract_id}/consumption/leaf-user/{leaf_user_id}' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { startTime: '2024-01-01T00:00:00.000Z', endTime: '2024-01-31T00:00:00.000Z' } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "areaUnit": "Acre", "areaPerDay": [ { "date": "2024-01-02T00:00:00Z", "totalArea": 0.6, "dailyArea": 0.6 }, { "date": "2024-01-03T00:00:00Z", "totalArea": 0.6, "dailyArea": 0.0 } ] } ``` **Response fields:** * `areaUnit`: Unit of measurement, `Acre` or `Hectare` * `areaPerDay`: Array of daily usage data for this user * `date`: The date for this data point * `totalArea`: Cumulative area processed by this user up to this date * `dailyArea`: New area processed by this user on this specific date # Configurations Source: https://docs.withleaf.io/api-reference/configurations Manage API owner and Leaf user configurations that control provider data syncing, field operations processing, machine file conversion, and output formats. For conceptual background, see [Configurations](/configuration/overview). Configurations control how Leaf syncs data from providers, processes machine files, creates field operations, and generates images. You set configurations at the API owner level (applies to all Leaf users by default) or override them for individual Leaf users. Leaf uses API owner settings as the runtime defaults for Leaf users. The public `GET /configs/{leafUserId}` endpoint, however, returns only the stored custom configuration for that Leaf user. If no Leaf-user-specific config exists, that endpoint returns `404`. ## Base URL ``` https://api.withleaf.io/services/config/api ``` ## Endpoints | Action | Method | Path | | -------------------------------------------------------------------- | ------------------- | ----------------------- | | [Get API owner's configuration](#get-api-owners-configuration) | GET | `/configs` | | [Get Leaf user's configuration](#get-leaf-users-configuration) | GET | `/configs/{leafUserId}` | | [Create Leaf user's configuration](#create-leaf-users-configuration) | POST | `/configs/{leafUserId}` | | [Update API owner's configuration](#update-api-owners-configuration) | PATCH | `/configs` | | [Update Leaf user's configuration](#update-leaf-users-configuration) | PATCH | `/configs/{leafUserId}` | | [Delete Leaf user's configuration](#delete-leaf-users-configuration) | DELETE | `/configs/{leafUserId}` | ## Common configuration properties | Property | Type | Description | | -------------------------- | ------- | ---------------------------------------------------------- | | `apiOwnerUsername` | string | Your API owner username (read-only). | | `leafUserId` | string | The Leaf user ID, or empty string for API owner configs. | | `operationsImageCreation` | boolean | Generate PNG images for field operation properties. | | `fieldsAutoSync` | boolean | Automatically sync field boundaries from providers. | | `fieldsMergeIntersection` | float | Minimum intersection ratio to merge overlapping fields. | | `fieldsAttachIntersection` | float | Minimum intersection ratio to attach operations to fields. | | `cleanupStandardGeojson` | boolean | Apply cleanup rules to standardized GeoJSON output. | | `cleanupRules` | object | Rules for filtering data points during cleanup. | This table lists common properties only. The full response object includes many more configuration fields. *** ## Get API owner's configuration `GET /configs` Returns the configuration for your API owner account. ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/config/api/configs' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/config/api/configs" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const endpoint = "https://api.withleaf.io/services/config/api/configs" const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "apiOwnerUsername": "api-owner", "leafUserId": "", "operationsImageCreation": true, "fieldsAutoSync": true, "fieldsMergeIntersection": 0.01, "fieldsAttachIntersection": 0.01, "cleanupStandardGeojson": true, "cleanupRules": { "wetMass": [{"operator": "GT", "value": 0.0}], "harvestMoisture": [{"operator": "GT", "value": 0.0}, {"operator": "LT", "value": 100.0}], "recordingStatus": [{"operator": "EQ", "value": "On"}] } } ``` *** ## Get Leaf user's configuration `GET /configs/{leafUserId}` Returns the stored custom configuration for a specific Leaf user. If the Leaf user has no custom configuration, this endpoint returns `404`. ### Path parameters | Parameter | Type | Description | | ------------ | ------------- | ----------------- | | `leafUserId` | string (UUID) | The Leaf user ID. | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/config/api/configs/f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" endpoint = f"https://api.withleaf.io/services/config/api/configs/{LEAF_USER_ID}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" const endpoint = `https://api.withleaf.io/services/config/api/configs/${LEAF_USER_ID}` const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "apiOwnerUsername": "api-owner", "leafUserId": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "operationsImageCreation": false, "fieldsAutoSync": true } ``` *** ## Create Leaf user's configuration `POST /configs/{leafUserId}` Creates a custom configuration for a Leaf user. Include only the properties you want to override. Omitted properties remain unset on the stored Leaf-user config and continue to resolve from the API owner at runtime. ### Path parameters | Parameter | Type | Description | | ------------ | ------------- | ----------------- | | `leafUserId` | string (UUID) | The Leaf user ID. | ### Request body All fields are optional. Only include the properties you want to set. ```json theme={null} { "operationsImageCreation": false, "fieldsAutoSync": true } ``` ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"operationsImageCreation": false, "fieldsAutoSync": true}' \ 'https://api.withleaf.io/services/config/api/configs/f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" endpoint = f"https://api.withleaf.io/services/config/api/configs/{LEAF_USER_ID}" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "operationsImageCreation": False, "fieldsAutoSync": True } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" const endpoint = `https://api.withleaf.io/services/config/api/configs/${LEAF_USER_ID}` const headers = { Authorization: `Bearer ${TOKEN}` } const data = { operationsImageCreation: false, fieldsAutoSync: true } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response The response includes the stored Leaf-user configuration object. Omitted properties are not filled in with inherited values in this response. ```json theme={null} { "apiOwnerUsername": "api-owner", "leafUserId": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "operationsImageCreation": false, "fieldsAutoSync": true } ``` *** ## Update API owner's configuration `PATCH /configs` Updates specific fields of the API owner's configuration. Only include the properties you want to change. Configuration changes are not retroactive. Existing data is not reprocessed. If you need existing data regenerated, choose the reprocess endpoint that matches the pipeline stage affected by the setting: use file reprocessing for machine-file processing settings, and operation reprocessing for operation-generation settings. ### Request body All fields are optional. ```json theme={null} { "operationsImageCreation": true, "fieldsAutoSync": false } ``` ### Request ```bash cURL theme={null} curl -X PATCH \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"operationsImageCreation": true, "fieldsAutoSync": false}' \ 'https://api.withleaf.io/services/config/api/configs' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/config/api/configs" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "operationsImageCreation": True, "fieldsAutoSync": False } response = requests.patch(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const endpoint = "https://api.withleaf.io/services/config/api/configs" const headers = { Authorization: `Bearer ${TOKEN}` } const data = { operationsImageCreation: true, fieldsAutoSync: false } axios.patch(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "apiOwnerUsername": "api-owner", "leafUserId": "", "operationsImageCreation": true, "fieldsAutoSync": false, "fieldsMergeIntersection": 0.01, "fieldsAttachIntersection": 0.01, "cleanupStandardGeojson": true, "cleanupRules": { "wetMass": [{"operator": "GT", "value": 0.0}], "harvestMoisture": [{"operator": "GT", "value": 0.0}, {"operator": "LT", "value": 100.0}], "recordingStatus": [{"operator": "EQ", "value": "On"}] } } ``` *** ## Update Leaf user's configuration `PATCH /configs/{leafUserId}` Updates specific fields of a Leaf user's configuration. Only include the properties you want to change. ### Path parameters | Parameter | Type | Description | | ------------ | ------------- | ----------------- | | `leafUserId` | string (UUID) | The Leaf user ID. | ### Request body All fields are optional. ```json theme={null} { "operationsImageCreation": false, "fieldsMergeIntersection": 0.05 } ``` ### Request ```bash cURL theme={null} curl -X PATCH \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"operationsImageCreation": false, "fieldsMergeIntersection": 0.05}' \ 'https://api.withleaf.io/services/config/api/configs/f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" endpoint = f"https://api.withleaf.io/services/config/api/configs/{LEAF_USER_ID}" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "operationsImageCreation": False, "fieldsMergeIntersection": 0.05 } response = requests.patch(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" const endpoint = `https://api.withleaf.io/services/config/api/configs/${LEAF_USER_ID}` const headers = { Authorization: `Bearer ${TOKEN}` } const data = { operationsImageCreation: false, fieldsMergeIntersection: 0.05 } axios.patch(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response The response includes the stored Leaf-user configuration object after the update. Omitted properties are not filled in with inherited values in this response. ```json theme={null} { "apiOwnerUsername": "api-owner", "leafUserId": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "operationsImageCreation": false, "fieldsMergeIntersection": 0.05 } ``` *** ## Delete Leaf user's configuration `DELETE /configs/{leafUserId}` Deletes the custom configuration for a Leaf user. After deletion, the Leaf user inherits all settings from the API owner. ### Path parameters | Parameter | Type | Description | | ------------ | ------------- | ----------------- | | `leafUserId` | string (UUID) | The Leaf user ID. | ### Request ```bash cURL theme={null} curl -X DELETE \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/config/api/configs/f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" endpoint = f"https://api.withleaf.io/services/config/api/configs/{LEAF_USER_ID}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.delete(endpoint, headers=headers) print(response.status_code) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" const endpoint = `https://api.withleaf.io/services/config/api/configs/${LEAF_USER_ID}` const headers = { Authorization: `Bearer ${TOKEN}` } axios.delete(endpoint, { headers }) .then(res => console.log(res.status)) .catch(console.error) ``` # Field Boundary Upload Source: https://docs.withleaf.io/api-reference/field-upload Upload field boundary files (shapefiles, GeoJSON, KML/KMZ) and track the status of each upload and its entries. Use the upload service to create many field boundaries at once from shapefiles, GeoJSON, or KML/KMZ files. This page covers the upload endpoints, supported formats, status values, and the follow-up calls you use to inspect results. For conceptual background, see [Uploading Boundaries](/fields/uploading-boundaries). ## Base URL ``` https://api.withleaf.io/services/uploadservice/api ``` ## Endpoints | Endpoint | Method | Path | | ------------------------------------------- | ----------------- | ---------------------------- | | [Upload a field file](#upload-a-field-file) | POST | `/upload` | | [Get all uploads](#get-all-uploads) | GET | `/upload` | | [Get an upload](#get-an-upload) | GET | `/upload/{uploadId}` | | [Get upload entries](#get-upload-entries) | GET | `/upload/{uploadId}/entries` | *** ## Supported formats You can upload field boundaries in the following formats: * **Shapefile** — packaged as a `.zip` containing `.shp`, `.shx`, `.dbf`, and optionally `.prj` * **GeoJSON** — `.json` or `.geojson` * **KML / KMZ** — `.kml` or `.kmz` Leaf reprojects all uploaded geometries to WGS 84 (EPSG:4326). ## Upload limits | Constraint | Limit | | ------------------------- | ----- | | Maximum file size | 3 GB | | Maximum fields per upload | 100 | ## Upload and entry statuses Each upload progresses through the following statuses: | Upload status | Description | | ------------- | --------------------------------------------------------------------------------------------------------------- | | `RECEIVED` | The file has been received and is queued for processing. | | `PROCESSED` | All files in the upload have finished processing, and at least one recognized file created fields successfully. | | `FAILED` | The upload did not generate any field boundaries successfully. | Individual entries within an upload have their own statuses: | Entry status | Description | | -------------------- | -------------------------------------------------------------- | | `PROCESSING` | The entry is being converted and validated. | | `CONVERTED` | The geometry has been converted and is pending field creation. | | `FINISHED` | The field has been created from this entry. | | `FAILED` | The entry could not be processed. | | `PARTIALLY_FINISHED` | Some geometries in the entry succeeded while others failed. | *** ## Upload a field file POST `/upload` Uploads a field boundary file for the specified Leaf user. The file is processed asynchronously. Use the [Get an upload](#get-an-upload) or [Get upload entries](#get-upload-entries) endpoints to track progress. ### Parameters | Parameter | Type | Description | | ------------ | --------------- | ---------------------------------------------------- | | `leafUserId` | query (UUID) | **Required.** The Leaf user who owns the fields. | | `farmId` | query (integer) | Optional farm to associate with the uploaded fields. | ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@/path/to/boundaries.zip' \ 'https://api.withleaf.io/services/uploadservice/api/upload?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/uploadservice/api/upload" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "UUID"} with open("/path/to/boundaries.zip", "rb") as f: response = requests.post(endpoint, headers=headers, params=params, files={"file": f}) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/uploadservice/api/upload' const form = new FormData() form.append('file', fs.createReadStream('/path/to/boundaries.zip')) axios.post(endpoint, form, { headers: { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders() }, params: { leafUserId: 'UUID' } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "leafUserId": "7494c90e-28b8-4bb2-9ede-95c1cc894349", "originalFileUrl": "https://storage.withleaf.io/upload/f1e2d3c4-b5a6-7890-abcd-ef1234567890.zip", "fileName": "boundaries.zip", "status": "RECEIVED", "createdTime": "2024-01-15T19:48:51.017Z" } ``` If the `.zip` does not contain the required shapefile components (`.shp`, `.shx`, `.dbf`), the upload fails with status `FAILED`. *** ## Get all uploads GET `/upload` Returns a paginated list of field boundary uploads. ### Parameters | Parameter | Type | Description | | ------------ | ------------- | ------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user. | | `status` | string | `RECEIVED`, `PROCESSED`, or `FAILED`. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`). | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/uploadservice/api/upload?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/uploadservice/api/upload" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "UUID"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/uploadservice/api/upload' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { leafUserId: 'UUID' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "leafUserId": "7494c90e-28b8-4bb2-9ede-95c1cc894349", "originalFileUrl": "https://storage.withleaf.io/upload/f1e2d3c4-b5a6-7890-abcd-ef1234567890.zip", "fileName": "boundaries.zip", "status": "PROCESSED", "createdTime": "2024-01-15T19:48:51.017Z" } ] ``` *** ## Get an upload GET `/upload/{uploadId}` Returns a single field boundary upload by its ID. ### Parameters | Parameter | Type | Description | | ---------- | ----------- | -------------- | | `uploadId` | path (UUID) | The upload ID. | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/uploadservice/api/upload/{uploadId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/uploadservice/api/upload/{uploadId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/uploadservice/api/upload/{uploadId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "leafUserId": "7494c90e-28b8-4bb2-9ede-95c1cc894349", "originalFileUrl": "https://storage.withleaf.io/upload/f1e2d3c4-b5a6-7890-abcd-ef1234567890.zip", "fileName": "boundaries.zip", "status": "PROCESSED", "createdTime": "2024-01-15T19:48:51.017Z" } ``` *** ## Get upload entries GET `/upload/{uploadId}/entries` Returns the individual entries for each recognized file extracted from an upload. Each entry tracks its own processing status and lists the Leaf field IDs created from that recognized file. ### Parameters | Parameter | Type | Description | | ---------- | ----------- | -------------------------- | | `uploadId` | path (UUID) | The upload ID. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`). | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/uploadservice/api/upload/{uploadId}/entries' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/uploadservice/api/upload/{uploadId}/entries" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/uploadservice/api/upload/{uploadId}/entries' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "uploadId": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "leafUserId": "7494c90e-28b8-4bb2-9ede-95c1cc894349", "fieldId": [ "0071484f-4a75-4190-9fd0-f5995d241c2c", "9c2d4d24-9f31-4c0b-9b4b-7a5df4d0d5b6" ], "converterFormat": "GEOJSON", "originalFileUrl": "https://storage.withleaf.io/upload/entries/a1b2c3d4.geojson", "status": "FINISHED", "createFieldErrorDetails": [], "createdTime": "2024-01-15T19:48:51.017Z", "processedTime": "2024-01-15T19:49:20.104Z" } ] ``` Poll the entries endpoint to track each recognized file inside the upload. Use the `fieldId` array to fetch the created fields from the Fields API, and inspect `createFieldErrorDetails` when a file finishes with `FAILED` or `PARTIALLY_FINISHED`. # Fields, Boundaries & Farms Source: https://docs.withleaf.io/api-reference/fields Create, read, and update fields, boundaries, and farms. Run intersection queries, trigger provider syncs, and look up machine files by field. Use the fields service to manage field records, field boundaries, boundary history, preview-field activation, spatial intersection queries, and farm records. This page is the reference for CRUD operations and field-level sync behavior after you already understand the Leaf field model. For conceptual background, see [Fields Overview](/fields/overview) and [Managing Fields](/fields/managing-fields). ## Base URL ``` https://api.withleaf.io/services/fields/api ``` ## Endpoints | Endpoint | Method | Path | | ------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------- | | [Get all fields](#get-all-fields) | GET | `/fields` | | [Get a field](#get-a-field) | GET | `/users/{leafUserId}/fields/{id}` | | [Create a field](#create-a-field) | POST | `/users/{leafUserId}/fields` | | [Update a field](#update-a-field) | PATCH | `/users/{leafUserId}/fields/{id}` | | [Delete a field](#delete-a-field) | DELETE | `/users/{leafUserId}/fields/{id}` | | [Get all operation files of a field](#get-all-operation-files-of-a-field) | GET | `/users/{leafUserId}/fields/{fieldId}/operations/files` | | [Get an operation file of a field](#get-an-operation-file-of-a-field) | GET | `/users/{leafUserId}/fields/{fieldId}/operations/files/{fileId}` | | [Get fields by geometry](#get-fields-by-geometry) | POST | `/users/{leafUserId}/fields/intersects` | | [Get intersection of fields](#get-intersection-of-fields) | POST | `/users/{leafUserId}/fields/intersect` | | [Sync fields manually](#sync-fields-manually) | POST | `/users/{leafUserId}/fields/sync` | | [Enable a preview field](#enable-a-preview-field) | POST | `/users/{leafUserId}/fields/{id}/enableSync` | | [Upload a field to provider](#upload-a-field-to-provider) | POST | `/users/{leafUserId}/fields/{fieldId}/integration/{providerName}` | | [Get all boundaries](#get-all-boundaries) | GET | `/users/{leafUserId}/fields/{fieldId}/boundaries` | | [Get a boundary](#get-a-boundary) | GET | `/users/{leafUserId}/fields/{fieldId}/boundaries/{boundaryId}` | | [Get active boundary](#get-active-boundary) | GET | `/users/{leafUserId}/fields/{fieldId}/boundary` | | [Update active boundary](#update-active-boundary) | PUT | `/users/{leafUserId}/fields/{fieldId}/boundary` | | [Get all farms](#get-all-farms) | GET | `/farms` | | [Get a farm](#get-a-farm) | GET | `/users/{leafUserId}/farms/{id}` | | [Create a farm](#create-a-farm) | POST | `/users/{leafUserId}/farms` | | [Update a farm](#update-a-farm) | PUT | `/users/{leafUserId}/farms/{id}` | *** ## Fields ### Get all fields GET `/fields` Returns a paginated list of fields across all Leaf users. You can narrow results with query parameters. #### Parameters | Parameter | Type | Description | | -------------------- | ----------------- | ------------------------------------------------------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user. | | `id` | string (UUID) | Filter by field ID. | | `name` | string | Filter by field name. | | `type` | string | Field type (`ORIGINAL`, `MERGED`). | | `farmId` | integer | Filter by farm ID. | | `provider` | string | Filter by provider name (`JohnDeere`, `ClimateFieldView`, `CNHI`, `Trimble`, `Leaf`). | | `organizationId` | string | Filter by provider organization ID. | | `mergedFieldId` | string (UUID) | Filter by merged field ID. | | `providerFieldId` | string | Filter by the field ID on the provider side. | | `providerFieldName` | string | Filter by the field name on the provider side. | | `legacy` | boolean | Filter by legacy field status. | | `status` | string | Filter by field status (`PROCESSED`, `PREVIEW`, `WAITING`). | | `providerStatus` | string | Filter by provider-side status. | | `beforeCreatedTime` | string (ISO 8601) | Fields created before this time. | | `afterCreatedTime` | string (ISO 8601) | Fields created after this time. | | `beforeUpdatedTime` | string (ISO 8601) | Fields updated before this time. | | `afterUpdatedTime` | string (ISO 8601) | Fields updated after this time. | | `operationType` | string | Filter by operation type (`harvested`, `planted`, `applied`, `tillage`). | | `operationProvider` | string | Filter by provider on associated operations. | | `operationStartTime` | string (ISO 8601) | Filter fields with operations starting after this time. | | `operationEndTime` | string (ISO 8601) | Filter fields with operations ending before this time. | | `operationCrop` | string | Filter by crop name on associated operations. | | `operationVariety` | string | Filter by variety name on associated operations. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Sorting order. Comma-separated fields with optional `,asc` or `,desc` suffix. | The default page size is 20 when `page` and `size` are not set. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/fields?leafUserId=UUID&status=PROCESSED&page=0&size=10' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/fields" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "UUID", "status": "PROCESSED", "page": 0, "size": 10} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/fields' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { leafUserId: 'UUID', status: 'PROCESSED', page: 0, size: 10 } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "1a952614-3673-4d1e-b677-1f7224339ec6", "leafUserId": "58800d61-91ac-4922-8e2a-f0216b9f052a", "boundaries": ["279b52d5-ec6d-4459-a06a-4f47ffab0659"], "providerName": "JohnDeere", "providerId": 2, "providerFieldId": "b96ed268-728f-489e-b928-9d3e70082be4", "providerBoundaryId": "125fc49f-7e75-43fe-89f2-af976addb392", "providerFieldName": "North Quarter", "organizationId": "428214", "type": "ORIGINAL", "farmId": 3746117, "mergedFieldId": "f97c5bbc-2dbf-4400-8d59-39eba37f8847", "sources": [], "status": "PROCESSED", "createdTime": "2021-10-20T21:21:24.732030Z", "updatedTime": "2021-11-03T01:34:15.154051Z" } ] ``` *** ### Get a field GET `/users/{leafUserId}/fields/{id}` Returns a single field for the specified Leaf user. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The field ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single field object (same shape as the objects in the [Get all fields](#get-all-fields) response). *** ### Create a field POST `/users/{leafUserId}/fields` Creates a field for the specified Leaf user. The request body must include a `geometry` property with `type` set to `MultiPolygon`. You can optionally set `id` and `name`. If you omit `id`, Leaf generates a UUID. The `id` cannot be changed after creation. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | #### Request body ```json theme={null} { "name": "North Quarter", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137] ]]] } } ``` #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"name":"North Quarter","geometry":{"type":"MultiPolygon","coordinates":[[[[-93.48821,41.77137],[-93.48817,41.77143],[-93.48821,41.76068],[-93.48821,41.77137]]]]}}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "name": "North Quarter", "geometry": { "type": "MultiPolygon", "coordinates": [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] } } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'North Quarter', geometry: { type: 'MultiPolygon', coordinates: [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] } } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "leafUserId": "95eb7d79-b93d-4fc2-877a-3f2b366f8beb", "area": { "value": 12.45, "unit": "ha" }, "boundaries": ["d0245010-157d-4988-96a2-5f3637098475"], "geometry": { "type": "MultiPolygon", "coordinates": [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] }, "type": "ORIGINAL", "name": "North Quarter", "status": "PROCESSED", "createdTime": "2024-01-15T19:48:51.017280Z", "updatedTime": "2024-01-15T19:48:51.017280Z" } ``` *** ### Update a field PATCH `/users/{leafUserId}/fields/{id}` Updates a field. You can update `name`, `farmId`, and `geometry`. If you update the geometry, Leaf creates a new active boundary and sets the previous one to inactive. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The field ID. | #### Request body ```json theme={null} { "name": "Updated Field Name", "farmId": 1538766 } ``` #### Request ```bash cURL theme={null} curl -X PATCH \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"name":"Updated Field Name","farmId":1538766}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"name": "Updated Field Name", "farmId": 1538766} response = requests.patch(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'Updated Field Name', farmId: 1538766 } axios.patch(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns the updated field object. *** ### Delete a field DELETE `/users/{leafUserId}/fields/{id}` Deletes a manually created field. Fields created by a provider cannot be deleted through the API. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The field ID. | #### Request ```bash cURL theme={null} curl -X DELETE \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.delete(endpoint, headers=headers) print(response.status_code) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.delete(endpoint, { headers }) .then(res => console.log(res.status)) .catch(console.error) ``` *** ### Get all operation files of a field GET `/users/{leafUserId}/fields/{fieldId}/operations/files` Returns a paginated list of machine files associated with the specified field. #### Parameters | Parameter | Type | Description | | --------------- | ----------------- | ----------------------------------------------------------------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `id` | string | Filter by file ID. | | `operationType` | string | `harvested`, `planted`, `applied`, or `tillage`. | | `provider` | string | `CNHI`, `JohnDeere`, `Trimble`, `ClimateFieldView`, `AgLeader`, `Stara`, or `Leaf`. | | `origin` | string | `provider`, `automerged`, `merged`, or `uploaded`. | | `crop` | string | Crop name filter. | | `variety` | string | Variety name filter. | | `startTime` | string (ISO 8601) | Files with operations starting after this time. | | `endTime` | string (ISO 8601) | Files with operations ending before this time. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Sorting order with optional `,asc` or `,desc` suffix. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files?operationType=harvested' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"operationType": "harvested"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { operationType: 'harvested' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "abbac24d-7f13-414a-989a-ee5dc9de624b", "operationType": "harvested", "origin": "automerged", "startTime": "2017-10-27T08:59:58Z", "endTime": "2017-10-27T09:40:33Z", "crops": ["corn"], "varieties": ["Corn"], "providerFileId": "cacde0d5-55b9-4bff-bf2c-05ec1def1c95", "provider": "Leaf", "leafUserId": "dcb6fd16-b6f4-40bc-805e-659c7f7350d6" } ] ``` *** ### Get an operation file of a field GET `/users/{leafUserId}/fields/{fieldId}/operations/files/{fileId}` Returns a single machine file associated with the specified field. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `fileId` | path (UUID) | The file ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files/{fileId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files/{fileId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files/{fileId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "75127023-190a-4579-b76c-ccbcfcf00d3c", "operationType": "harvested", "origin": "automerged", "startTime": "2017-10-27T08:59:58Z", "endTime": "2017-10-27T09:40:33Z", "crops": ["corn"], "varieties": ["Corn"], "providerFileId": "a3602817-57e4-4056-bdef-4fb687ba4c2e", "provider": "Leaf", "leafUserId": "01a17a22-e6fa-4d83-b343-ea23eddbd936" } ``` *** ### Get fields by geometry POST `/users/{leafUserId}/fields/intersects` Returns all fields that intersect with the provided GeoJSON `MultiPolygon` geometry. The `intersectionThreshold` parameter (default `0.01`, range 0.01–100) sets the minimum overlap percentage required. The API checks both "intersection by field" and "intersection by geometry" ratios and returns the field if either exceeds the threshold. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | #### Request body ```json theme={null} { "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137] ]]] }, "intersectionThreshold": 3 } ``` #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"geometry":{"type":"MultiPolygon","coordinates":[[[[-93.48821,41.77137],[-93.48817,41.77143],[-93.48821,41.76068],[-93.48821,41.77137]]]]},"intersectionThreshold":3}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/intersects' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/intersects" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "geometry": { "type": "MultiPolygon", "coordinates": [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] }, "intersectionThreshold": 3 } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/intersects' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { geometry: { type: 'MultiPolygon', coordinates: [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] }, intersectionThreshold: 3 } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a list of field objects that match the intersection criteria. *** ### Get intersection of fields POST `/users/{leafUserId}/fields/intersect` Returns the `MultiPolygon` geometry representing the intersection of the specified fields. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | #### Request body ```json theme={null} ["field-id-1", "field-id-2"] ``` #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '["field-id-1", "field-id-2"]' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/intersect' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/intersect" headers = {"Authorization": f"Bearer {TOKEN}"} data = ["field-id-1", "field-id-2"] response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/intersect' const headers = { Authorization: `Bearer ${TOKEN}` } const data = ['field-id-1', 'field-id-2'] axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "type": "MultiPolygon", "coordinates": [[[ [-89.84388, 39.71943], [-89.84392, 39.72439], [-89.83936, 39.72539], [-89.83928, 39.71951], [-89.84388, 39.71943] ]]] } ``` *** ### Sync fields manually POST `/users/{leafUserId}/fields/sync` Schedules a sync to fetch field boundaries from connected providers. Use this when `fieldsAutoSync` is disabled. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/sync' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/sync" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.post(endpoint, headers=headers) print(response.status_code) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/sync' const headers = { Authorization: `Bearer ${TOKEN}` } axios.post(endpoint, null, { headers }) .then(res => console.log(res.status)) .catch(console.error) ``` *** ### Enable a preview field POST `/users/{leafUserId}/fields/{id}/enableSync` Removes a field from `PREVIEW` mode and queues it for the next sync. The field status changes to `WAITING`, and after syncing it becomes `PROCESSED`. Use this when the `customDataSync` configuration is enabled. You can also activate all fields under a grower at once with the [growers enableSync](/api-reference/growers#enable-preview-fields-by-grower) endpoint. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The field ID. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}/enableSync' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}/enableSync" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.post(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{id}/enableSync' const headers = { Authorization: `Bearer ${TOKEN}` } axios.post(endpoint, null, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ### Upload a field to provider POST `/users/{leafUserId}/fields/{fieldId}/integration/{providerName}` Pushes a Leaf field boundary to a connected provider. Supported providers: `JohnDeere`, `ClimateFieldView`. The API prevents sending a field back to the provider it was fetched from to avoid recursive syncs. #### Parameters | Parameter | Type | Description | | ---------------- | -------------- | ---------------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `providerName` | path (string) | `JohnDeere` or `ClimateFieldView`. | | `organizationId` | query (string) | Required for John Deere uploads. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/integration/JohnDeere?organizationId=428214' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/integration/JohnDeere" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"organizationId": "428214"} response = requests.post(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/integration/JohnDeere' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { organizationId: '428214' } axios.post(endpoint, null, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns one object keyed by the Leaf field ID. Inside that object, the provider name maps to the provider-specific upload response. ```json theme={null} { "ba518264-7f2a-11ee-b962-0242ac120002": { "JohnDeere": { "field_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "farm_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "grower_id": "g1h2i3j4-k5l6-7890-abcd-ef1234567890", "boundary_id": "b1c2d3e4-f5a6-7890-abcd-ef1234567890" } } } ``` *** ## Boundaries ### Get all boundaries GET `/users/{leafUserId}/fields/{fieldId}/boundaries` Returns all boundaries (active and inactive) for a field. Leaf keeps a history of all boundary changes. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "90060545-d448-493a-965f-625a17916067", "status": "ACTIVE", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-89.84392, 39.72439], [-89.84388, 39.71943], [-89.83928, 39.71951], [-89.83936, 39.72539], [-89.84392, 39.72439] ]]] }, "area": { "value": 23.659, "unit": "ha" }, "validity": "VALID", "createdTime": "2024-01-10T03:33:51.528534Z", "updatedTime": "2024-01-10T03:33:51.528534Z" } ] ``` *** ### Get a boundary GET `/users/{leafUserId}/fields/{fieldId}/boundaries/{boundaryId}` Returns a single boundary by ID. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `boundaryId` | path (UUID) | The boundary ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries/{boundaryId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries/{boundaryId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries/{boundaryId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single boundary object (same shape as objects in the [Get all boundaries](#get-all-boundaries) response). *** ### Get active boundary GET `/users/{leafUserId}/fields/{fieldId}/boundary` Returns the currently active boundary for a field. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single boundary object. *** ### Update active boundary PUT `/users/{leafUserId}/fields/{fieldId}/boundary` Replaces the active boundary with a new geometry. The previous active boundary is preserved as an inactive historical record. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | #### Request body ```json theme={null} { "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137] ]]] } } ``` #### Request ```bash cURL theme={null} curl -X PUT \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"geometry":{"type":"MultiPolygon","coordinates":[[[[-93.48821,41.77137],[-93.48817,41.77143],[-93.48821,41.76068],[-93.48821,41.77137]]]]}}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "geometry": { "type": "MultiPolygon", "coordinates": [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] } } response = requests.put(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { geometry: { type: 'MultiPolygon', coordinates: [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] } } axios.put(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "g7941ef8-iddf-42c1-b43c-d36b0df369e8", "status": "ACTIVE", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137] ]]] }, "area": { "value": 0.197, "unit": "ha" }, "validity": "VALID", "createdTime": "2024-01-15T19:48:51.017280Z", "updatedTime": "2024-01-15T19:48:51.017280Z" } ``` *** ## Farms ### Get all farms GET `/farms` Returns a paginated list of all farms. #### Parameters | Parameter | Type | Description | | ---------------- | ------------- | ----------------------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user. | | `growerId` | integer | Filter by grower ID. | | `provider` | string | Filter by provider name. | | `name` | string | Filter by farm name. | | `providerFarmId` | string | Filter by the farm ID on the provider side. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Sorting order with optional `,asc` or `,desc` suffix. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/farms?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/farms" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "UUID"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/farms' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { leafUserId: 'UUID' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": 1538766, "name": "Main Farm", "providerId": 2, "providerName": "JohnDeere", "providerFarmId": "2f4a03ed-ac81-4c6d-810d-1db6b47baec2", "providerFarmName": "Main Farm", "leafUserId": "ace92e9c-2e83-4d85-ab34-1f76a480abc8", "fieldIds": ["6595418e-11d2-4260-9e6b-e8c452fb8375"], "growerId": 12345, "createdTime": "2024-01-06T09:34:11.759672Z", "updatedTime": "2024-01-07T09:15:42.855759Z" } ] ``` *** ### Get a farm GET `/users/{leafUserId}/farms/{id}` Returns a single farm by ID. #### Parameters | Parameter | Type | Description | | ------------ | -------------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (integer) | The farm ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single farm object (same shape as objects in the [Get all farms](#get-all-farms) response). *** ### Create a farm POST `/users/{leafUserId}/farms` Creates a farm for the specified Leaf user. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | #### Request body ```json theme={null} { "name": "South Farm", "growerId": 873300016 } ``` #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"name":"South Farm","growerId":873300016}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"name": "South Farm", "growerId": 873300016} response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'South Farm', growerId: 873300016 } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns the created farm object. *** ### Update a farm PUT `/users/{leafUserId}/farms/{id}` Updates a farm's `name` and/or `growerId`. #### Parameters | Parameter | Type | Description | | ------------ | -------------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (integer) | The farm ID. | #### Request body ```json theme={null} { "name": "Updated Farm Name", "growerId": 873300016 } ``` #### Request ```bash cURL theme={null} curl -X PUT \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"name":"Updated Farm Name","growerId":873300016}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"name": "Updated Farm Name", "growerId": 873300016} response = requests.put(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/farms/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'Updated Farm Name', growerId: 873300016 } axios.put(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns the updated farm object. # Machine Files & Upload Source: https://docs.withleaf.io/api-reference/files List, retrieve, and upload machine files from equipment monitors. Manage batch uploads and track processing status through Leaf's conversion pipeline. Use the files endpoints to inspect individual machine files after Leaf ingests them from a provider or manual upload. This page also covers batch upload endpoints, processing-status inspection, and helper endpoints for uncovered or outside-field data. For conceptual background, see [Machine Data Overview](/machine-data/overview) and [Uploading Files](/machine-data/uploading-files). ## Base URL ``` https://api.withleaf.io/services/operations/api ``` ## Endpoints ### Machine files | Method | Path | Description | | ----------------- | --------------------------------- | ------------------------------------------------------------- | | GET | `/files` | [Get all machine files](#get-all-machine-files) | | GET | `/files/{id}` | [Get a machine file](#get-a-machine-file) | | GET | `/files/{id}/summary` | [Get file summary](#get-file-summary) | | GET | `/files/{id}/standardGeoparquet` | [Get file standardGeoParquet](#get-file-standardgeoparquet) | | GET | `/files/{id}/polygonGeoparquet` | [Get file polygonGeoParquet](#get-file-polygongeoparquet) | | GET | `/files/{id}/units` | [Get file units](#get-file-units) | | GET | `/files/{id}/status` | [Get file status](#get-file-status) | | GET | `/files/{id}/outsideFieldGeojson` | [Get file outsideFieldGeoJSON](#get-file-outsidefieldgeojson) | | GET | `/files/outsideFieldGeojson` | [Get all outsideFieldGeoJSON](#get-all-outsidefieldgeojson) | | GET | `/files/uncoveredFiles` | [Get uncovered files](#get-uncovered-files) | | POST | `/files/merge` | [Merge files](#merge-files) | ### Batch upload | Method | Path | Description | | ----------------- | -------------------- | ------------------------------------------------- | | POST | `/batch` | [Upload a file](#upload-a-file) | | GET | `/batch/{id}` | [Get a batch](#get-a-batch) | | GET | `/batch` | [Get all batches](#get-all-batches) | | PUT | `/batch/{id}/retry` | [Retry a batch](#retry-a-batch) | | GET | `/batch/{id}/status` | [Get batch files status](#get-batch-files-status) | To access machine files, you need a Leaf user with valid provider credentials. See the Users and Integrations documentation to set up credentials. *** ## Get all machine files GET `/files` Returns a paginated list of machine files for the authenticated API owner. ### Parameters | Parameter | Type | Description | | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `leafUserId` | string | UUID of a Leaf user | | `provider` | string | `CNHI`, `JohnDeere`, `Trimble`, `ClimateFieldView`, `AgLeader`, `RavenSlingshot`, `Stara`, or `Leaf` | | `status` | string | `processed`, `failed`, or `processing` | | `origin` | string | `provider`, `automerged`, `merged`, or `uploaded` | | `organizationId` | string | Provider organization ID (John Deere only) | | `batchId` | string | UUID of the upload batch | | `fileId` | string | Filter by a specific file ID | | `fileFormat` | string | Filter by file format (e.g., `SHAPEFILE`, `CN1`, `ISO11783`, `AGDATA`) | | `createdTime` | string | ISO 8601 timestamp. Returns files created on or after this time | | `startTime` | string | ISO 8601 timestamp. Returns files with operations starting on or after this time | | `updatedTime` | string | ISO 8601 timestamp. Returns files updated on or after this time | | `endTime` | string | ISO 8601 timestamp. Returns files with operations ending on or before this time | | `operationStartTime` | string | ISO 8601 timestamp. Same as `startTime` (alternative parameter name) | | `operationEndTime` | string | ISO 8601 timestamp. Same as `endTime` (alternative parameter name) | | `operationType` | string | `applied`, `planted`, `harvested`, or `tillage` | | `minArea` | number | Minimum operation area in square meters | | `providerFileId` | string | The file ID from the original provider | | `standard` | boolean | Filter by whether the file has a standardGeojson | | `page` | integer | Page number (default `0`) | | `size` | integer | Page size (max `100`) | | `sort` | string | Sort order. Valid fields: `id`, `fileName`, `createdTime`, `updatedTime`, `origin`, `leafUserId`, `sizeInBytes`, `provider`, `organizationId`, `fileFormat`. Append `,asc` or `,desc` | The default page size is 20 when `page` and `size` are not set. ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers, params={'leafUserId': 'UUID'}) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { leafUserId: 'UUID' } }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get a machine file GET `/files/{id}` Returns a single machine file by its UUID. ### Parameters | Parameter | Type | Description | | --------- | ---- | ---------------- | | `id` | path | UUID of the file | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/{id}' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get file summary GET `/files/{id}/summary` Returns the summary for a machine file, containing aggregated statistics for the file's data points. ### Parameters | Parameter | Type | Description | | --------- | ---- | ---------------- | | `id` | path | UUID of the file | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/{id}/summary' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/summary' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/summary' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get file standardGeoParquet GET `/files/{id}/standardGeoparquet` Returns a URL to the standard GeoParquet file. You must enable the `enableGeoparquetOutput` configuration to use this endpoint. ### Parameters | Parameter | Type | Description | | --------- | ---- | ---------------- | | `id` | path | UUID of the file | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/{id}/standardGeoparquet' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/standardGeoparquet' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/standardGeoparquet' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get file polygonGeoParquet GET `/files/{id}/polygonGeoparquet` Returns a URL to the polygon GeoParquet file. You must enable the `enableGeoparquetOutput` configuration to use this endpoint. ### Parameters | Parameter | Type | Description | | --------- | ---- | ---------------- | | `id` | path | UUID of the file | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/{id}/polygonGeoparquet' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/polygonGeoparquet' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/polygonGeoparquet' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get file units GET `/files/{id}/units` Returns the property-to-unit mapping for the machine file. Properties vary by operation type but use standardized keys across providers. ### Parameters | Parameter | Type | Description | | --------- | ---- | ---------------- | | `id` | path | UUID of the file | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/{id}/units' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/units' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/units' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get file status GET `/files/{id}/status` Returns the processing status for each step of Leaf's pipeline for the specified machine file. ### Parameters | Parameter | Type | Description | | --------- | ---- | ---------------- | | `id` | path | UUID of the file | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/{id}/status' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/status' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/status' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "originalFile": { "status": "processed", "message": "ok" }, "rawGeojson": { "status": "processed", "message": "ok" }, "standardGeojson": { "status": "processed", "message": "ok" }, "filteredGeojson": { "status": "processed", "message": "ok" }, "propertiesPNGs": { "status": "processed", "message": "ok" }, "zippedPNGs": { "status": "processed", "message": "ok" }, "summary": { "status": "processed", "message": "ok" }, "units": { "status": "processed", "message": "ok" } } ``` Each key represents a pipeline step. The `status` value is one of: | Status | Meaning | | ----------- | ------------------------------------------------------------------------------------- | | `processed` | Step completed successfully | | `failed` | Step failed — see `message` for details | | `skipped` | Step was skipped due to an earlier failure or a configuration that prevents execution | ### Common failure messages | Message | Cause | | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `no points passed the filter` | All data points were removed during cleanup. The file may have 0 valid points. Toggle the `cleanupStandardGeojson` configuration to control this behavior. | | `unsupported operation type: {type}` | The detected operation type is not one of the four supported types (planting, application, harvest, tillage). | | `missing required properties: {properties}` | One or more required properties from Leaf's standard schema were not found in the file. | | `Failed to convert file on provider batch processing` | Leaf could not extract valid data from the provider file. Verify the file structure and format. | *** ## Get file outsideFieldGeoJSON GET `/files/{id}/outsideFieldGeojson` Returns a GeoJSON file containing data points from the machine file that do not fall within any field boundary. Requires both `splitOperationsByField` and `enableOutsideFieldGeojson` configurations to be enabled. ### Parameters | Parameter | Type | Description | | --------- | ---- | ---------------- | | `id` | path | UUID of the file | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/{id}/outsideFieldGeojson' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/outsideFieldGeojson' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/{id}/outsideFieldGeojson' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "fields": ["uuid"], "featureCount": 21, "outsideFieldGeojson": "URL", "downloadOutsideFieldGeojson": "URL" } ``` *** ## Get all outsideFieldGeoJSON GET `/files/outsideFieldGeojson` Returns a list of all machine files that have data points outside field boundaries. Requires both `splitOperationsByField` and `enableOutsideFieldGeojson` configurations to be enabled. ### Parameters | Parameter | Type | Description | | ------------ | ------- | ---------------------------- | | `leafUserId` | string | UUID of a Leaf user | | `page` | integer | Page number (default `0`) | | `size` | integer | Page size (max `100`) | | `sort` | string | Sort order (e.g., `id,desc`) | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/outsideFieldGeojson?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/outsideFieldGeojson' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers, params={'leafUserId': 'UUID'}) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/outsideFieldGeojson' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { leafUserId: 'UUID' } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "id": "uuid", "fields": ["uuid"], "featureCount": 21, "outsideFieldGeojson": "URL", "downloadOutsideFieldGeojson": "URL" } ] ``` *** ## Get uncovered files GET `/files/uncoveredFiles` Returns a list of machine file IDs that did not generate field operations because they do not intersect with any field boundary. ### Parameters | Parameter | Type | Description | | ------------ | ------ | --------------------------------- | | `leafUserId` | string | **Required.** UUID of a Leaf user | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files/uncoveredFiles?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/uncoveredFiles' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers, params={'leafUserId': 'UUID'}) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/uncoveredFiles' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { leafUserId: 'UUID' } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "files": [ "c3ad6c7b-19b8-4cd7-580a-dfab82043465", "c3ad6c7b-c472-49e9-aab2-7ad222843465" ] } ``` *** ## Merge files POST `/files/merge` Merges two or more machine files into a single file. Processing is asynchronous — poll the returned file ID to check status. All files must belong to the same Leaf user, share the same operation type, and have `processed` status. ### Request body ```json theme={null} { "ids": ["fileId1", "fileId2"] } ``` ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"ids": ["fileId1", "fileId2"]}' \ 'https://api.withleaf.io/services/operations/api/files/merge' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/files/merge' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.post(endpoint, headers=headers, json={'ids': ['fileId1', 'fileId2']}) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/files/merge' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.post(endpoint, { ids: ['fileId1', 'fileId2'] }, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "uuid", "status": "SENT_TO_MERGE" } ``` *** ## Upload a file POST `/batch` Uploads a `.zip` file containing operation data. Leaf detects the files inside the archive, creates machine file entries, and processes them asynchronously. ### Parameters | Parameter | Type | Description | | ------------ | ------ | --------------------------------------------------------------------------------------------------------------------------- | | `leafUserId` | string | **Required.** UUID of the Leaf user | | `provider` | string | **Required.** `Other`, `Leaf`, `ClimateFieldView`, `CNHI`, `JohnDeere`, `Trimble`, `AgLeader`, `RavenSlingshot`, or `Stara` | | `fileFormat` | string | Optional. Hints at the file format inside the archive | Maximum upload size is 3 GB. Set `provider` to `Other` if you are unsure of the format. Leaf auto-detects files from supported providers. ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@data.zip' \ 'https://api.withleaf.io/services/operations/api/batch?leafUserId=UUID&provider=JohnDeere' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/batch' headers = {'Authorization': f'Bearer {TOKEN}'} files = {'file': open('data.zip', 'rb')} params = {'leafUserId': 'UUID', 'provider': 'JohnDeere'} response = requests.post(endpoint, headers=headers, files=files, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/batch' const headers = { 'Authorization': `Bearer ${TOKEN}` } const params = { leafUserId: 'UUID', provider: 'JohnDeere' } const form = new FormData() form.append('file', fs.createReadStream('data.zip')) axios.post(endpoint, form, { headers: { ...headers, ...form.getHeaders() }, params }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "996aea67-52bc-4d4b-9b77-028756dc0ee9", "leafUserId": "ede8f781-1d55-4b2d-83a1-6785ddab6e1d", "fileName": "data.zip", "size": 8652951, "provider": "JohnDeere", "status": "RECEIVED", "uploadTimestamp": "2021-03-12T19:50:55.567755Z" } ``` ### Batch status values | Status | Meaning | | ----------- | --------------------------------------------------------------------- | | `RECEIVED` | Default state after upload | | `PROCESSED` | All files in the batch were processed and at least one succeeded | | `FAILED` | No files in the batch succeeded. Check `statusDetails` for the reason | *** ## Get a batch GET `/batch/{id}` Returns a single batch upload by its UUID, including the list of generated machine file IDs. ### Parameters | Parameter | Type | Description | | --------- | ---- | ----------------- | | `id` | path | UUID of the batch | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/batch/{id}' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/batch/{id}' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/batch/{id}' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "9b561906-efac-43a3-9378-641e3698da5d", "leafUserId": "1481bc9b-cdc7-45c1-9f0e-592da6306dfe", "provider": "Other", "status": "PROCESSED", "leafFiles": [ "f14203df-4144-43b7-a383-2ed321f395ce", "810b1475-cb49-437b-8658-d29038ce2fa4" ] } ``` *** ## Get all batches GET `/batch` Returns a paginated list of batch uploads. ### Parameters | Parameter | Type | Description | | ------------------ | ------- | -------------------------------------------------------------------- | | `leafUserId` | string | UUID of a Leaf user | | `status` | string | `RECEIVED`, `PROCESSED`, or `FAILED` | | `uploadStartTime` | string | ISO 8601 timestamp. Returns batches uploaded on or after this time | | `uploadEndTime` | string | ISO 8601 timestamp. Returns batches uploaded on or before this time | | `processStartTime` | string | ISO 8601 timestamp. Returns batches processed on or after this time | | `processEndTime` | string | ISO 8601 timestamp. Returns batches processed on or before this time | | `page` | integer | Page number (default `0`) | | `size` | integer | Page size (max `100`) | | `sort` | string | Sort order (e.g., `uploadTimestamp,desc`) | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/batch' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/batch' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/batch' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "id": "9e47ae29-6a84-4a9c-9e5f-01802f6dceea", "leafUserId": "5ded9409-c99f-4379-9173-c01b1631f274", "provider": "Other", "status": "PROCESSED", "leafFiles": [ "74d5aeb6-9a0e-43c6-986c-a5f17eecbddc", "475fcad3-b534-409d-8c8b-cec4dabd1b8b" ] } ] ``` *** ## Retry a batch PUT `/batch/{id}/retry` Retries processing for a batch upload. Only reprocesses the files that did not succeed previously — existing converted files are not affected. ### Parameters | Parameter | Type | Description | | --------- | ---- | ----------------- | | `id` | path | UUID of the batch | ### Request ```bash cURL theme={null} curl -X PUT \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/batch/{id}/retry' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/batch/{id}/retry' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.put(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/batch/{id}/retry' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.put(endpoint, {}, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "36d8551f-409d-41f2-94b4-04c9fe16289b", "leafUserId": "089bb77b-2415-43df-a246-6c0a5937c774", "fileName": "data.zip", "size": 8652951, "provider": "Other", "status": "RECEIVED", "uploadTimestamp": "2021-03-12T19:50:55.567755Z" } ``` *** ## Get batch files status GET `/batch/{id}/status` Returns the processing status of each machine file generated from the batch, grouped by status. ### Parameters | Parameter | Type | Description | | --------- | ------- | ------------------------- | | `id` | path | UUID of the batch | | `page` | integer | Page number (default `0`) | | `size` | integer | Page size (max `100`) | | `sort` | string | Sort order | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/batch/{id}/status' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/batch/{id}/status' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/batch/{id}/status' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "converted": { "leafFiles": [ "06512392-8d69-4033-8127-4cc62b7176b9", "075fd0f6-af1a-433d-ad7a-e3e979179244" ] }, "processing": { "leafFiles": ["9d22cbca-03ff-47e8-ac66-f6d463d206f4"] }, "failed": { "standardGeojson": { "leafFiles": ["0abca517-09f2-4d1d-9627-9cd3147e9ec3"], "status": "failed", "message": "no points passed the filter" } } } ``` # Growers Source: https://docs.withleaf.io/api-reference/growers Create and manage grower accounts that organize farms and fields. Growers are the top-level entity grouping farm and field data for a farming operation. Use the growers endpoints to create or manage grower records and to bulk-enable preview fields for onboarding. This page is the narrow reference for grower-specific operations, while farms and fields live in the broader fields reference. For conceptual background, see [Growers](/fields/growers). ## Base URL ``` https://api.withleaf.io/services/fields/api ``` ## Endpoints | Endpoint | Method | Path | | ------------------------------------------------------------------- | ----------------- | ---------------------------------- | | [Get all growers](#get-all-growers) | GET | `/growers` | | [Get a grower](#get-a-grower) | GET | `/users/{leafUserId}/growers/{id}` | | [Create a grower](#create-a-grower) | POST | `/users/{leafUserId}/growers` | | [Update a grower](#update-a-grower) | PUT | `/users/{leafUserId}/growers/{id}` | | [Enable preview fields by grower](#enable-preview-fields-by-grower) | POST | `/growers/enableSync` | *** ### Get all growers GET `/growers` Returns a paginated list of growers available to the authenticated API owner. Use `leafUserId` to narrow the list to one Leaf user. #### Parameters | Parameter | Type | Description | | ------------------------ | ------------- | ----------------------------------------------------------------------------- | | `leafUserId` | string (UUID) | Filter by Leaf user. | | `provider` | string | Filter by provider name (`JohnDeere`, `ClimateFieldView`, `CNHI`, `Trimble`). | | `name` | string | Filter by grower name. | | `providerOrganizationId` | string | Filter by the organization ID on the provider side. | | `providerCompanyId` | string | Filter by the company ID on the provider side. | | `providerGrowerId` | string | Filter by the grower ID on the provider side. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Sorting order with optional `,asc` or `,desc` suffix. | The default page size is 20 when `page` and `size` are not set. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/growers?leafUserId=1d3ecb0f-bf3d-42db-aae6-8c45c045d28c' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/growers" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "1d3ecb0f-bf3d-42db-aae6-8c45c045d28c"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/growers' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { leafUserId: '1d3ecb0f-bf3d-42db-aae6-8c45c045d28c' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": 873300016, "name": "1Grower", "leafUserId": "1d3ecb0f-bf3d-42db-aae6-8c45c045d28c", "providerName": "JohnDeere", "providerId": 2, "providerGrowerId": "1Grower", "farmIds": [], "createdTime": "2023-06-06T03:31:39.966630Z", "updatedTime": "2023-06-07T20:01:14.814346Z" } ] ``` For John Deere, growers correspond to John Deere "Clients." The `name` property comes directly from the Client name. *** ### Get a grower GET `/users/{leafUserId}/growers/{id}` Returns a single grower by ID. #### Parameters | Parameter | Type | Description | | ------------ | -------------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (integer) | The grower ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/users/1d3ecb0f-bf3d-42db-aae6-8c45c045d28c/growers/873300016' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/1d3ecb0f-bf3d-42db-aae6-8c45c045d28c/growers/873300016" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/1d3ecb0f-bf3d-42db-aae6-8c45c045d28c/growers/873300016' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": 873300016, "name": "1Grower", "leafUserId": "1d3ecb0f-bf3d-42db-aae6-8c45c045d28c", "providerName": "JohnDeere", "providerId": 2, "providerGrowerId": "1Grower", "farmIds": [], "createdTime": "2023-06-06T03:31:39.966630Z", "updatedTime": "2023-06-07T20:01:14.814346Z" } ``` *** ### Create a grower POST `/users/{leafUserId}/growers` Creates a grower for the specified Leaf user. Leaf assigns an auto-generated integer ID. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | #### Request body ```json theme={null} { "name": "Smith Farms" } ``` Only the `name` field is accepted. #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"name":"Smith Farms"}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/growers' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/growers" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"name": "Smith Farms"} response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/growers' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'Smith Farms' } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns the created grower object. *** ### Update a grower PUT `/users/{leafUserId}/growers/{id}` Updates the grower's `name`. #### Parameters | Parameter | Type | Description | | ------------ | -------------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (integer) | The grower ID. | #### Request body ```json theme={null} { "name": "Updated Grower Name" } ``` #### Request ```bash cURL theme={null} curl -X PUT \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"name":"Updated Grower Name"}' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/growers/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/users/{leafUserId}/growers/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"name": "Updated Grower Name"} response = requests.put(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/growers/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: 'Updated Grower Name' } axios.put(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns the updated grower object. *** ### Enable preview fields by grower POST `/growers/enableSync` Enables sync for John Deere fields in `PREVIEW` status under the specified growers. Matching fields are moved to `WAITING` and queued for sync. Use this when the `customDataSync` configuration is enabled. This is faster than enabling fields individually when you want to onboard an entire grower. #### Request body ```json theme={null} { "growerIds": [873300016, 873300017] } ``` #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"growerIds":[873300016,873300017]}' \ 'https://api.withleaf.io/services/fields/api/growers/enableSync' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/fields/api/growers/enableSync" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"growerIds": [873300016, 873300017]} response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/growers/enableSync' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { growerIds: [873300016, 873300017] } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "affectedFieldIds": [ "field-id-1", "field-id-2" ] } ``` # Sync Summary Source: https://docs.withleaf.io/api-reference/integrations Check how many growers, farms, and fields Leaf has synced from each provider for a Leaf user. Verify connections are working and expected data came through. After you connect a Leaf user to a provider, this endpoint tells you what actually synced. It returns counts of growers, farms, and fields per provider for a given Leaf user, along with the most recent sync timestamp. Use it to verify a new connection is working, confirm that `organizationDataSync` or `customDataSync` settings are producing the expected scope, or troubleshoot why data is missing. ## Base URL ``` https://api.withleaf.io/services/integrations/api ``` ## Endpoints | Endpoint | Method | Path | | ------------------------------------------------------- | ---------------- | ------------ | | [Get integration resources](#get-integration-resources) | GET | `/resources` | *** ## FMIS structure by provider Each provider organizes farm data with a different hierarchy. The table below shows which structural levels are available from each provider when Leaf syncs resources. | Provider | Grower | Farm | Field | | --------------------------- | ------ | ---- | ----- | | John Deere | ✓ | ✓ | ✓ | | Climate FieldView | ✗ | ✓ | ✓ | | CNHI (AFS Connect - Legacy) | ✓ | ✓ | ✓ | | CNHI FieldOps | ✓ | ✓ | ✓ | | Trimble | ✓ | ✓ | ✓ | | Stara | ✗ | ✗ | ✓ | | Raven | ✓ | ✓ | ✓ | | AgVance | ✓ | ✓ | ✓ | | Sentera | ✗ | ✗ | ✓ | Climate FieldView does not expose a grower-level entity. Stara only provides field-level data. *** ## Get integration resources GET `/resources` Returns summaries of synced resources per provider for the authenticated API owner, optionally filtered by Leaf user and/or provider. The response includes counts of growers, farms, and fields along with the most recent sync reference time. ### Parameters | Parameter | Type | Description | | ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `provider` | string | Filter by provider name (`JohnDeere`, `ClimateFieldView`, `CNHI`, `CNHIFieldOps`, `Trimble`, `Stara`, `Raven`, `AgVance`, `Sentera`). | | `leafUserId` | string (UUID) | Filter by Leaf user. | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/integrations/api/resources?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/integrations/api/resources" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"leafUserId": "UUID"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/integrations/api/resources' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { leafUserId: 'UUID' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "message": "SUCCESS", "summaries": [ { "provider": "JohnDeere", "leafUserId": "7494c90e-28b8-4bb2-9ede-95c1cc894349", "growers": 12, "farms": 12, "fields": 100, "syncReferenceTime": "2023-08-30T18:39:33.230612Z" } ] } ``` Use the `provider` parameter to compare resource counts across providers for a single Leaf user, or omit it to see all providers at once. # Irrigation Source: https://docs.withleaf.io/api-reference/irrigation Retrieve irrigation equipment, applied irrigation records, and field irrigation data from Lindsay and Valley systems through the Leaf API. Use these endpoints to retrieve irrigation equipment, applied irrigation records, and field irrigation data from Lindsay and Valley systems connected through a Leaf user. For conceptual background, see [Irrigation Overview](/irrigation/overview). ## Base URL ``` https://api.withleaf.io/services/irrigation/api ``` ## Endpoints | Endpoint | Method | Path | | ----------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------- | | [Get all irrigation equipment](#get-all-irrigation-equipment) | GET | `/users/{leafUserId}/irrigation-equipment` | | [Get an irrigation equipment](#get-an-irrigation-equipment) | GET | `/users/{leafUserId}/irrigation-equipment/{id}` | | [Get as-applied irrigation](#get-as-applied-irrigation) | GET | `/users/{leafUserId}/irrigation/applied-irrigation` | | [Get an irrigation activity](#get-an-irrigation-activity) | GET | `/users/{leafUserId}/irrigation/applied-irrigation/{id}` | | [Get an irrigation activity standard GeoJSON](#get-an-irrigation-activity-standard-geojson) | GET | `/users/{leafUserId}/irrigation/applied-irrigation/{id}/standardGeojson` | | [Get an irrigation activity units](#get-an-irrigation-activity-units) | GET | `/users/{leafUserId}/irrigation/applied-irrigation/{id}/units` | | [Get all irrigated fields](#get-all-irrigated-fields) | GET | `/users/{leafUserId}/irrigation/fields` | | [Get an irrigated field](#get-an-irrigated-field) | GET | `/users/{leafUserId}/irrigation/fields/{fieldId}` | | [Get an irrigated field activity](#get-an-irrigated-field-activity) | GET | `/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}` | | [Get an irrigated field activity standard GeoJSON](#get-an-irrigated-field-activity-standard-geojson) | GET | `/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}/standardGeojson` | | [Get an irrigated field activity units](#get-an-irrigated-field-activity-units) | GET | `/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}/units` | *** ## Equipment ### Get all irrigation equipment GET `/users/{leafUserId}/irrigation-equipment` Lists all irrigation system equipment available for a given Leaf user. #### Parameters | Parameter | Type | Description | | --------------------- | ----------- | ------------------------------------------------ | | `leafUserId` | path (UUID) | The Leaf user ID. | | `providerEquipmentId` | string | Filter by the equipment ID on the provider side. | | `provider` | string | Filter by provider (`Lindsay` or `Valley`). | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation-equipment?provider=Lindsay' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation-equipment" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"provider": "Lindsay"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation-equipment' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { provider: 'Lindsay' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "uuid", "providerEquipmentId": "uuid", "provider": "Lindsay", "name": "HHD 700C", "type": "pivot", "pivotLength": { "value": 0.0, "unit": "m" }, "endgunLength": { "value": 0.0, "unit": "m" }, "pivotRuntime": { "value": 0.0, "unit": "hr" }, "brand": "unknown", "originalEquipmentData": { "equipmentType": "", "equipmentSubType": "" } } ] ``` *** ### Get an irrigation equipment GET `/users/{leafUserId}/irrigation-equipment/{id}` Returns a single irrigation equipment record by ID. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | ----------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The equipment ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation-equipment/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation-equipment/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation-equipment/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single equipment object (same shape as the objects in the [Get all irrigation equipment](#get-all-irrigation-equipment) response). *** ## Applied Irrigation ### Get as-applied irrigation GET `/users/{leafUserId}/irrigation/applied-irrigation` Lists all irrigation activities from supported providers, summarized by day. #### Parameters | Parameter | Type | Description | | ------------ | ----------------- | -------------------------------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `provider` | string | Filter by provider (`Lindsay` or `Valley`). | | `startTime` | string (ISO 8601) | Return irrigation data from this timestamp onward. | | `endTime` | string (ISO 8601) | Return irrigation data up to this timestamp. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/applied-irrigation?provider=Lindsay&startTime=2024-01-01T00:00:00Z&endTime=2024-01-31T23:59:59Z' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/applied-irrigation" headers = {"Authorization": f"Bearer {TOKEN}"} params = { "provider": "Lindsay", "startTime": "2024-01-01T00:00:00Z", "endTime": "2024-01-31T23:59:59Z" } response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/applied-irrigation' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { provider: 'Lindsay', startTime: '2024-01-01T00:00:00Z', endTime: '2024-01-31T23:59:59Z' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "uuid", "provider": "Lindsay", "standardGeojson": "url.json", "downloadStandardGeojson": "url.json", "leafUserId": "uuid", "apiOwnerUsername": "apiowner@withleaf.io", "summary": { "type": "Feature", "properties": { "depth": { "avg": 4.87, "sum": 28.36, "min": 2.53, "max": 7.49, "unit": "mm" }, "totalArea": { "value": 52.72, "unit": "ha" }, "totalVolume": { "value": 1604.7, "unit": "L" }, "totalPowerOn": { "value": 16.7, "unit": "hr" } }, "geometry": {} }, "equipment": [ { "id": "uuid", "name": "My Pivot", "type": "pivot", "providerEquipmentId": "d0245010-157d-4988-96a2-5f3637098475" } ], "createdTime": "2024-03-04T00:31:25.497Z", "startTime": "2024-01-07T00:00:00Z", "endTime": "2024-01-07T23:59:59Z" } ] ``` *** ### Get an irrigation activity GET `/users/{leafUserId}/irrigation/applied-irrigation/{id}` Returns a single as-applied irrigation activity by ID. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | --------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The irrigation activity ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/applied-irrigation/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/applied-irrigation/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/applied-irrigation/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single irrigation activity object (same shape as the objects in the [Get as-applied irrigation](#get-as-applied-irrigation) response). *** ### Get an irrigation activity standard GeoJSON GET `/users/{leafUserId}/irrigation/applied-irrigation/{id}/standardGeojson` Returns the standard GeoJSON links for a single irrigation activity. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | --------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The irrigation activity ID. | #### Response ```json theme={null} { "standardGeojson": "url.json", "downloadStandardGeojson": "url.json" } ``` *** ### Get an irrigation activity units GET `/users/{leafUserId}/irrigation/applied-irrigation/{id}/units` Returns the units used by a single irrigation activity. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | --------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `id` | path (UUID) | The irrigation activity ID. | #### Response ```json theme={null} { "depth": "mm", "area": "ha", "volume": "L" } ``` *** ## Irrigated Fields ### Get all irrigated fields GET `/users/{leafUserId}/irrigation/fields` Lists all fields that have received irrigation. #### Parameters | Parameter | Type | Description | | ------------- | ----------------- | -------------------------------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `equipmentId` | string (UUID) | Filter by Leaf equipment ID. | | `startTime` | string (ISO 8601) | Return irrigation data from this timestamp onward. | | `endTime` | string (ISO 8601) | Return irrigation data up to this timestamp. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields?equipmentId={equipmentId}&startTime=2024-01-01T00:00:00Z&endTime=2024-01-31T23:59:59Z' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields" headers = {"Authorization": f"Bearer {TOKEN}"} params = { "equipmentId": "{equipmentId}", "startTime": "2024-01-01T00:00:00Z", "endTime": "2024-01-31T23:59:59Z" } response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { equipmentId: '{equipmentId}', startTime: '2024-01-01T00:00:00Z', endTime: '2024-01-31T23:59:59Z' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "fieldId": "95eb7d79-b93d-4fc2-877a-3f2b366f8beb", "lastIrrigationTime": "2024-02-18T23:59:59.000000Z" } ] ``` *** ### Get an irrigated field GET `/users/{leafUserId}/irrigation/fields/{fieldId}` Returns the irrigation records associated with a single field. #### Parameters | Parameter | Type | Description | | -------------- | ----------------- | -------------------------------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `irrigationId` | string (UUID) | Filter by as-applied irrigation ID. | | `provider` | string | Filter by provider (`Lindsay` or `Valley`). | | `startTime` | string (ISO 8601) | Return irrigation data from this timestamp onward. | | `endTime` | string (ISO 8601) | Return irrigation data up to this timestamp. | This endpoint relies on existing field boundaries for the Leaf user. Valley does not provide field boundaries, and Lindsay requires the FieldNET Advisor product to make them available. #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields/{fieldId}?provider=Lindsay&startTime=2024-01-01T00:00:00Z&endTime=2024-01-31T23:59:59Z' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields/{fieldId}" headers = {"Authorization": f"Bearer {TOKEN}"} params = { "provider": "Lindsay", "startTime": "2024-01-01T00:00:00Z", "endTime": "2024-01-31T23:59:59Z" } response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields/{fieldId}' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { provider: 'Lindsay', startTime: '2024-01-01T00:00:00Z', endTime: '2024-01-31T23:59:59Z' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "uuid", "fieldId": "uuid", "irrigationId": ["uuid"], "provider": "Lindsay", "standardGeojson": "url.json", "downloadStandardGeojson": "url.json", "leafUserId": "uuid", "apiOwnerUsername": "apiowner@withleaf.io", "summary": { "type": "Feature", "properties": { "depth": { "avg": 4.87, "sum": 28.36, "min": 2.53, "max": 7.49, "unit": "mm" }, "totalArea": { "value": 49.48, "unit": "ha" }, "totalVolume": { "value": 1604.7, "unit": "L" }, "totalPowerOn": { "value": 16.7, "unit": "hr" }, "coverage": { "value": 81.51, "unit": "percentage" } }, "geometry": {} }, "equipment": [ { "id": "uuid", "name": "My Pivot", "type": "pivot", "providerEquipmentId": "d0245010-157d-4988-96a2-5f3637098475" } ], "createdTime": "2024-03-04T00:31:25.497Z", "startTime": "2024-01-07T00:00:00Z", "endTime": "2024-01-07T23:59:59Z" } ] ``` *** ### Get an irrigated field activity standard GeoJSON GET `/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}/standardGeojson` Returns the standard GeoJSON links for a single irrigated field activity. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | --------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `id` | path (UUID) | The irrigation activity ID. | #### Response ```json theme={null} { "standardGeojson": "url.json", "downloadStandardGeojson": "url.json" } ``` *** ### Get an irrigated field activity units GET `/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}/units` Returns the units used by a single irrigated field activity. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | --------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `id` | path (UUID) | The irrigation activity ID. | #### Response ```json theme={null} { "depth": "mm", "area": "ha", "volume": "L" } ``` *** ### Get an irrigated field activity GET `/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}` Returns a single irrigation activity record associated with the specified field. #### Parameters | Parameter | Type | Description | | ------------ | ----------- | --------------------------- | | `leafUserId` | path (UUID) | The Leaf user ID. | | `fieldId` | path (UUID) | The field ID. | | `id` | path (UUID) | The irrigation activity ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/irrigation/api/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "uuid", "fieldId": "uuid", "irrigationId": ["uuid"], "provider": "Lindsay", "standardGeojson": "url.json", "downloadStandardGeojson": "url.json", "leafUserId": "uuid", "apiOwnerUsername": "apiowner@withleaf.io", "summary": { "type": "Feature", "properties": { "depth": { "avg": 4.87, "sum": 28.36, "min": 2.53, "max": 7.49, "unit": "mm" }, "totalArea": { "value": 49.48, "unit": "ha" }, "totalVolume": { "value": 1604.7, "unit": "L" }, "totalPowerOn": { "value": 16.7, "unit": "hr" }, "coverage": { "value": 81.51, "unit": "percentage" } }, "geometry": {} }, "equipment": [ { "id": "uuid", "name": "My Pivot", "type": "pivot", "providerEquipmentId": "d0245010-157d-4988-96a2-5f3637098475" } ], "createdTime": "2024-03-04T00:31:25.497Z", "startTime": "2024-01-07T00:00:00Z", "endTime": "2024-01-07T23:59:59Z" } ``` # Leaf Lake Source: https://docs.withleaf.io/api-reference/leaf-lake Execute SQL queries against your normalized agronomic data, USDA soil survey data, and state/county boundaries through the Leaf Lake query endpoint. Use the Leaf Lake endpoint to run SQL queries against your operation data, SSURGO soil polygons, and US state/county boundaries. Queries are automatically scoped to the authenticated API owner. Leaf Lake uses BigQuery SQL. For schema details and example queries, see [Querying Leaf Lake](/leaf-lake/querying). ## Base URL ``` https://api.withleaf.io/services/pointlake/api/v2 ``` ## Endpoints | Method | Path | Description | | ----------------- | -------------------------- | ------------------------------------------------------------------------- | | POST | `/query` | [Execute a SQL query](#execute-a-sql-query) | | GET | `/query` | [Execute a SQL query (via query parameter)](#execute-a-sql-query) | | POST | `/export-query-geoparquet` | [Export query results as GeoParquet](#export-query-results-as-geoparquet) | *** ## Execute a SQL query POST `/query` Executes a SQL query against the Leaf Lake tables and returns the results as a JSON array of rows. All queries are scoped to the authenticated API owner. You can only access data belonging to your own Leaf users. Only read queries are allowed (SELECT, WITH, ORDER BY, UNION). Write operations are not supported. This endpoint accepts both POST (SQL in the request body) and GET (SQL in the `sql` query parameter). POST is recommended for longer queries. ### POST request Send the SQL query as the request body with `Content-Type: text/plain`. | Header | Value | | --------------- | ------------------- | | `Authorization` | `Bearer YOUR_TOKEN` | | `Content-Type` | `text/plain` | ### GET request Pass the SQL as the `sql` query parameter. Useful for short queries or browser testing. ``` GET /query?sql=SELECT+operationType,+COUNT(*)+AS+cnt+FROM+points+GROUP+BY+operationType ``` ### Request body (POST) The raw SQL query as plain text. See [Querying Leaf Lake](/leaf-lake/querying) for available tables, columns, and example queries. ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: text/plain' \ -d "SELECT operationType, crop, COUNT(*) AS point_count FROM points GROUP BY operationType, crop" \ 'https://api.withleaf.io/services/pointlake/api/v2/query' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" sql = """ SELECT operationType, crop, COUNT(*) AS point_count FROM points GROUP BY operationType, crop """ response = requests.post( "https://api.withleaf.io/services/pointlake/api/v2/query", headers={ "Authorization": f"Bearer {token}", "Content-Type": "text/plain" }, data=sql ) data = response.json() ``` ```javascript JavaScript theme={null} const axios = require("axios"); const token = "YOUR_TOKEN"; const sql = ` SELECT operationType, crop, COUNT(*) AS point_count FROM points GROUP BY operationType, crop `; axios.post( "https://api.withleaf.io/services/pointlake/api/v2/query", sql, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "text/plain", }, } ) .then(({ data }) => console.log(data)) .catch(console.error); ``` ### Response The response is a JSON array where each element is an object representing one row. Column names match the aliases used in the SQL query. ```json theme={null} [ { "operationType": "planted", "crop": "corn", "point_count": 48523 }, { "operationType": "harvested", "crop": "corn", "point_count": 52104 }, { "operationType": "applied", "crop": "corn", "point_count": 31890 }, { "operationType": "harvested", "crop": "soybeans", "point_count": 44217 } ] ``` ### Query by field ID The simplest way to scope a query to a specific field is by Leaf field UUID using the `fieldIds` column on the `points` table. No WKT boundary polygon needed. ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: text/plain' \ -d "SELECT crop, ROUND(AVG(wetMassPerArea) / 62.77, 1) AS avg_yield_bu_ac, COUNT(*) AS points FROM points WHERE operationType = 'harvested' AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) GROUP BY crop" \ 'https://api.withleaf.io/services/pointlake/api/v2/query' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" field_id = "YOUR_FIELD_UUID" sql = f""" SELECT crop, ROUND(AVG(wetMassPerArea) / 62.77, 1) AS avg_yield_bu_ac, COUNT(*) AS points FROM points WHERE operationType = 'harvested' AND '{field_id}' IN UNNEST(fieldIds) GROUP BY crop """ response = requests.post( "https://api.withleaf.io/services/pointlake/api/v2/query", headers={ "Authorization": f"Bearer {token}", "Content-Type": "text/plain" }, data=sql ) data = response.json() ``` ```javascript JavaScript theme={null} const axios = require("axios"); const token = "YOUR_TOKEN"; const fieldId = "YOUR_FIELD_UUID"; const sql = ` SELECT crop, ROUND(AVG(wetMassPerArea) / 62.77, 1) AS avg_yield_bu_ac, COUNT(*) AS points FROM points WHERE operationType = 'harvested' AND '${fieldId}' IN UNNEST(fieldIds) GROUP BY crop `; axios.post( "https://api.withleaf.io/services/pointlake/api/v2/query", sql, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "text/plain", }, } ) .then(({ data }) => console.log(data)) .catch(console.error); ``` ### Spatial query example You can also filter by geography using spatial functions and a WKT boundary polygon. ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: text/plain' \ -d "SELECT crop, ROUND(AVG(wetMassPerArea) / 62.77, 1) AS avg_yield_bu_ac, COUNT(*) AS points FROM points WHERE operationType = 'harvested' AND ST_Intersects(geometry, ST_GeogFromText('POLYGON((-89.80 40.47, -89.81 40.47, -89.81 40.48, -89.80 40.48, -89.80 40.47))')) GROUP BY crop" \ 'https://api.withleaf.io/services/pointlake/api/v2/query' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" field_boundary = "POLYGON((-89.80 40.47, -89.81 40.47, -89.81 40.48, -89.80 40.48, -89.80 40.47))" sql = f""" SELECT crop, ROUND(AVG(wetMassPerArea) / 62.77, 1) AS avg_yield_bu_ac, COUNT(*) AS points FROM points WHERE operationType = 'harvested' AND ST_Intersects(geometry, ST_GeogFromText('{field_boundary}')) GROUP BY crop """ response = requests.post( "https://api.withleaf.io/services/pointlake/api/v2/query", headers={ "Authorization": f"Bearer {token}", "Content-Type": "text/plain" }, data=sql ) data = response.json() ``` ```javascript JavaScript theme={null} const axios = require("axios"); const token = "YOUR_TOKEN"; const fieldBoundary = "POLYGON((-89.80 40.47, -89.81 40.47, -89.81 40.48, -89.80 40.48, -89.80 40.47))"; const sql = ` SELECT crop, ROUND(AVG(wetMassPerArea) / 62.77, 1) AS avg_yield_bu_ac, COUNT(*) AS points FROM points WHERE operationType = 'harvested' AND ST_Intersects(geometry, ST_GeogFromText('${fieldBoundary}')) GROUP BY crop `; axios.post( "https://api.withleaf.io/services/pointlake/api/v2/query", sql, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "text/plain", }, } ) .then(({ data }) => console.log(data)) .catch(console.error); ``` ### Error handling | Status code | Meaning | | ----------- | ------------------------------------------------------------- | | `200` | Query executed successfully | | `204` | Query executed but returned no rows | | `400` | Invalid SQL syntax, unknown table/column, or non-SELECT query | | `401` | Missing or expired Bearer token | | `403` | Insufficient permissions | When a `400` error occurs, the response body contains a [Problem JSON (RFC 7807)](https://datatracker.ietf.org/doc/html/rfc7807) object with details about the issue. A `204 No Content` response has no body. Check the status code before attempting to parse JSON. *** ## Export query results as GeoParquet POST `/export-query-geoparquet` Runs a SQL query and returns the results as a downloadable GeoParquet file instead of JSON. Useful when you need to load results directly into GIS software or spatial analysis tools. The request format is the same as the query endpoint — send the SQL as the request body with `Content-Type: text/plain`. ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: text/plain' \ -d "SELECT crop, wetMassPerArea, geometry FROM points WHERE operationType = 'harvested' AND ST_Intersects(geometry, ST_GeogFromText('POLYGON((-89.80 40.47, -89.81 40.47, -89.81 40.48, -89.80 40.48, -89.80 40.47))'))" \ -o results.parquet \ 'https://api.withleaf.io/services/pointlake/api/v2/export-query-geoparquet' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" sql = """ SELECT crop, wetMassPerArea, geometry FROM points WHERE operationType = 'harvested' AND ST_Intersects(geometry, ST_GeogFromText('POLYGON((-89.80 40.47, -89.81 40.47, -89.81 40.48, -89.80 40.48, -89.80 40.47))')) """ response = requests.post( "https://api.withleaf.io/services/pointlake/api/v2/export-query-geoparquet", headers={ "Authorization": f"Bearer {token}", "Content-Type": "text/plain" }, data=sql ) with open("results.parquet", "wb") as f: f.write(response.content) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const fs = require("fs"); const token = "YOUR_TOKEN"; const sql = ` SELECT crop, wetMassPerArea, geometry FROM points WHERE operationType = 'harvested' AND ST_Intersects(geometry, ST_GeogFromText('POLYGON((-89.80 40.47, -89.81 40.47, -89.81 40.48, -89.80 40.48, -89.80 40.47))')) `; axios.post( "https://api.withleaf.io/services/pointlake/api/v2/export-query-geoparquet", sql, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "text/plain", }, responseType: "arraybuffer", } ) .then(({ data }) => fs.writeFileSync("results.parquet", Buffer.from(data))) .catch(console.error); ``` ### Response The response is a binary GeoParquet file with `Content-Type: application/octet-stream`. Save the response body to a `.parquet` file to use with tools like GeoPandas, QGIS, or DuckDB. ## What to do next * [Leaf Lake Overview](/leaf-lake/overview) — Product description and key concepts. * [Querying Leaf Lake](/leaf-lake/querying) — Schema reference and example queries for all operation types. * [Authentication](/getting-started/authentication) — How to get a Bearer token. # Leaf Link Source: https://docs.withleaf.io/api-reference/leaf-link Configure API keys and register provider applications for Leaf Link, the embeddable widget that lets growers connect their agricultural data accounts. Use the Leaf Link endpoints to create API keys for widget sessions and to register provider application credentials for embedded OAuth flows. This page is the reference for the backend setup that powers the Leaf Link UI components. For conceptual background, see [Leaf Link](/components/leaf-link). ## Overview Leaf Link widgets let your users connect their provider accounts directly from your application. To use them, you need: 1. **An API key** scoped to a Leaf user for widget authentication. 2. **Provider app registrations** so Leaf knows which provider credentials to use during the OAuth flow. **Base URL:** `https://api.withleaf.io/services/usermanagement/api` *** ## API Keys API keys authenticate Leaf Link widget sessions for a specific Leaf user. ### Endpoints | Endpoint | Method | Path | | ----------------- | -------- | ---------------------- | | Get all API keys | `GET` | `/api-keys` | | Create an API key | `POST` | `/api-keys` | | Revoke an API key | `DELETE` | `/api-keys/{apiKeyId}` | *** ### Get all API keys `GET /api-keys` Returns every API key associated with a Leaf user. #### Parameters | Parameter | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------- | | `leafUserId` | string | Yes | The UUID of the Leaf user to query keys for. | ```bash cURL theme={null} curl -X GET \ "https://api.withleaf.io/services/usermanagement/api/api-keys?leafUserId={leafUserId}" \ -H "Authorization: Bearer {token}" ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/usermanagement/api/api-keys" headers = {"Authorization": f"Bearer {token}"} params = {"leafUserId": leaf_user_id} response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( `https://api.withleaf.io/services/usermanagement/api/api-keys?leafUserId=${leafUserId}`, { headers: { Authorization: `Bearer ${token}` } } ); const keys = await response.json(); ``` #### Response ```json theme={null} [ { "key": "lk_abc123...", "expiresAt": "2025-10-01T00:00:00.000Z", "valid": true } ] ``` *** ### Create an API key `POST /api-keys` Creates a new API key for widget authentication. #### Request body | Field | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------- | | `leafUserId` | string | Yes | The UUID of the Leaf user. | | `expiresIn` | integer | No | Lifetime in seconds. Minimum `900`. Defaults to 1 year. | | `description` | string | No | A human-readable label for the key. | ```bash cURL theme={null} curl -X POST \ "https://api.withleaf.io/services/usermanagement/api/api-keys" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "leafUserId": "{leafUserId}", "expiresIn": 86400, "description": "Production widget key" }' ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/usermanagement/api/api-keys" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } body = { "leafUserId": leaf_user_id, "expiresIn": 86400, "description": "Production widget key", } response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.withleaf.io/services/usermanagement/api/api-keys", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ leafUserId, expiresIn: 86400, description: "Production widget key", }), } ); const key = await response.json(); ``` #### Response ```json theme={null} { "key": "lk_abc123...", "expiresAt": "2025-10-02T00:00:00.000Z", "valid": true } ``` *** ### Revoke an API key `DELETE /api-keys/{apiKeyId}` Permanently revokes an API key. This action cannot be undone. #### Path parameters | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------- | | `apiKeyId` | string | Yes | The ID of the key to revoke. | ```bash cURL theme={null} curl -X DELETE \ "https://api.withleaf.io/services/usermanagement/api/api-keys/{apiKeyId}" \ -H "Authorization: Bearer {token}" ``` ```python Python theme={null} import requests url = f"https://api.withleaf.io/services/usermanagement/api/api-keys/{api_key_id}" headers = {"Authorization": f"Bearer {token}"} response = requests.delete(url, headers=headers) ``` ```javascript JavaScript theme={null} await fetch( `https://api.withleaf.io/services/usermanagement/api/api-keys/${apiKeyId}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, } ); ``` *** ## Provider App Information Register your provider application credentials so Leaf Link widgets can initiate the OAuth flow on behalf of your users. All providers support the same CRUD operations, but the path shape differs depending on whether the provider uses a `clientEnvironment`. ### Endpoint pattern Providers without `clientEnvironment` use this pattern: | Endpoint | Method | Path | | ------------------ | -------- | -------------------------------- | | Get all apps | `GET` | `/app-keys/{Provider}` | | Get an app by name | `GET` | `/app-keys/{Provider}/{appName}` | | Create an app | `POST` | `/app-keys/{Provider}/{appName}` | | Update an app | `PUT` | `/app-keys/{Provider}/{appName}` | | Delete an app | `DELETE` | `/app-keys/{Provider}/{appName}` | For **CNHI**, **CNHI FieldOps**, and **John Deere**, the provider-specific endpoint pattern is: * `GET /app-keys/{Provider}` * `GET /app-keys/{Provider}/{appName}/{clientEnvironment}` * `POST /app-keys/{Provider}/{appName}/{clientEnvironment}` * `PUT /app-keys/{Provider}/{appName}/{clientEnvironment}` * `DELETE /app-keys/{Provider}/{appName}/{clientEnvironment}` The client environment is typically `STAGE` or `PRODUCTION`. ### Supported providers and request body fields | Provider | Path segment | Request body fields | | --------------------------- | ------------------ | --------------------------------------------- | | AgLeader | `AgLeader` | `privateKey`, `publicKey` | | Climate FieldView | `ClimateFieldView` | `apiKey`, `clientId`, `clientSecret` | | CNHI (AFS Connect - Legacy) | `CNHI` | `clientId`, `clientSecret`, `subscriptionKey` | | CNHI FieldOps | `CNHIFieldOps` | `clientId`, `clientSecret`, `subscriptionKey` | | John Deere | `JohnDeere` | `clientKey`, `clientSecret` | | Trimble | `Trimble` | `applicationName`, `clientId`, `clientSecret` | | Raven Slingshot | `RavenSlingshot` | `apiKey`, `sharedSecret` | | Stara | `Stara` | `user`, `pwd` | **CNHI**, **John Deere**, and **Trimble** require you to register `https://widget.withleaf.io` as a callback/redirect URL in your provider developer portal before Leaf Link can complete the OAuth flow. *** ### Example: John Deere The examples below show the full CRUD lifecycle for John Deere. All other providers follow the same pattern — only the path segment and request body fields differ. #### Create a John Deere app `POST /app-keys/JohnDeere/{appName}/{clientEnvironment}` ```bash cURL theme={null} curl -X POST \ "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "clientKey": "{clientKey}", "clientSecret": "{clientSecret}" }' ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } body = { "clientKey": client_key, "clientSecret": client_secret, } response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ clientKey, clientSecret, }), } ); const app = await response.json(); ``` #### Get all John Deere apps `GET /app-keys/JohnDeere` ```bash cURL theme={null} curl -X GET \ "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere" \ -H "Authorization: Bearer {token}" ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere" headers = {"Authorization": f"Bearer {token}"} response = requests.get(url, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere", { headers: { Authorization: `Bearer ${token}` } } ); const apps = await response.json(); ``` #### Get a John Deere app by name `GET /app-keys/JohnDeere/{appName}/{clientEnvironment}` ```bash cURL theme={null} curl -X GET \ "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" \ -H "Authorization: Bearer {token}" ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" headers = {"Authorization": f"Bearer {token}"} response = requests.get(url, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION", { headers: { Authorization: `Bearer ${token}` } } ); const app = await response.json(); ``` #### Update a John Deere app `PUT /app-keys/JohnDeere/{appName}/{clientEnvironment}` ```bash cURL theme={null} curl -X PUT \ "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "clientKey": "{newClientKey}", "clientSecret": "{newClientSecret}" }' ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } body = { "clientKey": new_client_key, "clientSecret": new_client_secret, } response = requests.put(url, headers=headers, json=body) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION", { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ clientKey: newClientKey, clientSecret: newClientSecret, }), } ); const updated = await response.json(); ``` #### Delete a John Deere app `DELETE /app-keys/JohnDeere/{appName}/{clientEnvironment}` ```bash cURL theme={null} curl -X DELETE \ "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" \ -H "Authorization: Bearer {token}" ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION" headers = {"Authorization": f"Bearer {token}"} response = requests.delete(url, headers=headers) ``` ```javascript JavaScript theme={null} await fetch( "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/my-jd-app/PRODUCTION", { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, } ); ``` Use the provider path matrix above when adapting these examples. Providers without `clientEnvironment` keep `appName` in the path, but omit the trailing environment segment. # Magic Link Source: https://docs.withleaf.io/api-reference/magic-link Create shareable URLs that let growers authenticate with John Deere, Climate FieldView, CNHi, CNHI FieldOps, and other providers or upload machine files. Use the Magic Link endpoints to create hosted authentication or file-upload flows that you can send directly to end users. This page covers the provider, single-provider authentication, and file-upload link variants, along with the settings object they share. For conceptual background, see [Magic Link](/components/magic-link). ## Overview Magic Links are shareable URLs that let your users connect provider accounts or upload files without embedding widgets in your application. There are three types: * **Provider** — multi-provider authentication (connect one or more providers). * **Authentication** — single-provider authentication (connect exactly one provider). * **File Upload** — manual file upload through a hosted interface. **Base URL:** `https://api.withleaf.io/services/widgets/api` All Magic Link types share the same `expiresIn` parameter: lifetime in seconds, minimum `900`, maximum approximately 1 year. The GET endpoints return usage-tracking fields such as `usageCount` and `maxUsage`. Magic Links are not inherently single-use. *** ## Settings object Every Magic Link type accepts an optional `settings` object to customize the hosted page: | Field | Type | Description | | ------------------- | ------- | ------------------------------------------- | | `backgroundColor` | string | Hex color for the page background. | | `headerImage` | string | URL of an image displayed in the header. | | `companyLogo` | string | URL of your company logo. | | `companyName` | string | Your company name displayed on the page. | | `showLeafUserName` | boolean | Whether to display the Leaf user's name. | | `disconnectEnabled` | boolean | Whether the user can disconnect a provider. | *** ## Provider Magic Link Lets the end user authenticate with multiple providers in a single session. ### Endpoints | Endpoint | Method | Path | | ---------------------------- | -------- | ----------------------------------------- | | Get all | `GET` | `/magic-link/provider` | | Get one | `GET` | `/magic-link/provider/{magicLinkId}` | | Create (with Leaf user) | `POST` | `/magic-link/users/{leafUserId}/provider` | | Create (with auto Leaf user) | `POST` | `/magic-link/provider` | | Delete | `DELETE` | `/magic-link/provider/{magicLinkId}` | *** ### Create a Provider Magic Link (with Leaf user) `POST /magic-link/users/{leafUserId}/provider` Creates a Magic Link for an existing Leaf user. #### Path parameters | Parameter | Type | Required | Description | | ------------ | ------ | -------- | -------------------------- | | `leafUserId` | string | Yes | The UUID of the Leaf user. | #### Request body | Field | Type | Required | Description | | ------------------ | --------- | -------- | -------------------------------------------------------------------- | | `expiresIn` | integer | No | Lifetime in seconds. Min `900`, max \~1 year. | | `allowedProviders` | string\[] | No | Provider keys to display (e.g. `"JohnDeere"`, `"ClimateFieldView"`). | | `settings` | object | No | Customization options. See [Settings object](#settings-object). | ```bash cURL theme={null} curl -X POST \ "https://api.withleaf.io/services/widgets/api/magic-link/users/{leafUserId}/provider" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "expiresIn": 604800, "allowedProviders": ["JohnDeere", "ClimateFieldView"], "settings": { "backgroundColor": "#ffffff", "companyName": "Acme Ag", "disconnectEnabled": true } }' ``` ```python Python theme={null} import requests url = f"https://api.withleaf.io/services/widgets/api/magic-link/users/{leaf_user_id}/provider" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } body = { "expiresIn": 604800, "allowedProviders": ["JohnDeere", "ClimateFieldView"], "settings": { "backgroundColor": "#ffffff", "companyName": "Acme Ag", "disconnectEnabled": True, }, } response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( `https://api.withleaf.io/services/widgets/api/magic-link/users/${leafUserId}/provider`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ expiresIn: 604800, allowedProviders: ["JohnDeere", "ClimateFieldView"], settings: { backgroundColor: "#ffffff", companyName: "Acme Ag", disconnectEnabled: true, }, }), } ); const magicLink = await response.json(); ``` #### Response ```json theme={null} { "id": "magicLinkId", "leafUserId": "UUID", "link": "https://magic-link.withleaf.io/{magicLinkId}", "expiresAt": "2024-10-26T14:23:56.584Z" } ``` *** ### Create a Provider Magic Link (with auto Leaf user) `POST /magic-link/provider` Creates a Magic Link for flows where Leaf creates a Leaf user based on the provided `externalId`. #### Request body | Field | Type | Required | Description | | ------------------ | --------- | -------- | ------------------------------------------------------------------------ | | `externalId` | string | Yes | Your identifier for the user. Required for automatic Leaf user creation. | | `name` | string | No | Display name for the auto-created Leaf user. | | `email` | string | No | Email for the auto-created Leaf user. | | `expiresIn` | integer | No | Lifetime in seconds. Min `900`, max \~1 year. | | `allowedProviders` | string\[] | No | Provider keys to display. | | `settings` | object | No | Customization options. See [Settings object](#settings-object). | ```bash cURL theme={null} curl -X POST \ "https://api.withleaf.io/services/widgets/api/magic-link/provider" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "externalId": "farmer-123", "name": "Jane Doe", "expiresIn": 604800, "allowedProviders": ["JohnDeere"] }' ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/widgets/api/magic-link/provider" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } body = { "externalId": "farmer-123", "name": "Jane Doe", "expiresIn": 604800, "allowedProviders": ["JohnDeere"], } response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.withleaf.io/services/widgets/api/magic-link/provider", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ externalId: "farmer-123", name: "Jane Doe", expiresIn: 604800, allowedProviders: ["JohnDeere"], }), } ); const magicLink = await response.json(); ``` *** ### Get all Provider Magic Links `GET /magic-link/provider` Returns all Provider Magic Links for your API owner. #### Query parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------------------------------------------------------------------- | | `nextPageToken` | string | No | Pagination token returned by a previous list response. Use `0` or omit it for the first page. | ```bash cURL theme={null} curl -X GET \ "https://api.withleaf.io/services/widgets/api/magic-link/provider" \ -H "Authorization: Bearer {token}" ``` ```python Python theme={null} import requests url = "https://api.withleaf.io/services/widgets/api/magic-link/provider" headers = {"Authorization": f"Bearer {token}"} response = requests.get(url, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.withleaf.io/services/widgets/api/magic-link/provider", { headers: { Authorization: `Bearer ${token}` } } ); const links = await response.json(); ``` #### Response ```json theme={null} { "items": [ { "id": "magicLinkId", "link": "https://magic-link.withleaf.io/{magicLinkId}", "createdAt": "2024-10-19T14:23:56.584Z", "expiresAt": "2024-10-26T14:23:56.584Z", "lastAccessedAt": "2024-10-19T14:23:56.584Z", "leafUserId": "UUID", "maxUsage": 3, "usageCount": 0, "widget": "PROVIDER", "allowedProviders": ["JohnDeere", "ClimateFieldView"], "settings": { "companyName": "Acme Ag", "companyLogo": "https://example.com/logo.svg", "disconnectEnabled": true } } ], "nextPageToken": "opaque-pagination-token" } ``` *** ### Get a Provider Magic Link `GET /magic-link/provider/{magicLinkId}` Returns a single Provider Magic Link by ID. #### Path parameters | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ------------------------- | | `magicLinkId` | string | Yes | The ID of the Magic Link. | #### Response ```json theme={null} { "id": "magicLinkId", "link": "https://magic-link.withleaf.io/{magicLinkId}", "createdAt": "2024-10-19T14:23:56.584Z", "expiresAt": "2024-10-26T14:23:56.584Z", "lastAccessedAt": "2024-10-19T14:23:56.584Z", "leafUserId": "UUID", "maxUsage": 3, "usageCount": 0, "widget": "PROVIDER", "allowedProviders": ["JohnDeere", "ClimateFieldView"], "settings": { "companyName": "Acme Ag", "companyLogo": "https://example.com/logo.svg", "disconnectEnabled": true } } ``` *** ### Delete a Provider Magic Link `DELETE /magic-link/provider/{magicLinkId}` Permanently deletes a Provider Magic Link. The URL immediately stops working. #### Path parameters | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ------------------------- | | `magicLinkId` | string | Yes | The ID of the Magic Link. | *** ## Authentication Magic Link Lets the end user authenticate with a single, specified provider. ### Endpoints | Endpoint | Method | Path | | ---------------------------- | -------- | ----------------------------------------------- | | Get all | `GET` | `/magic-link/authentication` | | Get one | `GET` | `/magic-link/authentication/{magicLinkId}` | | Create (with Leaf user) | `POST` | `/magic-link/users/{leafUserId}/authentication` | | Create (with auto Leaf user) | `POST` | `/magic-link/authentication` | | Delete | `DELETE` | `/magic-link/authentication/{magicLinkId}` | *** ### Create an Authentication Magic Link (with Leaf user) `POST /magic-link/users/{leafUserId}/authentication` Creates a Magic Link scoped to a single provider for an existing Leaf user. #### Path parameters | Parameter | Type | Required | Description | | ------------ | ------ | -------- | -------------------------- | | `leafUserId` | string | Yes | The UUID of the Leaf user. | #### Request body | Field | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------------------------------- | | `expiresIn` | integer | No | Lifetime in seconds. Min `900`, max \~1 year. | | `provider` | string | Yes | The provider key (e.g. `"JohnDeere"`, `"ClimateFieldView"`). | | `settings` | object | No | Customization options. See [Settings object](#settings-object). | ```bash cURL theme={null} curl -X POST \ "https://api.withleaf.io/services/widgets/api/magic-link/users/{leafUserId}/authentication" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "expiresIn": 604800, "provider": "JohnDeere", "settings": { "companyName": "Acme Ag" } }' ``` ```python Python theme={null} import requests url = f"https://api.withleaf.io/services/widgets/api/magic-link/users/{leaf_user_id}/authentication" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } body = { "expiresIn": 604800, "provider": "JohnDeere", "settings": {"companyName": "Acme Ag"}, } response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( `https://api.withleaf.io/services/widgets/api/magic-link/users/${leafUserId}/authentication`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ expiresIn: 604800, provider: "JohnDeere", settings: { companyName: "Acme Ag" }, }), } ); const magicLink = await response.json(); ``` #### Response ```json theme={null} { "id": "magicLinkId", "leafUserId": "UUID", "link": "https://magic-link.withleaf.io/{magicLinkId}", "expiresAt": "2024-10-26T14:23:56.584Z" } ``` *** ### Create an Authentication Magic Link (with auto Leaf user) `POST /magic-link/authentication` Creates a Magic Link for flows where Leaf creates a Leaf user based on the provided `externalId`. #### Request body | Field | Type | Required | Description | | ------------ | ------- | -------- | --------------------------------------------------------------- | | `externalId` | string | Yes | Your identifier for the user. | | `name` | string | No | Display name for the auto-created Leaf user. | | `email` | string | No | Email for the auto-created Leaf user. | | `expiresIn` | integer | No | Lifetime in seconds. Min `900`, max \~1 year. | | `provider` | string | Yes | The provider key. | | `settings` | object | No | Customization options. See [Settings object](#settings-object). | Use the auto-create variant when you do not want to create the Leaf user separately before sending the link. *** ### Get, Delete The Get all, Get one, and Delete endpoints for Authentication Magic Links follow the same pattern as the [Provider Magic Link](#provider-magic-link) endpoints, but the resource includes a single `provider` field instead of `allowedProviders`. Authentication list endpoints also use the same paginated envelope with `items` and `nextPageToken`. #### Authentication list/get response shape ```json theme={null} { "id": "magicLinkId", "link": "https://magic-link.withleaf.io/{magicLinkId}", "createdAt": "2024-10-19T14:23:56.584Z", "expiresAt": "2024-10-26T14:23:56.584Z", "lastAccessedAt": "2024-10-19T14:23:56.584Z", "leafUserId": "UUID", "maxUsage": 3, "usageCount": 0, "widget": "AUTHENTICATION", "provider": "JohnDeere", "settings": { "companyName": "Acme Ag", "companyLogo": "https://example.com/logo.svg" } } ``` *** ## File Upload Magic Link Lets the end user upload machine files through a hosted interface. ### Endpoints | Endpoint | Method | Path | | ---------------------------- | -------- | -------------------------------------------- | | Get all | `GET` | `/magic-link/file-upload` | | Get one | `GET` | `/magic-link/file-upload/{magicLinkId}` | | Create (with Leaf user) | `POST` | `/magic-link/users/{leafUserId}/file-upload` | | Create (with auto Leaf user) | `POST` | `/magic-link/file-upload` | | Delete | `DELETE` | `/magic-link/file-upload/{magicLinkId}` | *** ### Create a File Upload Magic Link (with Leaf user) `POST /magic-link/users/{leafUserId}/file-upload` Creates a Magic Link for uploading machine files, tied to an existing Leaf user. #### Path parameters | Parameter | Type | Required | Description | | ------------ | ------ | -------- | -------------------------- | | `leafUserId` | string | Yes | The UUID of the Leaf user. | #### Request body | Field | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------------------------------- | | `expiresIn` | integer | No | Lifetime in seconds. Min `900`, max \~1 year. | | `settings` | object | No | Customization options. See [Settings object](#settings-object). | ```bash cURL theme={null} curl -X POST \ "https://api.withleaf.io/services/widgets/api/magic-link/users/{leafUserId}/file-upload" \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "expiresIn": 604800, "settings": { "companyName": "Acme Ag", "companyLogo": "https://example.com/logo.png" } }' ``` ```python Python theme={null} import requests url = f"https://api.withleaf.io/services/widgets/api/magic-link/users/{leaf_user_id}/file-upload" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } body = { "expiresIn": 604800, "settings": { "companyName": "Acme Ag", "companyLogo": "https://example.com/logo.png", }, } response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( `https://api.withleaf.io/services/widgets/api/magic-link/users/${leafUserId}/file-upload`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ expiresIn: 604800, settings: { companyName: "Acme Ag", companyLogo: "https://example.com/logo.png", }, }), } ); const magicLink = await response.json(); ``` #### Response ```json theme={null} { "id": "magicLinkId", "leafUserId": "UUID", "link": "https://magic-link.withleaf.io/{magicLinkId}", "expiresAt": "2024-10-26T14:23:56.584Z" } ``` *** ### Create a File Upload Magic Link (with auto Leaf user) `POST /magic-link/file-upload` Creates a Magic Link for flows where Leaf creates a Leaf user based on the provided `externalId`. #### Request body | Field | Type | Required | Description | | ------------ | ------- | -------- | --------------------------------------------------------------- | | `externalId` | string | Yes | Your identifier for the user. | | `name` | string | No | Display name for the auto-created Leaf user. | | `email` | string | No | Email for the auto-created Leaf user. | | `expiresIn` | integer | No | Lifetime in seconds. Min `900`, max \~1 year. | | `settings` | object | No | Customization options. See [Settings object](#settings-object). | *** ### Get, Delete The Get all, Get one, and Delete endpoints for File Upload Magic Links follow the same paginated pattern as the [Provider Magic Link](#provider-magic-link) endpoints. File Upload list endpoints also use the same paginated envelope with `items` and `nextPageToken`. #### File Upload list/get response shape ```json theme={null} { "id": "magicLinkId", "link": "https://magic-link.withleaf.io/{magicLinkId}", "createdAt": "2024-10-19T14:23:56.584Z", "expiresAt": "2024-10-26T14:23:56.584Z", "lastAccessedAt": "2024-10-19T14:23:56.584Z", "leafUserId": "UUID", "maxUsage": 3, "usageCount": 0, "widget": "FILE_UPLOAD", "settings": { "companyName": "Acme Ag", "companyLogo": "https://example.com/logo.svg" } } ``` # MCP Tools Source: https://docs.withleaf.io/api-reference/mcp-tools Give AI coding assistants access to Leaf users, fields, operations, machine files, weather, and billing data through the Leaf MCP server. Use this page to understand what the remote Leaf MCP server exposes after you connect a client to `https://mcp.withleaf.io/mcp`. It is a tool reference, not a REST API reference, and it groups the available capabilities by product area. ## Overview The Leaf MCP (Model Context Protocol) server exposes tools that AI assistants can call to interact with the Leaf API programmatically. Instead of making REST calls directly, your MCP-compatible client (such as Cursor, Claude Desktop, or a custom agent) invokes typed tools and receives structured responses. The MCP server is a tool server, not a REST API. There is no base URL — you configure the server in your MCP client, and it handles communication with the Leaf API on your behalf. *** ## Configuration To use the Leaf MCP server, point your MCP client at the remote Leaf server and pass your Leaf API token as a `LEAF_TOKEN` header. The server authenticates every tool call against the Leaf API using this token. ```json theme={null} { "mcpServers": { "leaf": { "url": "https://mcp.withleaf.io/mcp", "headers": { "LEAF_TOKEN": "YOUR_TOKEN" } } } } ``` Use the remote HTTP server as the canonical setup. Older local `npx`-based examples are outdated. *** ## Available tools The server groups its tools into the categories listed below. Each tool accepts typed parameters and returns JSON matching the corresponding Leaf API response. ### User management | Tool | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | List users | Returns a paginated list of Leaf users for your account. Filterable by email, name, or external ID. | | Get Leaf user configuration | Returns configuration settings for a Leaf user. If the user has no custom configuration, settings are inherited from the API owner. | | Get API owner configuration | Returns the default configuration for the API owner, including operation image creation, auto-sync, and merge settings. | ### Fields | Tool | Description | | ------------------ | --------------------------------------------------------------------------------------------- | | List fields | Returns a paginated list of fields for a Leaf user. Filterable by type, farm ID, or provider. | | Get field | Returns a single field by its UUID. | | Get field boundary | Returns the active boundary geometry for a field. | ### Field operations | Tool | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------ | | List operations | Returns a paginated list of field operations. Filterable by provider, time range, operation type, and field. | | Get operation | Returns a single operation by its UUID. | | Get operation summary | Returns aggregated statistics (area, elevation, speed, and operation-specific properties) for an operation. | | Get operation units | Returns the property-to-unit mapping for an operation. | ### Machine files | Tool | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | | List files | Returns a paginated list of machine files. Filterable by provider, status, origin, operation type, time range, and more. | | Get file | Returns a single machine file by its UUID. | | Get file summary | Returns the summary for a machine file. | | Get file units | Returns the property-to-unit mapping for a machine file. | | Get file status | Returns the processing status for every step of the Leaf pipeline for a file. | ### Batch uploads | Tool | Description | | ---------------- | ------------------------------------------------------------------------------------- | | List batches | Returns a paginated list of manual-upload batches. Filterable by provider and status. | | Get batch | Returns a single batch by its UUID. | | Get batch status | Returns the processing status of all files inside a batch. | ### Weather | Tool | Description | | ---------------------------- | ---------------------------------------------------------- | | Forecast (field, daily) | Returns daily forecasted weather for a Leaf user's field. | | Forecast (field, hourly) | Returns hourly forecasted weather for a Leaf user's field. | | Forecast (lat/lon, daily) | Returns daily forecasted weather for a coordinate pair. | | Forecast (lat/lon, hourly) | Returns hourly forecasted weather for a coordinate pair. | | Historical (field, daily) | Returns daily historical weather for a Leaf user's field. | | Historical (field, hourly) | Returns hourly historical weather for a Leaf user's field. | | Historical (lat/lon, daily) | Returns daily historical weather for a coordinate pair. | | Historical (lat/lon, hourly) | Returns hourly historical weather for a coordinate pair. | Weather tools accept optional `model` and `units` parameters. Time parameters use ISO 8601 format. ### Billing | Tool | Description | | --------------- | --------------------------------------------------------------------------------------------- | | List contracts | Returns all billing contracts for your account. | | Get contract | Returns a single contract by ID, including product type, date range, region, and quota. | | Get consumption | Returns consumption data for a contract. Optionally pass a timestamp to query a specific day. | ### Provider credentials | Tool | Description | | --------------------------------------- | ------------------------------------------------------------------------------------- | | Get John Deere credential events | Returns connection events and status for a Leaf user's John Deere credentials. | | Get Climate FieldView credential events | Returns connection events and status for a Leaf user's Climate FieldView credentials. | | Get CNHI credential events | Returns connection events and status for a Leaf user's CNHI credentials. | Credential event tools are intended for troubleshooting. They expose authentication status and error details for provider connections. # Field Operations Source: https://docs.withleaf.io/api-reference/operations List and retrieve standardized field operations (planting, harvest, application, tillage) and their GeoJSON, summary, and image outputs through the Leaf API. Use the operations endpoints to list and inspect Leaf's merged field operations after machine files have been converted and allocated to field boundaries. This page covers operation retrieval, summaries, GeoJSON and GeoParquet outputs, images, units, and reprocessing. For conceptual background, see [Field Operations](/machine-data/field-operations). ## Base URL ``` https://api.withleaf.io/services/operations/api ``` ## Endpoints | Method | Path | Description | | ----------------- | ------------------------------------- | --------------------------------------------------------------------------- | | GET | `/operations` | [Get all field operations](#get-all-field-operations) | | GET | `/operations/{id}` | [Get a field operation](#get-a-field-operation) | | GET | `/operations/{id}/summary` | [Get operation summary](#get-operation-summary) | | GET | `/operations/{id}/standardGeojson` | [Get operation standardGeojson](#get-operation-standardgeojson) | | GET | `/operations/{id}/standardGeoparquet` | [Get operation standardGeoParquet](#get-operation-standardgeoparquet) | | GET | `/operations/{id}/filteredGeojson` | [Get operation filteredGeojson](#get-operation-filteredgeojson) | | GET | `/operations/{id}/filteredGeoparquet` | [Get operation filteredGeoParquet](#get-operation-filteredgeoparquet) | | GET | `/operations/{id}/imagesV2` | [Get operation images](#get-operation-images) | | GET | `/operations/{id}/geotiffImages` | [Get operation geotiff images](#get-operation-geotiff-images) | | GET | `/operations/{id}/units` | [Get operation units](#get-operation-units) | | GET | `/operations/{id}/machines` | [Get operation machines](#get-operation-machines) | | GET | `/operations/{id}/implements` | [Get operation implements](#get-operation-implements) | | GET | `/operations/{id}/operators` | [Get operation operators](#get-operation-operators) | | GET | `/operations/{id}/sessions` | [Get operation sessions](#get-operation-sessions) | | POST | `/operations/cropOperationByField` | [Crop operation by field](#crop-operation-by-field) | | POST | `/operations/reprocess` | [Reprocess field operations by field](#reprocess-field-operations-by-field) | | POST | `/operations/{id}/reprocess` | [Reprocess an operation](#reprocess-an-operation) | | GET | `/operations/{id}/files` | [Get files from an operation](#get-files-from-an-operation) | *** ## Get all field operations GET `/operations` Returns a paginated list of field operations for the authenticated API owner. ### Parameters | Parameter | Type | Description | | --------------- | ------- | --------------------------------------------------------------------------------------------------------------------- | | `leafUserId` | string | UUID of a Leaf user | | `provider` | string | `CNHI`, `JohnDeere`, `Trimble`, `ClimateFieldView`, `AgLeader`, `Stara`, `Panorama`, or `Leaf` | | `startTime` | string | ISO 8601 timestamp. Returns operations starting on or after this time | | `updatedTime` | string | ISO 8601 timestamp. Returns operations updated on or after this time | | `endTime` | string | ISO 8601 timestamp. Returns operations ending on or before this time | | `operationType` | string | `applied`, `planted`, `harvested`, or `tillage` | | `fieldId` | string | UUID of the field where the operation occurred | | `fileId` | string | UUID of a machine file associated with the operation | | `zoneId` | string | UUID of a zone associated with the operation | | `standard` | boolean | Filter by whether the operation has a standardGeojson | | `page` | integer | Page number (default `0`) | | `size` | integer | Page size (max `100`) | | `sort` | string | Sort order. Valid fields: `id`, `leafUserId`, `startTime`, `endTime`, `type`, `updatedTime`. Append `,asc` or `,desc` | The default page size is 20 when `page` and `size` are not set. ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations?leafUserId=UUID' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers, params={'leafUserId': 'UUID'}) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { leafUserId: 'UUID' } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "id": "5c8fdb34-4dc4-4b96-bfd5-53e6206ce971", "apiOwnerUsername": "test", "leafUserId": "7494c90e-28b8-4bb2-9ede-95c1cc894349", "startTime": "2015-04-18T19:31:27Z", "endTime": "2015-04-18T19:58:50Z", "updatedTime": "2021-08-24T16:00:15.062Z", "type": "planted", "files": ["a10b85c2-ac2e-4b0f-8e65-74edbd2ca53e"], "fields": [{ "id": "0071484f-4a75-4190-9fd0-f5995d241c2c" }], "providers": ["providerName"] } ] ``` *** ## Get a field operation GET `/operations/{id}` Returns a single field operation by its UUID. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "5c8fdb34-4dc4-4b96-bfd5-53e6206ce971", "apiOwnerUsername": "test", "leafUserId": "7494c90e-28b8-4bb2-9ede-95c1cc894349", "startTime": "2015-04-18T19:31:27Z", "endTime": "2015-04-18T19:58:50Z", "updatedTime": "2021-08-24T16:00:15.062Z", "type": "planted", "files": ["a10b85c2-ac2e-4b0f-8e65-74edbd2ca53e"], "fields": [{ "id": "0071484f-4a75-4190-9fd0-f5995d241c2c" }], "providers": ["providerName"] } ``` *** ## Get operation summary GET `/operations/{id}/summary` Returns the GeoJSON summary for a field operation. The summary contains aggregated statistics such as area, elevation, speed, and operation-specific properties (e.g., seed rate, applied rate, yield). ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/summary' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/summary' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/summary' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get operation standardGeojson GET `/operations/{id}/standardGeojson` Returns a URL to the standardGeojson file for the operation. This file contains all data points in Leaf's standardized schema. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/standardGeojson' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/standardGeojson' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/standardGeojson' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "downloadStandardGeojson": "URL" } ``` *** ## Get operation standardGeoParquet GET `/operations/{id}/standardGeoparquet` Returns a URL to the standard GeoParquet file for the operation. You must enable the `enableGeoparquetOutput` configuration to use this endpoint. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/standardGeoparquet' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/standardGeoparquet' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/standardGeoparquet' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "downloadStandardGeoparquet": "URL" } ``` *** ## Get operation filteredGeojson GET `/operations/{id}/filteredGeojson` Returns a URL to the filteredGeojson file for the operation. This file contains data points after Leaf applies statistical outlier removal. You must enable the `operationsFilteredGeojson` configuration to use this endpoint. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/filteredGeojson' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/filteredGeojson' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/filteredGeojson' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "filteredGeojson": "URL", "downloadFilteredGeoJson": "URL" } ``` *** ## Get operation filteredGeoParquet GET `/operations/{id}/filteredGeoparquet` Returns a URL to the filtered GeoParquet file for the operation. You must enable both `operationsFilteredGeojson` and `enableGeoparquetOutput` configurations to use this endpoint. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/filteredGeoparquet' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/filteredGeoparquet' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/filteredGeoparquet' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "downloadFilteredGeoparquet": "URL" } ``` *** ## Get operation images GET `/operations/{id}/imagesV2` Returns improved PNG images based on the filteredGeojson. Each image includes a quantile-classified legend with 7 color-coded ranges and an extent for map plotting. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/imagesV2' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/imagesV2' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/imagesV2' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "property": "elevation", "legend": { "ranges": [ { "colorCode": "#C80000", "min": 0, "max": 20 }, { "colorCode": "#FF2800", "min": 20, "max": 50 }, { "colorCode": "#FF9600", "min": 50, "max": 100 }, { "colorCode": "#FFF000", "min": 100, "max": 250 }, { "colorCode": "#00E600", "min": 250, "max": 340 }, { "colorCode": "#00BE00", "min": 340, "max": 480 }, { "colorCode": "#008200", "min": 480, "max": 570 } ] }, "extent": { "xmin": 0, "xmax": 0, "ymin": 0, "ymax": 0 }, "url": "URL", "downloadUrl": "URL" } ] ``` *** ## Get operation geotiff images GET `/operations/{id}/geotiffImages` Returns a list of GeoTIFF images generated from the operation's properties based on the filteredGeojson. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/geotiffImages' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/geotiffImages' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/geotiffImages' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "property": "dryMassPerArea", "url": "URL", "downloadUrl": "URL" } ] ``` *** ## Get operation units GET `/operations/{id}/units` Returns the property-to-unit mapping for the operation. Properties vary by operation type but use standardized keys across providers. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/units' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/units' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/units' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` *** ## Get operation machines GET `/operations/{id}/machines` Returns the UUIDs of machines used in the operation. Use the Assets endpoints to fetch full machine details. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/machines' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/machines' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/machines' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "machines": [ "77385069-7666-4867-8d72-72c2584e2b4e", "baad537c-69e3-4d86-a99b-92d5b716b574" ] } ``` *** ## Get operation implements GET `/operations/{id}/implements` Returns the UUIDs of implements used in the operation. Use the Assets endpoints to fetch full implement details. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/implements' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/implements' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/implements' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "implements": [ "1190bc0d-e94c-407a-8aba-ac4c6a1cd29b" ] } ``` *** ## Get operation operators GET `/operations/{id}/operators` Returns the UUIDs of operators who performed the operation. Use the Assets endpoints to fetch full operator details. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/operators' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/operators' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/operators' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "operators": [ "f2f4723a-2bfe-472b-b6f7-7874c8500208" ] } ``` *** ## Get operation sessions GET `/operations/{id}/sessions` Returns compiled session data grouped by machine, with session time ranges, operator data, and covered area for each session in the operation. Requires the `enableOperationsSession` configuration. Currently available for John Deere operations only. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/sessions' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/sessions' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/sessions' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "machineId": "uuid", "serialNumber": "SERIALNUMBER001", "sessions": [ { "id": "sessionId", "startTime": "2023-11-29T11:03:42", "endTime": "2023-11-29T20:58:36", "operator": { "id": "operatorId", "name": "Operator A" }, "area": { "value": 18.215, "unit": "ha" } } ] } ] ``` *** ## Crop operation by field POST `/operations/cropOperationByField` Removes data points from the operation standardGeojson that fall outside the associated field boundary. Processing is asynchronous. ### Request body ```json theme={null} { "id": "operationId" } ``` ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"id": "operationId"}' \ 'https://api.withleaf.io/services/operations/api/operations/cropOperationByField' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/cropOperationByField' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.post(endpoint, headers=headers, json={'id': 'operationId'}) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/cropOperationByField' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.post(endpoint, { id: 'operationId' }, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "1162a1c6-9872-4d7f-9833-5d48add8eed4", "message": "Sent operation to be processed.", "leafFileId": "33020f03-5889-4c0f-b465-7a7e2c03a91d" } ``` Use the `leafFileId` with the Alerts service to monitor processing status. *** ## Reprocess field operations by field POST `/operations/reprocess` Reprocesses field operations for one field and Leaf user. When `overwrite` is `true`, Leaf removes the existing operations for that field before regenerating them. ### Request body ```json theme={null} { "leafUserId": "uuid", "fieldId": "uuid", "overwrite": true } ``` ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"leafUserId":"uuid","fieldId":"uuid","overwrite":true}' \ 'https://api.withleaf.io/services/operations/api/operations/reprocess' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/reprocess' headers = {'Authorization': f'Bearer {TOKEN}'} data = {'leafUserId': 'uuid', 'fieldId': 'uuid', 'overwrite': True} response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/reprocess' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { leafUserId: 'uuid', fieldId: 'uuid', overwrite: true } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "totalFiles": 42, "message": "Sending files to create automerge and operations based on field" } ``` *** ## Reprocess an operation POST `/operations/{id}/reprocess` Reprocesses an existing field operation starting from the merge step. The standardGeoJSON, filteredGeoJSON, summary, and images are regenerated. ### Parameters | Parameter | Type | Description | | --------- | ---- | --------------------- | | `id` | path | UUID of the operation | ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/reprocess' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/reprocess' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.post(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/reprocess' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.post(endpoint, {}, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Use the Alerts service to monitor the reprocessing status. *** ## Get files from an operation GET `/operations/{id}/files` Returns the machine files that were aggregated to produce the field operation. ### Parameters | Parameter | Type | Description | | --------- | ------- | ---------------------------- | | `id` | path | UUID of the operation | | `page` | integer | Page number (default `0`) | | `size` | integer | Page size (max `100`) | | `sort` | string | Sort order (e.g., `id,desc`) | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations/{id}/files' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/files' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/operations/api/operations/{id}/files' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` # Provider Organizations Source: https://docs.withleaf.io/api-reference/organizations Reference for provider organization list and sync-management endpoints on the Leaf user management API (John Deere and Trimble). Use these endpoints to list provider organizations for a Leaf user and, for John Deere, to control which organizations Leaf processes. For behavior, status meanings, and `organizationDataSync`, see [Provider Organizations](/providers/organizations). ## Base URL ``` https://api.withleaf.io/services/usermanagement/api ``` ## Endpoints ### Provider organization list Returns connected and not-connected organizations for a provider account when the provider supports that distinction. | Action | Method | Path | | --------------------------------- | ---------------- | ---------------------------------------------- | | List organizations for a provider | GET | `/users/{leafUserId}/organizations/{provider}` | * Supported `{provider}` values include `JohnDeere` and `Trimble`. * John Deere returns both `connectedOrganizations` and `notConnectedOrganizations`. * Trimble does not distinguish connected vs not connected in the same way. ### Provider organization sync management These paths control which organizations Leaf is allowed to process for a connected account. Sync-management operations in this group are **John Deere only**. Use `{provider}` = `JohnDeere`. | Action | Method | Path | | ----------------------------- | ------------------ | ----------------------------------------------------------------------- | | List provider organizations | GET | `/users/{leafUserId}/{provider}/organizations` | | Get one provider organization | GET | `/users/{leafUserId}/{provider}/organizations/{providerOrgId}` | | Update organization status | PATCH | `/users/{leafUserId}/{provider}/organizations/{providerOrgId}/{status}` | | Sync provider organizations | POST | `/users/{leafUserId}/{provider}/organizations/sync` | ## Example: organization list response ```json theme={null} { "connectedOrganizations": [ { "id": "organization_id_1", "name": "Organization Name 1", "managementUri": "https://connections.deere.com/connections/clientKey/connections-dialog?orgId=organization_id_1" } ], "notConnectedOrganizations": [ { "id": "organization_id_2", "name": "Organization Name 2", "managementUri": "https://connections.deere.com/connections/clientKey/connections-dialog?orgId=organization_id_2" } ] } ``` ## Example: GET organization list ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/organizations/JohnDeere' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" leaf_user_id = "{leafUserId}" endpoint = ( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/organizations/JohnDeere" ) headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const leafUserId = "{leafUserId}"; const endpoint = `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/organizations/JohnDeere`; const headers = { Authorization: `Bearer ${TOKEN}` }; axios.get(endpoint, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ## Provider organization resource ```json theme={null} { "providerOrgId": "520674381", "providerOrgName": "Leaf Farms", "status": "SELECTED", "managementUri": "https://connections.deere.com/connections/clientKey/connections-dialog?orgId=Leaf Farms" } ``` | Field | Description | | ----------------- | ------------------------------------------------------------------------------------- | | `providerOrgId` | Provider organization ID | | `providerOrgName` | Provider organization name | | `managementUri` | Provider URL to review or fix the app-to-organization connection | | `status` | Whether Leaf processes data from this organization (`SELECTED`, `PREVIEW`, `BLOCKED`) | ## Organization statuses | Status | Meaning | | ---------- | ------------------------------------------------------------------ | | `SELECTED` | Leaf processes resources from this organization. | | `PREVIEW` | Visible to Leaf but downstream resources are not processed. | | `BLOCKED` | The app lacks required provider-side access for this organization. | If a John Deere `managementUri` contains `connections-dialog`, the connection is established. If it contains `select-organizations`, setup is incomplete until you fix the provider-side connection and run a sync again. ## Sync scope configuration `organizationDataSync` on the Leaf user configuration controls whether Leaf syncs every available organization or only those you mark `SELECTED`: * `ALL` — sync every organization the account can access. * `SELECTED_ONLY` — sync only organizations you set to `SELECTED` via these endpoints. See [Configuration](/configuration/overview) and [Provider Organizations](/providers/organizations) for details and billing implications. ## Verify synced resources Confirm grower, farm, and field counts after changing organization scope: `GET https://api.withleaf.io/services/integrations/api/resources` See [Integrations](/api-reference/integrations). ## What to do next * Read [Provider Organizations](/providers/organizations) for workflows and warnings. * Use [Provider credentials](/api-reference/providers) to connect the provider account first. * Use [Configurations](/api-reference/configurations) for `organizationDataSync` and `customDataSync`. # Provider Credentials Source: https://docs.withleaf.io/api-reference/providers Provider credential endpoints to connect Leaf users to John Deere, Climate FieldView, CLAAS, CNHi, Trimble, AgLeader, Stara, Raven, and other providers. Use these endpoints to attach provider credentials to a Leaf user. The base path is consistent, but the request body and supported helper endpoints vary by provider, so this page is best used as a path matrix rather than a single normalized contract. For setup requirements and request-body examples, see the provider-specific guides under [Connecting Providers](/providers/overview). ## Base URL ``` https://api.withleaf.io/services/usermanagement/api ``` ## Credential path matrix | Provider | Credential path | Notes | Documentation | | --------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | | John Deere | `john-deere-credentials` | OAuth credentials | [John Deere](/providers/john-deere) | | CLAAS | `claas-credentials` | OAuth with `clientKey` / `clientSecret` and grower `refreshToken`; `clientEnvironment` supports `STAGE` and `PRODUCTION` | [CLAAS](/providers/claas) | | Climate FieldView | `climate-field-view-credentials` | OAuth credentials | [Climate FieldView](/providers/climate-fieldview) | | CNHI (AFS Connect - Legacy) | `cnhi-credentials` | Requires `clientEnvironment`; legacy AFS Connect API | [CNHI (AFS Connect)](/providers/cnhi) | | CNHI FieldOps | `cnhi-field-ops-credentials` | Requires `clientEnvironment`; existing CNHI keys do not work | [CNHI FieldOps](/providers/cnhi-fieldops) | | Trimble | `trimble-credentials` | OAuth credentials | [Trimble](/providers/trimble) | | AgLeader | `ag-leader-credentials` | Uses app keys plus grower refresh token | [AgLeader](/providers/agleader) | | Stara | `stara-credentials` | Uses `apiKey`, `accessToken`, `accessTokenClient`, and `refreshToken` | [Stara](/providers/stara) | | Raven | `raven-credentials` | OAuth credentials for grower, farm, and field data | [Raven](/providers/raven) | | Raven Slingshot | `raven-slingshot-credentials` | API key for machine file ingestion | [Raven Slingshot](/providers/raven-slingshot) | | Sentera | `sentera-credentials` | Username/password credentials | [Sentera](/providers/sentera) | | AgVance | `agvance-credentials` | API key plus account credentials | [AgVance](/providers/agvance) | | Panorama | `panorama-credentials` | Precision Planting Panorama | [Panorama](/providers/panorama) | | Lindsay | `lindsay-credentials` | OAuth credentials with `clientEnvironment` | [Lindsay](/providers/lindsay) | | Valley | `valley-credentials` | API key plus account credentials | [Valley](/providers/valley) | ## Common path shape The credential endpoints always start with the Leaf user: ```text theme={null} /users/{leafUserId}/{credential-path} ``` Examples: * `GET /users/{leafUserId}/john-deere-credentials` * `POST /users/{leafUserId}/ag-leader-credentials` * `DELETE /users/{leafUserId}/panorama-credentials` Most providers also expose a credential events endpoint: ```text theme={null} /users/{leafUserId}/{credential-path}/events ``` Use the provider-specific documentation to confirm the exact request body and any provider-specific fields. ## Example: get stored credentials ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials"; const headers = { Authorization: `Bearer ${TOKEN}` }; axios.get(endpoint, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ## Events endpoint Many provider credential endpoints expose `/events` for troubleshooting authentication failures, token refreshes, and sync-related issues. When available, these endpoints support: | Parameter | Type | Description | | --------- | ------- | ------------------------------------- | | `page` | integer | Page number (default `0`) | | `size` | integer | Page size (default `20`) | | `sort` | string | Sort order such as `createdDate,desc` | ## What to do next * Use [Connecting Providers](/providers/overview) to choose the right provider flow. * Use the provider-specific guides for request-body schemas and setup details. * Use [Leaf users](/api-reference/users) to create the Leaf user before attaching credentials. # Satellite & Crop Monitoring Source: https://docs.withleaf.io/api-reference/satellite Register fields for satellite monitoring, retrieve NDVI, NDRE, and RGB imagery from Sentinel-2 and PlanetScope, and manage monitoring subscriptions. Use the satellite service to register geometries for monitoring, retrieve processed image captures, and manage Planet subscriptions or reprocessing. This page is the endpoint reference for Sentinel and Planet imagery once you already understand the product-level behavior. For conceptual background, see [Satellite Imagery Overview](/satellite/overview). ## Base URL ``` https://api.withleaf.io/services/satellite/api ``` ## Endpoints | Endpoint | Method | Path | | ------------------------------------------------------------------- | ------------------- | -------------------------------------------- | | [Get all satellite fields](#get-all-satellite-fields) | GET | `/fields` | | [Get a satellite field](#get-a-satellite-field) | GET | `/fields/{id}` | | [Get images of satellite field](#get-images-of-satellite-field) | GET | `/fields/{id}/processes` | | [Get an image of satellite field](#get-an-image-of-satellite-field) | GET | `/fields/{id}/processes/{processId}` | | [Create a satellite field](#create-a-satellite-field) | POST | `/fields` | | [Delete a satellite field](#delete-a-satellite-field) | DELETE | `/fields/{id}` | | [Get subscription for Planet](#get-subscription-for-planet) | GET | `/fields/{id}/subscription` | | [Reprocess satellite images](#reprocess-satellite-images) | POST | `/fields/{id}/process/{processId}/reprocess` | Planet imagery is billed per area processed. Every satellite field with `Planet` in its `providers` array consumes quota when new imagery is processed. Use a small test geometry when validating your integration. *** ### Get all satellite fields GET `/fields` Returns a paginated list of satellite fields for the authenticated API owner. #### Parameters | Parameter | Type | Description | | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Sorting order. Valid fields: `createdAt`, `providers`, `externalId`. Append `,asc` or `,desc` (e.g. `createdAt,desc`). | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/satellite/api/fields?page=0&size=10&sort=createdAt,desc' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"page": 0, "size": 10, "sort": "createdAt,desc"} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { page: 0, size: 10, sort: 'createdAt,desc' } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "b2a3c4d5-e6f7-4890-abcd-ef1234567890", "externalId": "north-quarter", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137] ]]] }, "providers": ["Sentinel"], "createdAt": "2023-07-21T13:01:11Z" } ] ``` *** ### Get a satellite field GET `/fields/{id}` Returns a single satellite field by ID. #### Parameters | Parameter | Type | Description | | --------- | ----------- | ----------------------- | | `id` | path (UUID) | The satellite field ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/satellite/api/fields/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single satellite field object (same shape as the objects in the [Get all satellite fields](#get-all-satellite-fields) response). *** ### Get images of satellite field GET `/fields/{id}/processes` Returns a paginated list of processed satellite images for the specified field. Each process represents a single capture date and contains one or more image types (NDVI, RGB, etc.). #### Parameters | Parameter | Type | Description | | ------------------------- | ----------- | ------------------------------------------------------------------------ | | `id` | path (UUID) | The satellite field ID. | | `startDate` | string | ISO 8601 date. Returns images captured on or after this date. | | `endDate` | string | ISO 8601 date. Returns images captured on or before this date. | | `startProcessedTimestamp` | string | ISO 8601 timestamp. Returns images processed on or after this time. | | `endProcessedTimestamp` | string | ISO 8601 timestamp. Returns images processed on or before this time. | | `maxClouds` | number | Maximum cloud percentage to include, from `0.0` to `100.0`. | | `minCoverage` | number | Minimum field coverage percentage to include, from `0.0` to `100.0`. | | `provider` | string | Filter by provider: `sentinel` or `planet`. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (default `20`, max `100`). | | `sort` | string | Sorting order with optional `,asc` or `,desc` suffix (e.g. `date,desc`). | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/satellite/api/fields/{id}/processes?startDate=2020-09-07&endDate=2020-09-10&provider=sentinel&page=0&size=10&sort=date,desc' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields/{id}/processes" headers = {"Authorization": f"Bearer {TOKEN}"} params = { "startDate": "2020-09-07", "endDate": "2020-09-10", "provider": "sentinel", "page": 0, "size": 10, "sort": "date,desc", } response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields/{id}/processes' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { startDate: '2020-09-07', endDate: '2020-09-10', provider: 'sentinel', page: 0, size: 10, sort: 'date,desc', } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} [ { "id": "c3d4e5f6-a7b8-4901-cdef-ab1234567890", "date": "2020-09-07T19:03:57.882Z", "clouds": 0, "provider": "sentinel", "status": "SUCCESS", "coverage": 100, "images": [ { "url": "https://satellite-imagery.withleaf.io/ndvi/c3d4e5f6.png", "downloadUrl": "https://satellite-imagery.withleaf.io/ndvi/c3d4e5f6.tif", "type": "NDVI" }, { "url": "https://satellite-imagery.withleaf.io/rgb/c3d4e5f6.png", "downloadUrl": "https://satellite-imagery.withleaf.io/rgb/c3d4e5f6.tif", "type": "RGB" } ], "processedTimestamp": "2020-09-07T19:03:58.881731Z" } ] ``` *** ### Get an image of satellite field GET `/fields/{id}/processes/{processId}` Returns a single satellite image process by ID. #### Parameters | Parameter | Type | Description | | ----------- | ----------- | ----------------------- | | `id` | path (UUID) | The satellite field ID. | | `processId` | path (UUID) | The process ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/satellite/api/fields/{id}/processes/{processId}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields/{id}/processes/{processId}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields/{id}/processes/{processId}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response Returns a single process object (same shape as the objects in the [Get images of satellite field](#get-images-of-satellite-field) response). *** ### Create a satellite field POST `/fields` Creates a satellite field and begins imagery processing. Once created, Leaf fetches available satellite imagery for the geometry from the specified providers. #### Parameters | Parameter | Type | Description | | ------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `externalId` | string (required) | Your identifier for the field. | | `geometry` | GeoJSON MultiPolygon (required) | The field boundary. | | `providers` | array of strings (optional) | Satellite providers to enable. Accepted values: `sentinel`, `planet`. Defaults to `["sentinel"]` if omitted. | Start with `sentinel` for development and testing. Add `planet` when you need higher-resolution imagery in production. #### Request body ```json theme={null} { "externalId": "north-quarter", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137] ]]] }, "providers": ["sentinel"] } ``` #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"externalId":"north-quarter","geometry":{"type":"MultiPolygon","coordinates":[[[[-93.48821,41.77137],[-93.48817,41.77143],[-93.48821,41.76068],[-93.48821,41.77137]]]]},"providers":["sentinel"]}' \ 'https://api.withleaf.io/services/satellite/api/fields' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "externalId": "north-quarter", "geometry": { "type": "MultiPolygon", "coordinates": [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] }, "providers": ["sentinel"] } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields' const headers = { Authorization: `Bearer ${TOKEN}` } const data = { externalId: 'north-quarter', geometry: { type: 'MultiPolygon', coordinates: [[[[-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137]]]] }, providers: ['sentinel'] } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "id": "b2a3c4d5-e6f7-4890-abcd-ef1234567890", "externalId": "north-quarter", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48817, 41.77143], [-93.48821, 41.76068], [-93.48821, 41.77137] ]]] }, "providers": ["sentinel"], "createdAt": "2023-07-21T13:01:11Z" } ``` *** ### Delete a satellite field DELETE `/fields/{id}` Deletes a satellite field and stops all future imagery processing for it. #### Parameters | Parameter | Type | Description | | --------- | ----------- | ----------------------- | | `id` | path (UUID) | The satellite field ID. | #### Request ```bash cURL theme={null} curl -X DELETE \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/satellite/api/fields/{id}' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields/{id}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.delete(endpoint, headers=headers) print(response.status_code) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields/{id}' const headers = { Authorization: `Bearer ${TOKEN}` } axios.delete(endpoint, { headers }) .then(res => console.log(res.status)) .catch(console.error) ``` *** ### Get subscription for Planet GET `/fields/{id}/subscription` Returns the Planet subscription status for a satellite field. This endpoint only applies to fields that have `Planet` in their `providers` array. #### Parameters | Parameter | Type | Description | | --------- | ----------- | ----------------------- | | `id` | path (UUID) | The satellite field ID. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/satellite/api/fields/{id}/subscription' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields/{id}/subscription" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields/{id}/subscription' const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` #### Response ```json theme={null} { "fieldId": "b2a3c4d5-e6f7-4890-abcd-ef1234567890", "provider": "Planet", "status": "ACTIVE" } ``` *** ### Reprocess satellite images POST `/fields/{id}/process/{processId}/reprocess` Triggers reprocessing for a specific satellite image. Use this if a process failed or if you need updated imagery outputs. #### Parameters | Parameter | Type | Description | | ----------- | ----------- | ---------------------------- | | `id` | path (UUID) | The satellite field ID. | | `processId` | path (UUID) | The process ID to reprocess. | #### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/satellite/api/fields/{id}/process/{processId}/reprocess' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/satellite/api/fields/{id}/process/{processId}/reprocess" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.post(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/satellite/api/fields/{id}/process/{processId}/reprocess' const headers = { Authorization: `Bearer ${TOKEN}` } axios.post(endpoint, null, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` # Soil Sampling Source: https://docs.withleaf.io/api-reference/soil Upload soil sample files, check batch status, and retrieve normalized result URLs, including GeoJSON and canonical JSON, through the Soil Sampling API. Upload soil sample `.zip` files, track processing status, and download normalized results in GeoJSON and canonical JSON formats. For output format details, see [Soil Sampling Overview](/soil/overview). The Soil Sampling service is currently available by invitation. Contact your account team to request access. ## Base URL ``` https://api.withleaf.io/services/soil/api ``` ## Endpoints | Method | Path | Description | | ----------------- | -------------------------- | --------------------------------------- | | POST | `/soil/batch` | [Upload soil files](#upload-soil-files) | | GET | `/soil/batch/{id}` | [Get batch status](#get-batch-status) | | GET | `/soil/batch/{id}/results` | [Get batch results](#get-batch-results) | | GET | `/soil/batches` | [List all batches](#list-all-batches) | *** ## Upload soil files POST `/soil/batch` Upload one or more `.zip` soil sample files for processing. Each file becomes an entry within the batch. Processing is asynchronous; poll the batch status endpoint to track progress. ### Parameters | Name | Type | In | Required | Description | | ------------ | ------- | ---------------- | -------- | ------------------------------------------- | | `files` | file(s) | body (multipart) | Yes | One or more `.zip` soil sample files | | `leafUserId` | UUID | query | Yes | The Leaf user ID associated with the upload | ### Headers | Header | Value | | --------------- | --------------------- | | `Authorization` | `Bearer YOUR_TOKEN` | | `Content-Type` | `multipart/form-data` | ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'files=@soil_samples.zip' \ 'https://api.withleaf.io/services/soil/api/soil/batch?leafUserId=YOUR_LEAF_USER_ID' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" leaf_user_id = "YOUR_LEAF_USER_ID" response = requests.post( f"https://api.withleaf.io/services/soil/api/soil/batch?leafUserId={leaf_user_id}", headers={"Authorization": f"Bearer {token}"}, files={"files": open("soil_samples.zip", "rb")} ) batch = response.json() ``` ```javascript JavaScript theme={null} const axios = require("axios"); const FormData = require("form-data"); const fs = require("fs"); const token = "YOUR_TOKEN"; const leafUserId = "YOUR_LEAF_USER_ID"; const form = new FormData(); form.append("files", fs.createReadStream("soil_samples.zip")); axios.post( `https://api.withleaf.io/services/soil/api/soil/batch?leafUserId=${leafUserId}`, form, { headers: { Authorization: `Bearer ${token}`, ...form.getHeaders(), }, } ) .then(({ data }) => console.log(data)) .catch(console.error); ``` To upload multiple files in a single batch, repeat the `files` field for each file. Each file becomes a separate entry within the batch. ```bash theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'files=@field_north.zip' \ -F 'files=@field_south.zip' \ 'https://api.withleaf.io/services/soil/api/soil/batch?leafUserId=YOUR_LEAF_USER_ID' ``` In Python, pass a list of tuples: ```python theme={null} files = [ ("files", open("field_north.zip", "rb")), ("files", open("field_south.zip", "rb")), ] response = requests.post(url, headers=headers, files=files) ``` ### Response `201 Created` ```json theme={null} { "id": "fd22d4bb-e0c3-45a1-8d70-c5cc886088e4", "status": "PROCESSING", "fileCount": 1, "entries": [ { "id": "aea0b567-8279-48a3-a226-7c57529a79b3", "fileName": "soil_samples.zip", "status": "PROCESSING", "downloadRawFile": "https://api.withleaf.io/services/files/soil/raw/.../file.zip", "downloadStandardGeojson": null, "downloadCanonicalJson": null, "errorMessage": null, "createdAt": "2026-04-10T19:21:46.358Z" } ], "createdAt": "2026-04-10T19:21:46.358Z", "updatedAt": "2026-04-10T19:21:46.900Z" } ``` *** ## Get batch status GET `/soil/batch/{id}` Retrieve the current status of a batch and all its entries. When an entry reaches `COMPLETED`, `downloadStandardGeojson` contains the flat result URL. `downloadCanonicalJson` contains the hierarchical result URL when that output is available. ### Parameters | Name | Type | In | Required | Description | | ------------ | ---- | ----- | -------- | -------------------------------------------------------------------------------------- | | `id` | UUID | path | Yes | Batch ID returned from the upload | | `leafUserId` | UUID | query | No | Filter by Leaf user ID. Omit to return results for all Leaf users under the API owner. | ### Request ```bash cURL theme={null} curl -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/soil/api/soil/batch/fd22d4bb-e0c3-45a1-8d70-c5cc886088e4' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" batch_id = "fd22d4bb-e0c3-45a1-8d70-c5cc886088e4" response = requests.get( f"https://api.withleaf.io/services/soil/api/soil/batch/{batch_id}", headers={"Authorization": f"Bearer {token}"} ) batch = response.json() ``` ```javascript JavaScript theme={null} const axios = require("axios"); const token = "YOUR_TOKEN"; const batchId = "fd22d4bb-e0c3-45a1-8d70-c5cc886088e4"; axios.get( `https://api.withleaf.io/services/soil/api/soil/batch/${batchId}`, { headers: { Authorization: `Bearer ${token}` }, } ) .then(({ data }) => console.log(data)) .catch(console.error); ``` ### Response `200 OK` ```json theme={null} { "id": "fd22d4bb-e0c3-45a1-8d70-c5cc886088e4", "status": "COMPLETED", "fileCount": 1, "entries": [ { "id": "aea0b567-8279-48a3-a226-7c57529a79b3", "fileName": "soil_samples.zip", "status": "COMPLETED", "downloadRawFile": "https://api.withleaf.io/services/files/soil/raw/.../file.zip", "downloadStandardGeojson": "https://api.withleaf.io/services/files/soil/results/.../result.geojson", "downloadCanonicalJson": "https://api.withleaf.io/services/files/soil/results/.../canonical.json", "errorMessage": null, "createdAt": "2026-04-10T19:21:46.358Z" } ], "createdAt": "2026-04-10T19:21:46.358Z", "updatedAt": "2026-04-10T19:21:49.344Z" } ``` *** ## Get batch results GET `/soil/batch/{id}/results` Returns a flat list of entries with their status and output URLs. Lighter than the full batch status when you only need the download links. ### Parameters | Name | Type | In | Required | Description | | ------------ | ---- | ----- | -------- | -------------------------------------------------------------------------------------- | | `id` | UUID | path | Yes | Batch ID | | `leafUserId` | UUID | query | No | Filter by Leaf user ID. Omit to return results for all Leaf users under the API owner. | ### Request ```bash cURL theme={null} curl -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/soil/api/soil/batch/fd22d4bb-e0c3-45a1-8d70-c5cc886088e4/results' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" batch_id = "fd22d4bb-e0c3-45a1-8d70-c5cc886088e4" response = requests.get( f"https://api.withleaf.io/services/soil/api/soil/batch/{batch_id}/results", headers={"Authorization": f"Bearer {token}"} ) results = response.json() ``` ```javascript JavaScript theme={null} const axios = require("axios"); const token = "YOUR_TOKEN"; const batchId = "fd22d4bb-e0c3-45a1-8d70-c5cc886088e4"; axios.get( `https://api.withleaf.io/services/soil/api/soil/batch/${batchId}/results`, { headers: { Authorization: `Bearer ${token}` }, } ) .then(({ data }) => console.log(data)) .catch(console.error); ``` ### Response `200 OK` ```json theme={null} [ { "entryId": "aea0b567-8279-48a3-a226-7c57529a79b3", "fileName": "soil_samples.zip", "status": "COMPLETED", "downloadStandardGeojson": "https://api.withleaf.io/services/files/soil/results/.../result.geojson", "downloadCanonicalJson": "https://api.withleaf.io/services/files/soil/results/.../canonical.json", "downloadRawFile": "https://api.withleaf.io/services/files/soil/raw/.../file.zip", "errorMessage": null } ] ``` *** ## List all batches GET `/soil/batches` List all batches for the authenticated API owner. Results are paginated. ### Parameters | Name | Type | In | Required | Description | | ------------ | ------- | ----- | -------- | ---------------------------------------------------------------------------------------------- | | `leafUserId` | UUID | query | No | Filter batches by Leaf user ID. Omit to return batches for all Leaf users under the API owner. | | `page` | integer | query | No | Page number (default: 0) | | `size` | integer | query | No | Page size (default: 20) | ### Request ```bash cURL theme={null} curl -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/soil/api/soil/batches?leafUserId=YOUR_LEAF_USER_ID&page=0&size=10' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" leaf_user_id = "YOUR_LEAF_USER_ID" response = requests.get( "https://api.withleaf.io/services/soil/api/soil/batches", headers={"Authorization": f"Bearer {token}"}, params={"leafUserId": leaf_user_id, "page": 0, "size": 10} ) batches = response.json() ``` ```javascript JavaScript theme={null} const axios = require("axios"); const token = "YOUR_TOKEN"; const leafUserId = "YOUR_LEAF_USER_ID"; axios.get( "https://api.withleaf.io/services/soil/api/soil/batches", { headers: { Authorization: `Bearer ${token}` }, params: { leafUserId, page: 0, size: 10 }, } ) .then(({ data }) => console.log(data)) .catch(console.error); ``` ### Response `200 OK` The response body is a JSON array of batch objects. Pagination metadata is returned in headers. | Header | Description | | --------------- | ------------------------------------------ | | `X-Total-Count` | Total number of batches matching the query | | `Link` | Pagination links (first, prev, next, last) | ```json theme={null} [ { "id": "fd22d4bb-e0c3-45a1-8d70-c5cc886088e4", "status": "COMPLETED", "fileCount": 1, "entries": [ { "id": "aea0b567-8279-48a3-a226-7c57529a79b3", "fileName": "soil_samples.zip", "status": "COMPLETED", "downloadRawFile": "https://api.withleaf.io/services/files/soil/raw/.../file.zip", "downloadStandardGeojson": "https://api.withleaf.io/services/files/soil/results/.../result.geojson", "downloadCanonicalJson": "https://api.withleaf.io/services/files/soil/results/.../canonical.json", "errorMessage": null, "createdAt": "2026-04-10T19:21:46.358Z" } ], "createdAt": "2026-04-10T19:21:46.358Z", "updatedAt": "2026-04-10T19:21:49.344Z" } ] ``` *** ## Status values | Status | Level | Description | | --------------------- | ------------- | --------------------------------------------------- | | `PROCESSING` | Entry / Batch | File uploaded, conversion in progress | | `COMPLETED` | Entry / Batch | Conversion finished, result URLs available | | `PARTIALLY_COMPLETED` | Batch only | Some entries completed, some failed | | `FAILED` | Entry / Batch | Conversion failed; check `errorMessage` for details | ## Error responses | Code | Reason | | ----- | ------------------------------------------------------------ | | `400` | API owner not enabled, missing files, or invalid parameters | | `401` | Missing or invalid JWT token | | `404` | Batch not found or does not belong to the authenticated user | ## Accessing result files The `downloadStandardGeojson`, `downloadCanonicalJson`, and `downloadRawFile` fields contain URLs for the converted results. These URLs require the same `Authorization: Bearer` header used for all other API calls. Requests without a valid token return `401`. `downloadStandardGeojson` is a flat GeoJSON FeatureCollection suited for mapping and GIS tools. `downloadCanonicalJson` is the hierarchical data model with lab info, provenance, and analyte categories when that output is available. `downloadCanonicalJson` may be `null` for some formats. Both output formats are described in [Soil Sampling Overview: Output Formats](/soil/overview#output-formats). ```bash cURL theme={null} curl -H 'Authorization: Bearer YOUR_TOKEN' \ -o result.geojson \ 'https://api.withleaf.io/services/files/soil/results/.../result.geojson' ``` ```python Python theme={null} import requests token = "YOUR_TOKEN" geojson_url = batch["entries"][0]["downloadStandardGeojson"] response = requests.get( geojson_url, headers={"Authorization": f"Bearer {token}"} ) with open("result.geojson", "wb") as f: f.write(response.content) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const fs = require("fs"); const token = "YOUR_TOKEN"; const geojsonUrl = batch.entries[0].downloadStandardGeojson; axios.get(geojsonUrl, { headers: { Authorization: `Bearer ${token}` }, responseType: "arraybuffer", }) .then(({ data }) => fs.writeFileSync("result.geojson", data)) .catch(console.error); ``` ## What to do next * [Soil Sampling Overview](/soil/overview) — Conceptual overview, output format, and common analytes. * [Supported Formats](/soil/supported-formats) — Full catalog of accepted soil data formats. * [Authentication](/getting-started/authentication) — How to get a Bearer token. # Leaf Users Source: https://docs.withleaf.io/api-reference/users Create and manage grower accounts (Leaf users) that connect to agricultural data providers and store field, operation, and machine data through the Leaf API. A Leaf user represents an end user of your application (for example, a grower). Each Leaf user keeps provider credentials and farm data organized under your API owner account. You create a Leaf user, attach provider credentials, and Leaf begins syncing field boundaries, machine files, and field operations from those providers. For conceptual background -- what a Leaf user represents, account structure patterns, and configuration inheritance -- see [Leaf Users overview](/leaf-users/overview). ## Base URL ``` https://api.withleaf.io/services/usermanagement/api ``` ## Endpoints | Action | Method | Path | | ---------------------------- | ------------------- | ------------- | | Get all Leaf users | GET | `/users` | | Get a Leaf user | GET | `/users/{id}` | | Create a Leaf user | POST | `/users` | | Partially update a Leaf user | PATCH | `/users/{id}` | | Update a Leaf user | PUT | `/users` | | Delete a Leaf user | DELETE | `/users/{id}` | ## Example resource shape ```json theme={null} { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "123 Field Rd, Ames, IA 50010", "externalId": "grower-9381", "trimbleCredentials": {}, "cnhiCredentials": {}, "johnDeereCredentials": {}, "ravenCredentials": {}, "climateFieldViewCredentials": {}, "staraCredentials": {}, "agLeaderCredentials": {}, "ravenSlingshotCredentials": {} } ``` This is an example resource shape, not an exhaustive list of every credential object that may appear on a Leaf user. *** ## Get all Leaf users `GET /users` Returns a paginated list of Leaf users belonging to your API owner account. ### Query parameters | Parameter | Type | Description | | ------------ | ------- | ----------------------------------- | | `email` | string | Filter by email address. | | `name` | string | Filter by name. | | `externalId` | string | Filter by your external identifier. | | `page` | integer | Page number (default `0`). | | `size` | integer | Page size (max `100`). | | `sort` | string | Sorting order (e.g. `name,asc`). | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users?page=0&size=10' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"page": 0, "size": 10} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const endpoint = "https://api.withleaf.io/services/usermanagement/api/users" const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers, params: { page: 0, size: 10 } }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} [ { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "123 Field Rd, Ames, IA 50010", "externalId": "grower-9381", "trimbleCredentials": {}, "cnhiCredentials": {}, "johnDeereCredentials": {}, "ravenCredentials": {}, "climateFieldViewCredentials": {}, "staraCredentials": {}, "agLeaderCredentials": {}, "ravenSlingshotCredentials": {} } ] ``` *** ## Get a Leaf user `GET /users/{id}` Returns a single Leaf user by ID, including all linked provider credentials. ### Path parameters | Parameter | Type | Description | | --------- | ------------- | ----------------- | | `id` | string (UUID) | The Leaf user ID. | ### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" endpoint = f"https://api.withleaf.io/services/usermanagement/api/users/{LEAF_USER_ID}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" const endpoint = `https://api.withleaf.io/services/usermanagement/api/users/${LEAF_USER_ID}` const headers = { Authorization: `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "123 Field Rd, Ames, IA 50010", "externalId": "grower-9381", "trimbleCredentials": {}, "cnhiCredentials": {}, "johnDeereCredentials": {}, "ravenCredentials": {}, "climateFieldViewCredentials": {}, "staraCredentials": {}, "agLeaderCredentials": {}, "ravenSlingshotCredentials": {} } ``` *** ## Create a Leaf user `POST /users` Creates a new Leaf user. After creation, you can attach provider credentials to start syncing data. ### Request body | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------- | | `name` | string | Yes | Full name. | | `email` | string | Yes | Email address. | | `phone` | string | No | Phone number. | | `address` | string | No | Mailing address. | | `externalId` | string | No | Your own identifier for the user. | You can also attach provider credentials in the same request by including the credentials ID. For example, to link John Deere credentials: ```json theme={null} { "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "123 Field Rd, Ames, IA 50010", "johnDeereCredentials": { "id": "a1b2c3d4-5678-9abc-def0-1234567890ab" } } ``` ### Request ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "123 Field Rd, Ames, IA 50010" }' \ 'https://api.withleaf.io/services/usermanagement/api/users' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "123 Field Rd, Ames, IA 50010" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const endpoint = "https://api.withleaf.io/services/usermanagement/api/users" const headers = { Authorization: `Bearer ${TOKEN}` } const data = { name: "Jane Smith", email: "jane@example.com", phone: "+15551234567", address: "123 Field Rd, Ames, IA 50010" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "123 Field Rd, Ames, IA 50010" } ``` *** ## Partially update a Leaf user `PATCH /users/{id}` Updates specific profile fields on an existing Leaf user without replacing the entire object. Only the fields you include in the request body are changed; everything else -- including provider credentials -- is left untouched. To update provider credentials or replace the full Leaf user object, use [PUT /users](#update-a-leaf-user) instead. ### Path parameters | Parameter | Type | Description | | --------- | ------------- | ----------------- | | `id` | string (UUID) | The Leaf user ID. | ### Request body All fields are optional. Include only the fields you want to change. | Field | Type | Description | | ------------ | ------ | --------------------------------- | | `name` | string | Full name. Must not be blank. | | `email` | string | Email address. Must not be blank. | | `phone` | string | Phone number. | | `address` | string | Mailing address. | | `externalId` | string | Your own identifier for the user. | ### Request ```bash cURL theme={null} curl -X PATCH \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "address": "456 Harvest Ln, Ames, IA 50010" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" endpoint = f"https://api.withleaf.io/services/usermanagement/api/users/{LEAF_USER_ID}" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"address": "456 Harvest Ln, Ames, IA 50010"} response = requests.patch(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" const endpoint = `https://api.withleaf.io/services/usermanagement/api/users/${LEAF_USER_ID}` const headers = { Authorization: `Bearer ${TOKEN}` } const data = { address: "456 Harvest Ln, Ames, IA 50010" } axios.patch(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response Returns the full Leaf user object with the updated fields. ```json theme={null} { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "456 Harvest Ln, Ames, IA 50010", "externalId": "grower-9381", "trimbleCredentials": {}, "cnhiCredentials": {}, "johnDeereCredentials": {} } ``` *** ## Update a Leaf user `PUT /users` Replaces an existing Leaf user with the provided object. You must include the `id` field in the request body. This is a full replacement. If the existing Leaf user has provider credentials and you omit them from the request body, those credentials are removed. Always include credentials you want to keep. ### Request body | Field | Type | Required | Description | | ------------ | ------------- | -------- | --------------------------------- | | `id` | string (UUID) | Yes | The Leaf user ID. | | `name` | string | Yes | Full name. | | `email` | string | Yes | Email address. | | `phone` | string | No | Phone number. | | `address` | string | No | Mailing address. | | `externalId` | string | No | Your own identifier for the user. | To keep or update linked credentials, include them in the body: ```json theme={null} { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "456 Harvest Ln, Ames, IA 50010", "johnDeereCredentials": { "id": "a1b2c3d4-5678-9abc-def0-1234567890ab" } } ``` ### Request ```bash cURL theme={null} curl -X PUT \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "456 Harvest Ln, Ames, IA 50010" }' \ 'https://api.withleaf.io/services/usermanagement/api/users' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "456 Harvest Ln, Ames, IA 50010" } response = requests.put(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const endpoint = "https://api.withleaf.io/services/usermanagement/api/users" const headers = { Authorization: `Bearer ${TOKEN}` } const data = { id: "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", name: "Jane Smith", email: "jane@example.com", phone: "+15551234567", address: "456 Harvest Ln, Ames, IA 50010" } axios.put(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ### Response ```json theme={null} { "id": "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f", "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "address": "456 Harvest Ln, Ames, IA 50010" } ``` *** ## Delete a Leaf user `DELETE /users/{id}` Deletes a Leaf user by ID. Returns HTTP `204 No Content` on success. ### Path parameters | Parameter | Type | Description | | --------- | ------------- | --------------------------- | | `id` | string (UUID) | The Leaf user ID to delete. | Deleting a Leaf user removes all associated provider credentials and stops data syncing for that user. This action cannot be undone. ### Request ```bash cURL theme={null} curl -X DELETE \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" endpoint = f"https://api.withleaf.io/services/usermanagement/api/users/{LEAF_USER_ID}" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.delete(endpoint, headers=headers) print(response.status_code) ``` ```javascript JavaScript theme={null} const axios = require("axios") const TOKEN = "YOUR_TOKEN" const LEAF_USER_ID = "f2a0b4d1-e567-4a8c-9e1f-0c3d5a7b9e2f" const endpoint = `https://api.withleaf.io/services/usermanagement/api/users/${LEAF_USER_ID}` const headers = { Authorization: `Bearer ${TOKEN}` } axios.delete(endpoint, { headers }) .then(res => console.log(res.status)) .catch(console.error) ``` # Weather API Source: https://docs.withleaf.io/api-reference/weather Retrieve daily and hourly forecast and historical weather for fields or coordinates, including temperature, precipitation, wind, humidity, and solar radiation. Retrieve forecast and historical weather data either by Leaf field or by latitude/longitude. Use the field endpoints when you want Leaf to resolve the field centroid for you, and use the lat/lon endpoints when you already know the exact coordinates. For conceptual background, see [Weather Overview](/weather/overview). ## Base URL ``` https://api.withleaf.io/services/weather/api ``` ## Endpoints | Description | Method | Path | | ---------------------------------------------------------------- | ---------------- | --------------------------------------------------------------- | | [Get daily forecast (field)](#get-daily-forecast-field) | GET | `/users/{leafUserId}/weather/forecast/field/{fieldId}/daily` | | [Get hourly forecast (field)](#get-hourly-forecast-field) | GET | `/users/{leafUserId}/weather/forecast/field/{fieldId}/hourly` | | [Get daily forecast (lat/lon)](#get-daily-forecast-latlon) | GET | `/weather/forecast/daily/{lat},{lon}` | | [Get hourly forecast (lat/lon)](#get-hourly-forecast-latlon) | GET | `/weather/forecast/hourly/{lat},{lon}` | | [Get daily historical (field)](#get-daily-historical-field) | GET | `/users/{leafUserId}/weather/historical/field/{fieldId}/daily` | | [Get hourly historical (field)](#get-hourly-historical-field) | GET | `/users/{leafUserId}/weather/historical/field/{fieldId}/hourly` | | [Get daily historical (lat/lon)](#get-daily-historical-latlon) | GET | `/weather/historical/daily/{lat},{lon}` | | [Get hourly historical (lat/lon)](#get-hourly-historical-latlon) | GET | `/weather/historical/hourly/{lat},{lon}` | Daily endpoints accept a maximum range of **366 days** per request. Hourly endpoints accept a maximum of **30 days**. Historical data less than 5 days old is unavailable. Use the forecast endpoints for recent weather data. *** ## Field-based endpoints These endpoints retrieve weather data centered on a Leaf user's field. The API uses the field's centroid coordinates automatically. ### Get daily forecast (field) GET `/users/{leafUserId}/weather/forecast/field/{fieldId}/daily` Returns daily forecast weather data for a field. #### Parameters | Parameter | Type | Location | Required | Description | | ---------- | ------ | -------- | -------- | ----------------------------------------------------------------------------------------- | | leafUserId | string | path | Yes | UUID of the Leaf user. | | fieldId | string | path | Yes | UUID of the field. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Forecast model: `gfs` (default), `icon`, `ifs`, `metNordic`, `jma`, `gem`, `arpegeArome`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/users/{leafUserId}/weather/forecast/field/{fieldId}/daily?startTime=2026-03-09&endTime=2026-03-15' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = ( "https://api.withleaf.io/services/weather/api" "/users/{leafUserId}/weather/forecast/field/{fieldId}/daily" ) params = {"startTime": "2026-03-09", "endTime": "2026-03-15"} response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api" + "/users/{leafUserId}/weather/forecast/field/{fieldId}/daily"; axios .get(url, { headers, params: { startTime: "2026-03-09", endTime: "2026-03-15" } }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "date": "2026-03-09", "temperatureMax": 18.2, "temperatureMin": 6.4, .... }, { "date": "2026-03-10", "temperatureMax": 20.1, "temperatureMin": 7.8, .... } ] ``` *** ### Get hourly forecast (field) GET `/users/{leafUserId}/weather/forecast/field/{fieldId}/hourly` Returns hourly forecast weather data for a field. #### Parameters | Parameter | Type | Location | Required | Description | | ---------- | ------ | -------- | -------- | ----------------------------------------------------------------------------------------- | | leafUserId | string | path | Yes | UUID of the Leaf user. | | fieldId | string | path | Yes | UUID of the field. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Forecast model: `gfs` (default), `icon`, `ifs`, `metNordic`, `jma`, `gem`, `arpegeArome`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/users/{leafUserId}/weather/forecast/field/{fieldId}/hourly?startTime=2026-03-09&endTime=2026-03-10' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = ( "https://api.withleaf.io/services/weather/api" "/users/{leafUserId}/weather/forecast/field/{fieldId}/hourly" ) params = {"startTime": "2026-03-09", "endTime": "2026-03-10"} response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api" + "/users/{leafUserId}/weather/forecast/field/{fieldId}/hourly"; axios .get(url, { headers, params: { startTime: "2026-03-09", endTime: "2026-03-10" } }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "timestamp": "2026-03-09T00:00:00Z", "temperature": 12.3, "humidity": 65, .... }, { "timestamp": "2026-03-09T01:00:00Z", "temperature": 11.8, "humidity": 68, .... } ] ``` *** ### Get daily historical (field) GET `/users/{leafUserId}/weather/historical/field/{fieldId}/daily` Returns daily historical weather data for a field. #### Parameters | Parameter | Type | Location | Required | Description | | ---------- | ------ | -------- | -------- | ------------------------------------------------- | | leafUserId | string | path | Yes | UUID of the Leaf user. | | fieldId | string | path | Yes | UUID of the field. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Historical model: `era5` (default) or `era5Land`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/users/{leafUserId}/weather/historical/field/{fieldId}/daily?startTime=2025-06-01&endTime=2025-06-30' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = ( "https://api.withleaf.io/services/weather/api" "/users/{leafUserId}/weather/historical/field/{fieldId}/daily" ) params = {"startTime": "2025-06-01", "endTime": "2025-06-30"} response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api" + "/users/{leafUserId}/weather/historical/field/{fieldId}/daily"; axios .get(url, { headers, params: { startTime: "2025-06-01", endTime: "2025-06-30" } }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "date": "2025-06-01", "temperatureMax": 29.4, "temperatureMin": 17.1, .... }, { "date": "2025-06-02", "temperatureMax": 31.0, "temperatureMin": 18.6, .... } ] ``` *** ### Get hourly historical (field) GET `/users/{leafUserId}/weather/historical/field/{fieldId}/hourly` Returns hourly historical weather data for a field. #### Parameters | Parameter | Type | Location | Required | Description | | ---------- | ------ | -------- | -------- | ------------------------------------------------- | | leafUserId | string | path | Yes | UUID of the Leaf user. | | fieldId | string | path | Yes | UUID of the field. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Historical model: `era5` (default) or `era5Land`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/users/{leafUserId}/weather/historical/field/{fieldId}/hourly?startTime=2025-06-01&endTime=2025-06-02' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = ( "https://api.withleaf.io/services/weather/api" "/users/{leafUserId}/weather/historical/field/{fieldId}/hourly" ) params = {"startTime": "2025-06-01", "endTime": "2025-06-02"} response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api" + "/users/{leafUserId}/weather/historical/field/{fieldId}/hourly"; axios .get(url, { headers, params: { startTime: "2025-06-01", endTime: "2025-06-02" } }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "timestamp": "2025-06-01T00:00:00Z", "temperature": 22.1, "humidity": 55, .... }, { "timestamp": "2025-06-01T01:00:00Z", "temperature": 21.5, "humidity": 58, .... } ] ``` *** ## Lat/lon endpoints These endpoints retrieve weather data for an arbitrary latitude/longitude pair. They support additional `model` and `units` query parameters. ### Get daily forecast (lat/lon) GET `/weather/forecast/daily/{lat},{lon}` Returns daily forecast weather data for a coordinate pair. #### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | ----------------------------------------------------------------------------------------- | | lat | number | path | Yes | Latitude coordinate. | | lon | number | path | Yes | Longitude coordinate. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Forecast model: `gfs` (default), `icon`, `ifs`, `metNordic`, `jma`, `gem`, `arpegeArome`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/weather/forecast/daily/39.7128,-86.1580?startTime=2026-03-09&endTime=2026-03-15&model=gfs&units=metric' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = "https://api.withleaf.io/services/weather/api/weather/forecast/daily/39.7128,-86.1580" params = { "startTime": "2026-03-09", "endTime": "2026-03-15", "model": "gfs", "units": "metric", } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api/weather/forecast/daily/39.7128,-86.1580"; axios .get(url, { headers, params: { startTime: "2026-03-09", endTime: "2026-03-15", model: "gfs", units: "metric" }, }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "date": "2026-03-09", "temperatureMax": 15.6, "temperatureMin": 3.2, .... }, { "date": "2026-03-10", "temperatureMax": 17.0, "temperatureMin": 4.8, .... } ] ``` *** ### Get hourly forecast (lat/lon) GET `/weather/forecast/hourly/{lat},{lon}` Returns hourly forecast weather data for a coordinate pair. #### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | ----------------------------------------------------------------------------------------- | | lat | number | path | Yes | Latitude coordinate. | | lon | number | path | Yes | Longitude coordinate. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Forecast model: `gfs` (default), `icon`, `ifs`, `metNordic`, `jma`, `gem`, `arpegeArome`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/weather/forecast/hourly/39.7128,-86.1580?startTime=2026-03-09&endTime=2026-03-10&model=gfs&units=metric' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = "https://api.withleaf.io/services/weather/api/weather/forecast/hourly/39.7128,-86.1580" params = { "startTime": "2026-03-09", "endTime": "2026-03-10", "model": "gfs", "units": "metric", } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api/weather/forecast/hourly/39.7128,-86.1580"; axios .get(url, { headers, params: { startTime: "2026-03-09", endTime: "2026-03-10", model: "gfs", units: "metric", }, }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "timestamp": "2026-03-09T00:00:00Z", "temperature": 8.4, "humidity": 72, .... }, { "timestamp": "2026-03-09T01:00:00Z", "temperature": 7.9, "humidity": 74, .... } ] ``` *** ### Get daily historical (lat/lon) GET `/weather/historical/daily/{lat},{lon}` Returns daily historical weather data for a coordinate pair. #### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | ------------------------------------------------- | | lat | number | path | Yes | Latitude coordinate. | | lon | number | path | Yes | Longitude coordinate. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Historical model: `era5` (default) or `era5Land`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/weather/historical/daily/39.7128,-86.1580?startTime=2025-06-01&endTime=2025-06-30&model=era5&units=metric' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = "https://api.withleaf.io/services/weather/api/weather/historical/daily/39.7128,-86.1580" params = { "startTime": "2025-06-01", "endTime": "2025-06-30", "model": "era5", "units": "metric", } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api/weather/historical/daily/39.7128,-86.1580"; axios .get(url, { headers, params: { startTime: "2025-06-01", endTime: "2025-06-30", model: "era5", units: "metric" }, }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "date": "2025-06-01", "temperatureMax": 30.2, "temperatureMin": 18.4, .... }, { "date": "2025-06-02", "temperatureMax": 28.7, "temperatureMin": 17.9, .... } ] ``` *** ### Get hourly historical (lat/lon) GET `/weather/historical/hourly/{lat},{lon}` Returns hourly historical weather data for a coordinate pair. #### Parameters | Parameter | Type | Location | Required | Description | | --------- | ------ | -------- | -------- | ------------------------------------------------- | | lat | number | path | Yes | Latitude coordinate. | | lon | number | path | Yes | Longitude coordinate. | | startTime | string | query | No | Start date in `YYYY-MM-DD` format. | | endTime | string | query | No | End date in `YYYY-MM-DD` format. | | model | string | query | No | Historical model: `era5` (default) or `era5Land`. | | units | string | query | No | Unit system: `metric` (default) or `imperial`. | #### Request ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/weather/api/weather/historical/hourly/39.7128,-86.1580?startTime=2025-06-01&endTime=2025-06-02&model=era5&units=metric' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} url = "https://api.withleaf.io/services/weather/api/weather/historical/hourly/39.7128,-86.1580" params = { "startTime": "2025-06-01", "endTime": "2025-06-02", "model": "era5", "units": "metric", } response = requests.get(url, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const headers = { Authorization: `Bearer ${TOKEN}` }; const url = "https://api.withleaf.io/services/weather/api/weather/historical/hourly/39.7128,-86.1580"; axios .get(url, { headers, params: { startTime: "2025-06-01", endTime: "2025-06-02", model: "era5", units: "metric", }, }) .then((res) => console.log(res.data)) .catch(console.error); ``` #### Response ```json theme={null} [ { "timestamp": "2025-06-01T00:00:00Z", "temperature": 22.1, "humidity": 55, .... }, { "timestamp": "2025-06-01T01:00:00Z", "temperature": 21.5, "humidity": 58, .... } ] ``` *** ## Models reference ### Forecast models | Model | Description | | ------------- | ---------------------------------- | | `gfs` | Global Forecast System (default) | | `icon` | ICON by DWD | | `ifs` | IFS by ECMWF | | `metNordic` | MET Nordic by MET Norway | | `jma` | JMA by Japan Meteorological Agency | | `gem` | GEM by Environment Canada | | `arpegeArome` | ARPEGE/AROME by Meteo-France | ### Historical models | Model | Description | | ---------- | ---------------------------------------- | | `era5` | ERA5 reanalysis (default) | | `era5Land` | ERA5-Land reanalysis (higher resolution) | Use `era5Land` when you need finer spatial resolution for land-surface variables like soil temperature and soil moisture. # Assets Source: https://docs.withleaf.io/beta/assets Manage machines, implements, and operators for a Leaf user. Track equipment from John Deere, CNHi, Trimble, and Stara, or create your own records. The Assets API gives you access to machines, implements, and operators tied to a Leaf user. Equipment data comes from two sources: automatic syncing from connected providers (John Deere, CNHi, Stara, Trimble) and manual creation through the API. ## How it works When a Leaf user connects provider credentials, Leaf pulls machine, implement, and operator records from that provider. These show up with `originType: PROVIDER_POOLED`. You can also create machines manually (`originType: USER_CREATED`), which is useful for equipment that doesn't exist in a cloud platform. Each machine can be linked to machine files, giving you per-equipment usage data including distance traveled, fuel consumption, and time windows. Implements and operators are currently read-only from providers. Implements come from John Deere and Trimble. Operators come from John Deere. ## Key concepts **Machine** — A vehicle or self-propelled unit (tractor, sprayer, harvester). Machines have properties like `name`, `serialNumber`, `vin`, `make`, `model`, `category`, and `modelYear`. Machines sourced from a provider cannot be edited or deleted; only user-created machines can. **Implement** — An attachment pulled by or mounted on a machine (planter, cultivator, header). Implements are synced from providers and cannot be created or modified through Leaf. **Operator** — A person who operated equipment during field work. Operators are synced from John Deere and include name, license, and status fields. **Origin types:** * `PROVIDER_POOLED` — Synced from a connected provider. Read-only. * `FILE_POOLED` — Extracted from uploaded machine file data. * `USER_CREATED` — Created manually via the API. Editable and deletable. **Machine files** — You can query which machine files are associated with a specific machine. Each machine-file record includes the `leafFileId`, start and end times, distance, and fuel consumption for that file. ## What to do next * [Assets API Reference](/api-reference/beta-assets) — Full endpoint details for machines, implements, and operators. # Input Validator Source: https://docs.withleaf.io/beta/input-validator Look up products, varieties, and tank mixes from standardized databases. Match names from machine files against known products in Agrian, CDMS, and John Deere. The Input Validator API gives you access to standardized product, variety, and tank mix databases. It also matches the raw product names found in machine files against known products, so you can identify what was actually applied or planted in a field operation. ## How it works Leaf maintains databases of agricultural products sourced from Agrian, CDMS, and John Deere. When a machine file contains a product name like "ams" or "counter," the matching endpoint finds the closest known product and returns it with a confidence score. Matching has two statuses: * `PREDICTED` — Leaf's best guess, returned with a numeric `score`. Higher scores indicate stronger matches. * `VALIDATED` — A human has confirmed the match is correct, either by approving the prediction or by manually assigning a different product. You can approve a prediction or override it with a different product ID using the PATCH endpoint. Leaf tracks the full change history for each match. ## Key concepts **Products** — Chemical, fertilizer, or additive records with registration numbers, active ingredients, labels, and physical state. Products come from three label providers: Agrian, CDMS, and John Deere. Agrian and CDMS products are available globally; John Deere products are scoped to the Leaf user level. **Summarized products** — Product names extracted directly from a Leaf user's machine files, before any matching. Useful for seeing exactly what names the monitor recorded. **Varieties** — Seed variety records with crop type, company name, and status. Currently sourced from John Deere at the Leaf user level. **Tank mixes** — Predefined combinations of products with a carrier and one or more components, including solution rate and target crops. Sourced from John Deere. **Product matching** — Given a field operation ID, Leaf returns the best product match for each raw product name in that operation. You can validate matches, override them, and view the change history. ## What to do next * [Input Validator API Reference](/api-reference/beta-input) — Full endpoint details for products, varieties, tank mixes, and product matching. # Layers Source: https://docs.withleaf.io/beta/layers Retrieve imagery layers from Sentera including tassel count, stand count, NDVI, and RGB. Upload RGB layers to Climate FieldView through the Leaf API. The Layers API provides access to imagery layers synced from Sentera and allows you to push RGB layers to Climate FieldView. Layers are raster datasets tied to specific fields and Leaf users. ## How it works After you connect Sentera credentials for a Leaf user, Leaf syncs available layers automatically. Each layer has a `type` (RGB, NDVI, TASSEL\_COUNT, or STAND\_COUNT), a download URL, and references to the Leaf fields it covers. You can also upload GeoTIFF layers to Climate FieldView. Uploaded layers are sent directly to Climate FieldView and are not stored on the Leaf side. You must complete the [Sentera integration steps](https://withleaf.io/en/whats-new/sentera-integration-with-leaf/) before layers appear for a Leaf user. ## Key concepts **Layer types** — Four types are available from Sentera: * `RGB` — True color imagery. * `NDVI` — Normalized Difference Vegetation Index. * `TASSEL_COUNT` — Tassel detection counts. * `STAND_COUNT` — Plant stand counts. **Layer fields** — Each layer includes a `leafFieldIds` array linking it to one or more Leaf field boundaries it covers geographically. **Climate FieldView upload** — You can send RGB GeoTIFF files to Climate FieldView via the upload endpoint. The file must be a multi-band GeoTIFF with 3 bands (Red, Green, Blue), use a UTM projection with WGS84 datum, and include GDAL metadata with `acquisitionStartDate`, `acquisitionEndDate`, and `isCalibrated`. Maximum file size is 5 MB. ## What to do next * [Layers API Reference](/api-reference/beta-layers) — Full endpoint details for listing layers and uploading to Climate FieldView. # Operations Planning Source: https://docs.withleaf.io/beta/operations-planning Plan and schedule field operations before they are executed. This feature is in early development and not yet available for general use. Operations Planning will allow you to define planned field operations (planting, application, tillage, harvest) before they happen, then track execution against the plan. This closes the loop between prescription/intent and the actual machine data Leaf collects. Documentation will be added here as the feature reaches testable status. Contact [help@withleaf.io](mailto:help@withleaf.io) if you want early access or have a use case to share. ## What to do next * [Beta Overview](/beta/overview) — See all current beta features. * [Field Operations](/machine-data/field-operations) — How Leaf processes executed field operations from machine data. # Beta Features Source: https://docs.withleaf.io/beta/overview Leaf beta features available for use but subject to change: asset management, prescription maps, custom layers, input validation, and operations planning. This section covers Leaf API features that are available and functional but still subject to breaking changes. Beta endpoints use the `/services/beta/` base path. Leaf may modify request/response schemas, rename fields, or adjust default values as these features mature. ## What's in beta **[Assets](/beta/assets)** — Manage machines, implements, and operators associated with a Leaf user. Assets are pooled from connected providers or created manually, and link to machine files for equipment-level reporting. **[Prescriptions](/beta/prescriptions)** — Upload and list prescription maps across providers including John Deere, CNHi, Climate FieldView, Raven Slingshot, Trimble, and Ag Leader. **[Layers](/beta/layers)** — Retrieve imagery layers (tassel count, stand count, NDVI, RGB) from Sentera and upload layers to Climate FieldView. **[Input Validator](/beta/input-validator)** — Look up and match agricultural products, varieties, and tank mixes against standardized databases. Includes automatic product matching for field operations. **[Operations Planning](/beta/operations-planning)** — Plan and schedule field operations ahead of execution. ## How beta features graduate When a beta feature stabilizes, Leaf moves it to the main API surface under `/services/`. Existing beta endpoints continue to work during a deprecation window, typically 90 days. Leaf announces graduations in the changelog. # Prescriptions Source: https://docs.withleaf.io/beta/prescriptions Upload prescription maps through Leaf and use provider-specific list or download endpoints where available. The Prescriptions API lets you upload prescription (Rx) maps to a grower's provider account. Leaf also stores uploaded prescription files and prescription records. Depending on the provider, you can also list existing prescriptions or download them through provider-specific endpoints. ## How it works You upload a `.zip` file containing a shapefile set (`.shp`, `.dbf`, `.shx` — all with the same base name) to the provider-specific endpoint for a Leaf user. Leaf uses that user's provider credentials to push the prescription into the provider's platform and stores the uploaded file and prescription metadata. Where provider list or download endpoints exist, you can use those to retrieve provider-side prescription data. Each provider has slightly different requirements and endpoint support: **John Deere** — The zip must contain a folder named `Rx/` holding the shapefile set. Requires an `organizationId` query parameter. Supports upload, list, and download. **CNHi** — Flat zip with no subfolders. Requires a `companyId` query parameter, which you can get from the grower endpoints' `providerOrganizationId` field. Supports upload and list. **Climate FieldView** — Flat zip, no subfolders. No extra parameters needed. Upload only. **Raven Slingshot** — Flat zip, no subfolders. Supports upload and list. **Trimble** — Flat zip, no subfolders. Requires `organizationId`, `rateColumn`, and `rateUnit`. Upload only. **Ag Leader** — Flat zip, no subfolders. Upload only. ## Key concepts **Shapefile set** — Every prescription upload requires three files with identical base names: `.shp` (geometry), `.dbf` (attributes), `.shx` (spatial index). These are bundled into a single `.zip`. **Provider-scoped** — Listing and download availability depends on the provider endpoint. John Deere supports upload, list, and download. CNHi and Raven Slingshot support upload and list. Climate FieldView, Trimble, and Ag Leader support upload only. **Organization/company ID** — John Deere and CNHi require you to specify which provider-side organization the prescription belongs to. Other providers derive this from the Leaf user's credentials. ## What to do next * [Prescriptions API Reference](/api-reference/beta-prescriptions) — Full endpoint details for uploading and listing prescriptions across all supported providers. # Usage Tracking Source: https://docs.withleaf.io/billing/overview Track your Leaf API usage across field boundaries, machine file processing, field operations, and satellite imagery using contracts and consumption endpoints. Leaf tracks data processing usage, not API access. You have unlimited API calls to retrieve processed data. Charges occur when Leaf successfully processes your data (pulls a file, creates a boundary, processes an image), not when you download or query results afterward. ## How usage tracking works Usage is based on **spatially unique acres** processed per Leaf user. A few rules to keep in mind: * The same geographic area processed under different Leaf users counts separately for each user. * Repeated retrieval of the same processed data does not incur additional charges. * If processing fails, the area is not counted. * Deleted boundaries count toward usage for the current contract term only. ## What gets tracked Each service tracks area with a specific product identifier: | Product ID | What it tracks | | ---------------------------- | --------------------------------------------------------------------------------------- | | `UNIFIED_AREA` | Total unique area consumed across all services during the contract period | | `FIELDS_BOUNDARY` | Field boundary area processed during the contract period | | `AUDIT_FIELDS_BOUNDARY` | Current total boundary area (not bounded by contract dates) | | `OPERATIONS_FILE` | Machine file area processed during the contract period | | `OPERATIONS_OPERATION` | Field operation area (machine files intersecting boundaries) during the contract period | | `SATELLITE_PROCESS_PLANET` | Planet satellite imagery area processed during the contract period | | `SATELLITE_PROCESS_SENTINEL` | Sentinel satellite imagery area processed during the contract period | ## Contracts Contracts are automatically generated when you start using Leaf services. Each contract tracks usage for one product type with annual cycles. Use the contracts endpoints to find your contract IDs and check quota limits. ## Tracking usage There are several ways to monitor consumption: **API endpoints.** The billing endpoints give you programmatic access to usage data at both the API owner and individual Leaf user level. **API owner level** shows spatially unique area across all your users (overlapping boundaries between users are deduplicated). This gives the overall footprint of your account. **Leaf user level** shows each user's individual processing. These values may include spatial overlap between users, which is expected and reflects how billing is calculated. The difference between the API owner total and the sum of all Leaf user totals indicates spatial overlap across your organization. **Alerts.** Set up webhooks for events like `fieldBoundaryCreated`, `providerFileProcessingFinished`, and `operationProcessingFinished` to track processing as it happens. **X-Total-Count header.** Most `GET All` endpoints include this header in the response, giving you a quick count of resources without paging through all results. **CSM reports.** Your Customer Success Manager can provide usage and billing reports broken down by API owner and Leaf user. ## Endpoints Base URL: `https://api.withleaf.io/services/billingapplication/api` | Action | Method | Path | | --------------------------- | ------ | -------------------------------------------------------------------- | | List contracts | GET | `/billing/contracts` | | Get contract details | GET | `/billing/contracts/{contractId}` | | Get daily usage summary | GET | `/billing/contracts/{contractId}/consumption` | | Get usage range (API owner) | GET | `/billing/contracts/{contractId}/consumption/api-owner` | | Get usage range (Leaf user) | GET | `/billing/contracts/{contractId}/consumption/leaf-user/{leafUserId}` | The daily usage summary returns current-day data by default. Pass a `timestamp` parameter to get data for a specific day. The range endpoints accept `startTime` and `endTime` parameters and return daily consumption breakdowns. ## Avoiding unexpected charges * Assign provider credentials to one Leaf user per actual customer. Re-connecting the same data under multiple Leaf users counts each user's acreage separately. * Use `customDataSync` and `organizationDataSync` to limit which fields and organizations Leaf processes. * Create a separate API owner for test environments (e.g., `leaf-test@yourcompany.com`) to isolate test usage from production billing. ## What to do next * [Billing API Reference](/api-reference/billing) for full endpoint details. * [Configuration](/configuration/overview) for `customDataSync` and `organizationDataSync` to control data scope. * [Alerts Overview](/alerts/overview) to set up usage monitoring webhooks. # Leaf Link Source: https://docs.withleaf.io/components/leaf-link Embed Leaf Link widgets so growers can connect John Deere, Climate FieldView, CNHi, CNHI FieldOps, AgLeader, Trimble, and other accounts. Leaf Link is a set of drop-in UI widgets that handle two tasks: connecting your users' provider accounts and uploading machine files. Leaf provides React and Angular packages, with provider availability depending on the package and widget runtime. ## Widgets Leaf Link includes two widgets: * **Provider Connection** lets your users authenticate with CNHi, CNHI FieldOps, John Deere, Trimble, Climate FieldView, AgLeader, Raven Slingshot, Lindsay, and Stara from inside your application. * **File Upload** lets your users drag-and-drop or browse for `.zip` files containing machine data. Leaf processes these files through the standard conversion pipeline. Both widgets authenticate using a per-Leaf-user API key (not your API Owner bearer token). Create API keys via the [API Key endpoints](/api-reference/leaf-link). ## Prerequisites 1. A Leaf account with at least one Leaf user created. 2. A Leaf user API key (created via `POST /api-keys`). 3. For provider connection: provider app credentials registered with Leaf for each provider you want to enable. ## Provider setup Each provider requires a one-time registration of your application credentials with Leaf. You also need to add `https://widget.withleaf.io` as a redirect/callback URL in the provider's developer portal. | Provider | Redirect URL field | Credentials needed | | --------------------------- | --------------------------- | --------------------------------------------- | | John Deere | Redirect URI | `clientKey`, `clientSecret` | | Climate FieldView | (none required) | `apiKey`, `clientId`, `clientSecret` | | CNHI (AFS Connect - Legacy) | App OAuth Callback URL(s) | `clientId`, `clientSecret`, `subscriptionKey` | | CNHI FieldOps | App OAuth Callback URL(s) | `clientId`, `clientSecret`, `subscriptionKey` | | AgLeader | Redirection URL | `privateKey`, `publicKey` | | Trimble | Authentication Callback URL | `applicationName`, `clientId`, `clientSecret` | | Raven Slingshot | (none required) | `apiKey`, `sharedSecret` | | Lindsay | Redirect URI | `clientId`, `clientSecret` | | Stara | (none required) | `user`, `pwd` | Register credentials by calling `POST /usermanagement/api/app-keys/{Provider}/{appName}` with the provider-specific fields. ```bash cURL theme={null} curl -X POST \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -d '{"clientKey": "your-app-id", "clientSecret": "your-secret"}' \ 'https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/MyApp/PRODUCTION' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/MyApp/PRODUCTION" headers = {"Authorization": f"Bearer {TOKEN}"} data = {"clientKey": "your-app-id", "clientSecret": "your-secret"} response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const endpoint = "https://api.withleaf.io/services/usermanagement/api/app-keys/JohnDeere/MyApp/PRODUCTION"; const headers = { Authorization: `Bearer ${TOKEN}` }; const data = { clientKey: "your-app-id", clientSecret: "your-secret" }; axios.post(endpoint, data, { headers }).then(res => console.log(res.data)); ``` ## Installation ### React ```shell theme={null} npm i @withleaf/leaf-link-react ``` **Provider Connection:** ```javascript theme={null} import { Providers } from "@withleaf/leaf-link-react"; function App() { return ( ); } ``` **File Upload:** ```javascript theme={null} import { FileUpload } from "@withleaf/leaf-link-react"; function App() { return ( ); } ``` ### Angular ```shell theme={null} npm i @withleaf/leaf-link-angular ``` **Provider Connection:** ```javascript theme={null} import { ProvidersModule } from "@withleaf/leaf-link-angular"; ``` ```html theme={null} ``` **File Upload:** ```javascript theme={null} import { FileUploadModule } from "@withleaf/leaf-link-angular"; ``` ```html theme={null} ``` ## Properties ### Provider Connection | Property | Type | Description | | ------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | `apiKey` | String | Leaf user API key (required) | | `leafUser` | String | Leaf user ID (required) | | `companyName` | String | Your company name, displayed in the widget | | `companyLogo` | String | URL to your company logo (PNG, JPEG, or SVG) | | `isDarkMode` | Boolean | Enable dark mode. Default: `false` | | `locale` | String | Force language: `en_US`, `pt_BR`, `es_ES`, or `fr_FR`. Defaults to browser language | | `title` | String | Widget title. Default: "Select your integration" | | `showSearchbar` | Boolean | Show/hide the provider search bar. Default: `true` | | `allowedProviders` | `string[]` | Restrict the widget to a specific list of providers | | `fastMode` | Boolean | Enable the widget's fast mode behavior | | `applications` | `Array<{ appName: string; provider: string; clientEnvironment: string }>` | Preload provider app settings for the widget | ### File Upload | Property | Type | Description | | ---------------- | ------- | --------------------------------------- | | `apiKey` | String | Leaf user API key (required) | | `leafUser` | String | Leaf user ID (required) | | `companyName` | String | Your company name | | `companyLogo` | String | URL to your company logo | | `isDarkMode` | Boolean | Enable dark mode. Default: `false` | | `locale` | String | Force language | | `title` | String | Text displayed at the top of the widget | | `filesTimeRange` | Number | Days of upload history to display | ## Hooks Both widgets expose hooks for tracking widget state in your application. ### Provider Connection hooks Use `useLeaf()` from `@withleaf/leaf-link-react`: | Hook | Type | Description | | ---------------------- | ----------------------------------- | ------------------------------------------------- | | `providersConnected` | `string[]` | Provider names connected after the flow completes | | `providerWidgetStatus` | `{ code: number; message: string }` | Widget status: `-1` Error, `0` Started, `1` Done | ### File Upload hooks | Hook | Type | Description | | -------------- | ---------- | ------------------------------------ | | `leafBatchIds` | `string[]` | Batch IDs for each successful upload | To use hooks in React, wrap your component tree with the `` context provider: ```javascript theme={null} import { Leaf, Providers, useLeaf } from "@withleaf/leaf-link-react"; function StatusDisplay() { const { providerWidgetStatus, providersConnected } = useLeaf(); return
{JSON.stringify({ providerWidgetStatus, providersConnected }, null, 2)}
; } function App() { return ( ); } ``` In Angular, use the `(getWidgetStatus)` output binding on the `` component. The Angular package does not currently expose `Stara` in its provider list. ## What to do next * [Magic Link](/components/magic-link) for generating shareable authentication and upload links without embedding widgets. * [Leaf Connect](/components/leaf-connect) for sharing data between API owners. * [Leaf Link API Reference](/api-reference/leaf-link) for API key and app info endpoints. # Magic Link Source: https://docs.withleaf.io/components/magic-link Generate shareable URLs that let growers connect their provider accounts or upload machine files without building a custom authentication UI. Magic Link generates shareable URLs that give your users access to Leaf's provider connection or file upload flows, without requiring you to embed any widget code. Send the link via email, SMS, or in-app notification. The link controls the entire authentication or upload experience and can be customized with your branding. ## Link types There are three types of Magic Links: **Provider (multiple providers)** lets the user connect to any of your enabled providers in one session. Requires provider app info to be registered with Leaf first (same setup as [Leaf Link](/components/leaf-link#provider-setup)). **Authentication (single provider)** restricts the flow to one specific provider. Useful when you know which provider the user needs to connect. **File Upload** lets the user upload `.zip` files containing machine data for processing. ## Creating a Magic Link Each link type has its own creation endpoint under `https://api.withleaf.io/services/widgets/api`. ### With an existing Leaf user ```bash cURL theme={null} curl -X POST \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -d '{"settings": {"companyName": "Acme Ag", "companyLogo": "https://example.com/logo.svg"}}' \ 'https://api.withleaf.io/services/widgets/api/magic-link/users/{leafUserId}/provider' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" leaf_user_id = "your-leaf-user-id" endpoint = f"https://api.withleaf.io/services/widgets/api/magic-link/users/{leaf_user_id}/provider" headers = {"Authorization": f"Bearer {TOKEN}"} data = { "settings": { "companyName": "Acme Ag", "companyLogo": "https://example.com/logo.svg" } } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const leafUserId = "your-leaf-user-id"; const endpoint = `https://api.withleaf.io/services/widgets/api/magic-link/users/${leafUserId}/provider`; const headers = { Authorization: `Bearer ${TOKEN}` }; const data = { settings: { companyName: "Acme Ag", companyLogo: "https://example.com/logo.svg" } }; axios.post(endpoint, data, { headers }).then(res => console.log(res.data)); ``` ### With automatic Leaf user creation You can also create a Magic Link without specifying a `leafUserId`. In that flow, Leaf creates or reuses the Leaf user as part of link creation based on the external ID you provide: ``` POST /magic-link/provider POST /magic-link/authentication POST /magic-link/file-upload ``` ## Endpoints | Action | Method | Path | | ------------------------------------------ | ------ | ----------------------------------------------- | | List provider links | GET | `/magic-link/provider` | | Get provider link | GET | `/magic-link/provider/{magicLinkId}` | | Create provider link (existing user) | POST | `/magic-link/users/{leafUserId}/provider` | | Create provider link (new user) | POST | `/magic-link/provider` | | Delete provider link | DELETE | `/magic-link/provider/{magicLinkId}` | | List authentication links | GET | `/magic-link/authentication` | | Get authentication link | GET | `/magic-link/authentication/{magicLinkId}` | | Create authentication link (existing user) | POST | `/magic-link/users/{leafUserId}/authentication` | | Create authentication link (new user) | POST | `/magic-link/authentication` | | Delete authentication link | DELETE | `/magic-link/authentication/{magicLinkId}` | | List file upload links | GET | `/magic-link/file-upload` | | Get file upload link | GET | `/magic-link/file-upload/{magicLinkId}` | | Create file upload link (existing user) | POST | `/magic-link/users/{leafUserId}/file-upload` | | Create file upload link (new user) | POST | `/magic-link/file-upload` | | Delete file upload link | DELETE | `/magic-link/file-upload/{magicLinkId}` | ## Customization All Magic Link creation endpoints accept a `settings` object for branding: | Setting | Type | Description | | ------------------- | ------- | ------------------------------------------------------ | | `backgroundColor` | String | Background color of the link page | | `companyLogo` | String | URL to your company logo | | `companyName` | String | Your company name | | `headerImage` | String | URL for a header image | | `showLeafUserName` | Boolean | Display the Leaf user name | | `disconnectEnabled` | Boolean | Allow users to disconnect providers from the link page | ## What to do next * [Leaf Link](/components/leaf-link) for embedding widgets directly in your application. * [Leaf Connect](/components/leaf-connect) for sharing data between API owners. * [Magic Link API Reference](/api-reference/magic-link) for full endpoint details. # Configurations Source: https://docs.withleaf.io/configuration/overview Control how Leaf syncs, processes, and outputs field boundaries, machine files, operations, images, and irrigation at the API owner and Leaf user level. Configurations control how Leaf pulls data from providers, processes machine files, creates field operations, and generates output formats. You can set configurations at the API owner level (applies to all Leaf users by default) or at the individual Leaf user level for granular control. ## How configuration inheritance works Every API owner starts with a default set of configurations. These defaults can be changed but not deleted or set to null. When you set a configuration on a specific Leaf user, that user stops inheriting that configuration from the API owner. The Leaf user's value takes precedence and does not change if you later update the API owner's configuration. If a Leaf user has no custom configuration, they inherit everything from the API owner. Configuration changes are not retroactive. Existing data is not reprocessed when you change a configuration. Use the Reprocess Operation endpoint if you need to apply new settings to existing data. ## Endpoints Base URL: `https://api.withleaf.io/services/config/api` | Action | Method | Path | | ------------------------------ | ------ | ----------------------- | | Get API owner configuration | GET | `/configs` | | Get Leaf user configuration | GET | `/configs/{leafUserId}` | | Create Leaf user configuration | POST | `/configs/{leafUserId}` | | Update API owner configuration | PATCH | `/configs` | | Update Leaf user configuration | PATCH | `/configs/{leafUserId}` | | Delete Leaf user configuration | DELETE | `/configs/{leafUserId}` | ## Configuration categories Configurations are grouped by what they control. See the [Configuration Reference](/configuration/reference) for the full list with defaults and descriptions. | Category | What it controls | Key configs | | ------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Data synchronization | Which data Leaf pulls from providers | `fieldsAutoSync`, `operationsAutoSync`, `customDataSync`, `organizationDataSync` | | Field boundary management | How boundaries are validated, linked, and merged | `automaticFixBoundary`, `fieldsAttachIntersection`, `fieldsAutoMerge` | | Machine file conversion | How raw data is cleaned and output | `cleanupStandardGeojson`, `cleanupRules`, `unitMeasurement`, `enableGeoparquetOutput`, `cropOptional`, `seedRateOptional` | | Field operations | How files merge into operations | `fieldOperationCreation`, `operationsFilteredGeojson`, `operationsRemoveOutliers`, `operationsMergeRange`, `splitOperationsByField`, `outOfStandardOperations` | | Image generation | Which images are produced for operations | `operationsImageCreation`, `operationsImageAsGeoTiff` | | Irrigation | How far back to fetch irrigation data | `irrigationProcessingRange` | ## Configurations by use case The right configuration depends on your use case. Here are recommended starting points. ### Crop insurance Focus on consistent results close to SMS-driven pipelines. Key settings: * `cleanupStandardGeojson`: `true` with default rules. * `operationsRemoveOutliers`: `true` with `operationsOutliersLimit` at `3`. * `splitOperationsByField`: `true`. * `operationsImageCreation`: `true` for visual verification. * `unitMeasurement`: `IMPERIAL` (for US customers). ### FMIS Prioritize processing efficiency and pull only the data you need: * `customDataSync`: `true` to selectively process fields. * `fieldsAutoSync`: `true`. * `operationsAutoSync`: `true`. * `enableGeoparquetOutput`: `true` for faster data ingestion. ### Managed service provider Similar to FMIS, with additional attention to multi-organization accounts: * `organizationDataSync`: `SELECTED_ONLY` to limit scope. * `customDataSync`: `true`. * Per-Leaf-user configurations for different growers with different processing needs. ## What to do next * [Configuration Reference](/configuration/reference) for every config option with defaults and descriptions. * [Configuration API Reference](/api-reference/configurations) for endpoint details. * [Machine Data Overview](/machine-data/overview) for how configurations affect file processing. * [Field Operations](/machine-data/field-operations) for how merge ranges and split settings shape operations. # Configuration Reference Source: https://docs.withleaf.io/configuration/reference Reference for every Leaf configuration option with defaults, allowed values, and behavior for data sync, boundaries, file conversion, operations, and images. Every configuration option available at the API owner or Leaf user level. For how inheritance works and use-case recommendations, see [Configuration Overview](/configuration/overview). ## Quick reference **[Data synchronization](#data-synchronization):** [`fieldsAutoSync`](#fieldsautosync), [`operationsAutoSync`](#operationsautosync), [`customDataSync`](#customdatasync), [`organizationDataSync`](#organizationdatasync), [`syncPartnerData`](#syncpartnerdata), [`machinesAutoSync`](#machinesautosync), [`implementsAutoSync`](#implementsautosync), [`operatorsAutoSync`](#operatorsautosync), [`productsAutoSync`](#productsautosync), [`zonesAutoSync`](#zonesautosync) **[Field boundary management](#field-boundary-management):** [`automaticFixBoundary`](#automaticfixboundary), [`fieldsAttachIntersection`](#fieldsattachintersection), [`fieldsAutoMerge`](#fieldsautomerge), [`fieldsMergeIntersection`](#fieldsmergeintersection) **[Machine file conversion](#machine-file-conversion):** [`cleanupStandardGeojson`](#cleanupstandardgeojson), [`cleanupRules`](#cleanuprules), [`originalOperationData`](#originaloperationdata), [`unitMeasurement`](#unitmeasurement), [`enableOutsideFieldGeojson`](#enableoutsidefieldgeojson), [`enableGeojsonOutput`](#enablegeojsonoutput), [`enableGeoparquetOutput`](#enablegeoparquetoutput), [`enablePolygonOutput`](#enablepolygonoutput), [`cropOptional`](#cropoptional), [`seedRateOptional`](#seedrateoptional) **[Field operations](#field-operations):** [`fieldOperationCreation`](#fieldoperationcreation), [`operationsFilteredGeojson`](#operationsfilteredgeojson), [`operationsRemoveOutliers`](#operationsremoveoutliers), [`operationsOutliersLimit`](#operationsoutlierslimit), [`operationsMergeRange`](#operationsmergerange), [`operationsMergeRangeHarvested`](#operationsmergerangeharvested), [`operationsProcessingRange`](#operationsprocessingrange), [`splitOperationsByField`](#splitoperationsbyfield), [`splitOperationsByProvider`](#splitoperationsbyprovider), [`splitOperationsByTillType`](#splitoperationsbytilltype), [`summarizeByProductEntry`](#summarizebyproductentry), [`outOfStandardOperations`](#outofstandardoperations), [`enableOperationsSession`](#enableoperationssession) **[Image generation](#field-operations-image-generation):** [`operationsImageCreation`](#operationsimagecreation), [`operationsImageAsGeoTiff`](#operationsimageAsgeotiff), [`operationsImageAttributeCreation`](#operationsimageattributecreation) **[Irrigation](#irrigation):** [`irrigationProcessingRange`](#irrigationprocessingrange) *** ## Data synchronization These configurations control what data Leaf pulls from connected providers. ### fieldsAutoSync **Default:** `true` Leaf automatically syncs field boundaries from connected providers. Set to `false` if you want to trigger syncs manually via the Manual Sync endpoint. ### operationsAutoSync **Default:** `true` Leaf automatically syncs operation data from connected providers. ### customDataSync **Default:** `true` When enabled, Leaf initially fetches field boundaries in `PREVIEW` mode instead of downloading full data for every provider field. You can then selectively enable specific fields for full processing. Useful for controlling costs when connecting accounts with thousands of fields. ### organizationDataSync **Default:** `ALL` Controls which organizations Leaf syncs data from when a provider account contains multiple organizations (common with John Deere). Set to `SELECTED_ONLY` to sync only organizations you explicitly mark as selected. ### syncPartnerData **Default:** `false` When `true`, Leaf fetches shared or partner data (e.g., John Deere Operations Center partnerships or AgLeader shared accounts) if the necessary permissions are granted. When `false`, only directly connected account data is fetched. ### machinesAutoSync **Default:** `false` Automatically sync machine data from connected providers. ### implementsAutoSync **Default:** `false` Automatically sync implement data from connected providers. ### operatorsAutoSync **Default:** `false` Automatically sync operator data from connected providers. ### productsAutoSync **Default:** `false` Automatically fetch input/product data (currently John Deere only). ### zonesAutoSync **Default:** `false` Automatically sync zone data from connected providers. ## Field boundary management ### automaticFixBoundary **Default:** `true` Leaf attempts to correct invalid field boundary geometries received from providers. ### fieldsAttachIntersection **Default:** `0.01` Minimum intersection percentage (0 to 100) required to link a field operation to a field. `0.01` is the smallest possible overlap. ### fieldsAutoMerge **Default:** `false` Automatically merge fields that meet the intersection threshold defined by `fieldsMergeIntersection`. ### fieldsMergeIntersection **Default:** `0.01` Minimum intersection percentage (0 to 100) for two fields to be merged. When met and `fieldsAutoMerge` is enabled, Leaf creates a new `MERGED` field. Original fields remain inactive for reference. ## Machine file conversion ### cleanupStandardGeojson **Default:** `true` Remove points marked as invalid from the `standardGeoJSON` output. Validity is determined by the `cleanupRules` configuration. ### cleanupRules **Default:** See below. Rules that determine which data points are valid. Points failing any applicable rule are removed from `standardGeoJSON` when `cleanupStandardGeojson` is enabled. Default rules: ```json theme={null} { "cleanupRules": { "harvestMoisture": [{"operator": "GT", "value": 0.0}, {"operator": "LT", "value": 100.0}], "tillageDepthActual": [{"operator": "GTE", "value": 0.0}], "recordingStatus": [{"operator": "EQ", "value": "On"}], "appliedRate": [{"operator": "GT", "value": 0.0}], "wetVolume": [{"operator": "GT", "value": 0.0}], "wetVolumePerArea": [{"operator": "GT", "value": 0.0}], "wetMass": [{"operator": "GT", "value": 0.0}], "seedRate": [{"operator": "GT", "value": 0.0}], "wetMassPerArea": [{"operator": "GT", "value": 0.0}], "crop": [{"operator": "NE", "value": "unknown"}], "products": [{"operator": "GTE", "value": 0.0}] } } ``` Available operators: `GT` (greater than), `GTE` (greater than or equal), `LT` (less than), `LTE` (less than or equal), `EQ` (equal), `NE` (not equal). Setting custom `cleanupRules` replaces the entire default rule set. It does not merge with defaults. If you only specify a rule for `recordingStatus`, all other default rules stop applying. Include every rule you need. ### originalOperationData **Default:** `true` Include non-standard properties (original farm name, field name, grower, operation type) in file summary output. ### unitMeasurement **Default:** varies by account. Choose `METRIC`, `IMPERIAL`, or `DEFAULT` for summary, `standardGeoJSON`, and `filteredGeoJSON` outputs. `DEFAULT` uses the units from the original data source. ### enableOutsideFieldGeojson **Default:** `false` Capture machine file points that fall outside field boundaries when `splitOperationsByField` is active. Retrieve these points via the outsideFieldGeoJSON endpoint. ### enableGeojsonOutput **Default:** `true` Generate vector point outputs in GeoJSON format. Enabled by default. Disable to skip GeoJSON generation, for example when you only need GeoParquet output (`enableGeoparquetOutput`). ### enableGeoparquetOutput **Default:** `false` Generate vector point outputs in GeoParquet format in addition to GeoJSON. Faster processing and smaller files. ### enablePolygonOutput **Default:** `false` Output data in polygon format (in addition to point format). Polygon output is provided in GeoParquet only. ### cropOptional **Default:** `required` Make crop an optional property instead of required. Leaf passes through the original value without validation. ### seedRateOptional **Default:** `required` Make seedRate an optional property instead of required. ## Field operations ### fieldOperationCreation **Default:** `true` Allow Leaf to automatically create field operations by merging machine files that intersect with active field boundaries. ### operationsFilteredGeojson **Default:** `true` Generate filtered GeoJSON output for field operations. ### operationsRemoveOutliers **Default:** `true` Remove outlier points from harvest `filteredGeojson` based on the standard deviation threshold set by `operationsOutliersLimit`. Requires `operationsFilteredGeojson` to be enabled. ### operationsOutliersLimit **Default:** `3` Standard deviations from the mean for identifying harvest volume outliers. ### operationsMergeRange **Default:** `5` days Time window for grouping machine files into a single non-harvest field operation. Files within this range for the same field, crop, and operation type are merged. ### operationsMergeRangeHarvested **Default:** `21` days Time window for grouping machine files into a single harvest field operation. ### operationsProcessingRange **Default:** typically 12 months. Lookback period (in months) for fetching and processing operations data from providers. ### splitOperationsByField **Default:** `true` Create separate field operations for each field boundary that intersects with the machine data. ### splitOperationsByProvider **Default:** `true` Group machine files by provider when creating field operations. When `false`, data from different providers can merge into the same operation. ### splitOperationsByTillType **Default:** `false` Create separate field operations for each unique `tillType` in tillage data. ### summarizeByProductEntry **Default:** `true` Aggregate product application data in field operation summaries by product name, summing area and totalApplied, and averaging rate. ### outOfStandardOperations **Default:** `false` Allow processing of operations that don't meet standard validation criteria. These operations are marked as non-standard. ### enableOperationsSession **Default:** `false` Enable a session view of field operation data, grouped by operator, implement, and machines. ## Field operations image generation ### operationsImageCreation **Default:** `false` Generate images for field operations. ### operationsImageAsGeoTiff **Default:** `false` Generate field operation images in GeoTIFF format. ### operationsImageAttributeCreation Control which image attributes are generated per operation type. Set individual attributes to `true` to generate their images. Available attributes vary by operation type (harvested, planted, applied, tillage) and include properties like `wetMassPerArea`, `seedRate`, `appliedRate`, `elevation`, `speed`, and others. ## Irrigation ### irrigationProcessingRange **Default:** `12` months Lookback period (in months) for fetching and processing irrigation activities from providers. # Growers Source: https://docs.withleaf.io/fields/growers How Leaf manages growers in the Grower/Farm/Field hierarchy, including provider sync from John Deere, manual creation, and the grower-farm-field relationship. Growers sit at the top of Leaf's field hierarchy. They represent the data owner, typically a farmer or farm operation. Growers are synced from connected providers or created manually. ## How growers map to providers Each provider has its own concept of the data owner: * **John Deere** — Growers correspond to John Deere "Clients." The `name` property comes directly from the Client name. * **Other providers** — The mapping varies. Some providers have a direct grower equivalent; others do not. When Leaf syncs from a provider, it creates grower records with the provider's identifiers attached (`providerName`, `providerGrowerId`). ## Grower resource ```json theme={null} { "id": 873300016, "name": "Smith Farms", "leafUserId": "1d3ecb0f-bf3d-42db-aae6-8c45c045d28c", "providerName": "JohnDeere", "providerId": 2, "providerGrowerId": "1Grower", "farmIds": [1538766, 1538767], "createdTime": "2023-06-06T03:31:39.966630Z", "updatedTime": "2023-06-07T20:01:14.814346Z" } ``` A grower links to one Leaf user and contains a list of farm IDs. Farms in turn contain field IDs. The `name` property is present only when a name is available from the provider or was set manually. ## Creating a grower `POST /services/fields/api/users/{leafUserId}/growers` ```json theme={null} { "name": "Smith Farms" } ``` Only the `name` field is accepted. Leaf assigns the grower an auto-generated integer ID. ## Updating a grower `PUT /services/fields/api/users/{leafUserId}/growers/{id}` You can update the `name` field. ## Listing growers `GET /services/fields/api/growers` Returns a paged list of all growers. Filter by `provider`, `leafUserId`, or use `page` and `size` for pagination. ## Enabling preview fields by grower When `customDataSync` is enabled, you can activate all preview fields under one or more growers at once: `POST /services/fields/api/growers/enableSync` ```json theme={null} { "growerIds": [873300016, 873300017] } ``` This removes all of those growers' fields from `PREVIEW` mode and queues them for the next full sync. It's faster than enabling fields individually when you want to onboard an entire grower. ## Farms Farms group fields under a grower. They work the same way as growers: synced from providers or created manually. `POST /services/fields/api/users/{leafUserId}/farms` creates a farm. You can pass `name` and optionally `growerId` to link it to a grower. `GET /services/fields/api/farms` lists all farms, filterable by `growerId`, `provider`, and `leafUserId`. ## What to do next * [Fields Overview](/fields/overview) — The full Grower/Farm/Field hierarchy. * [Managing Fields](/fields/managing-fields) — Working with fields and boundaries. * [API Reference: Growers](/api-reference/growers) — Full endpoint reference for grower and farm operations. # Managing Fields Source: https://docs.withleaf.io/fields/managing-fields List, create, update, delete, and sync fields through the Leaf API, including boundaries, GeoJSON, provider sync, preview mode, and intersection queries. Fields are the core spatial entity in Leaf. You can create them manually with a boundary polygon, sync them from connected providers, or both. This page covers listing, reading, and mutating fields and their boundaries. ## Listing fields `GET /services/fields/api/fields` Returns a paginated list of fields across all Leaf users. Filter by `leafUserId`, `status`, `type`, `provider`, `farmId`, and more. The default page size is 20. ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/fields/api/fields?leafUserId=UUID&status=PROCESSED&page=0&size=10' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/fields/api/fields' headers = {'Authorization': f'Bearer {TOKEN}'} params = {'leafUserId': 'UUID', 'status': 'PROCESSED', 'page': 0, 'size': 10} response = requests.get(endpoint, headers=headers, params=params) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/fields' const headers = { Authorization: `Bearer ${TOKEN}` } const params = { leafUserId: 'UUID', status: 'PROCESSED', page: 0, size: 10 } axios.get(endpoint, { headers, params }) .then(res => console.log(res.data)) .catch(console.error) ``` See [API Reference: Fields](/api-reference/fields#get-all-fields) for the full parameter table including time-range filters, operation-linked filters, and sort options. ## Getting a field `GET /services/fields/api/users/{leafUserId}/fields/{id}` Returns a single field by its ID for the specified Leaf user. See [API Reference: Fields](/api-reference/fields#get-a-field) for details. ## Creating a field To create a field manually, POST a GeoJSON `MultiPolygon` geometry to the fields endpoint. Leaf creates both the field and its first active boundary from the geometry you provide. `POST /services/fields/api/users/{leafUserId}/fields` ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "North Quarter", "geometry": { "type": "MultiPolygon", "coordinates": [[[[ [-93.4880, 41.7710], [-93.4800, 41.7710], [-93.4800, 41.7600], [-93.4880, 41.7600], [-93.4880, 41.7710] ]]]] } }' \ 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields' headers = {'Authorization': f'Bearer {TOKEN}'} data = { 'name': 'North Quarter', 'geometry': { 'type': 'MultiPolygon', 'coordinates': [[[[ [-93.4880, 41.7710], [-93.4800, 41.7710], [-93.4800, 41.7600], [-93.4880, 41.7600], [-93.4880, 41.7710] ]]]] } } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/fields/api/users/{leafUserId}/fields' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { name: 'North Quarter', geometry: { type: 'MultiPolygon', coordinates: [[[[ [-93.4880, 41.7710], [-93.4800, 41.7710], [-93.4800, 41.7600], [-93.4880, 41.7600], [-93.4880, 41.7710] ]]]] } } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` You can optionally set `id` and `name` on the request body. If you omit `id`, Leaf generates a UUID. The `id` cannot be changed after creation. ## Updating a field `PATCH /services/fields/api/users/{leafUserId}/fields/{id}` You can update `name`, `farmId`, and `geometry` on manually created fields. Provider-created fields must be updated through the provider platform. If you update the geometry, Leaf creates a new active boundary and sets the old one to inactive. ## Getting boundaries Every field can have multiple boundaries, but only one is active at a time. Leaf keeps a full history of boundary changes, so the list endpoint returns both active and inactive records. `GET /services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries` Returns all boundaries for a field, including inactive and historical records. Each boundary has a `status` (`ACTIVE`, `INACTIVE`, `OUTDATED_ON_PROVIDER`, or `DELETED_ON_PROVIDER`) that indicates its lifecycle state. `GET /services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary` Returns only the currently active boundary for a field. `GET /services/fields/api/users/{leafUserId}/fields/{fieldId}/boundaries/{boundaryId}` Returns a specific boundary by its ID. See [API Reference: Fields](/api-reference/fields#get-all-boundaries) for response details. ## Getting operation files for a field `GET /services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files` Returns operation files (harvested, planted, applied, tillage) associated with this specific field. These are the same operations available through the [Operations endpoints](/machine-data/field-operations), scoped to the field's boundary. You can filter by `operationType`, `provider`, `crop`, `variety`, and time range. `GET /services/fields/api/users/{leafUserId}/fields/{fieldId}/operations/files/{fileId}` Returns a single operation file for the field. See [API Reference: Fields](/api-reference/fields#get-all-operation-files-of-a-field) for the full parameter list. ## Updating the active boundary `PUT /services/fields/api/users/{leafUserId}/fields/{fieldId}/boundary` This replaces the active boundary with a new geometry. The previous active boundary is preserved as an inactive historical record. ## Syncing fields from providers When `fieldsAutoSync` is enabled, Leaf pulls field boundaries from all connected providers automatically. To trigger a manual sync: `POST /services/fields/api/users/{leafUserId}/fields/sync` This schedules a sync for the Leaf user. New fields from the provider appear after the sync completes. Changed boundaries on the provider side produce new boundary records in Leaf. ## Preview mode When `customDataSync` is enabled, newly synced fields arrive in `PREVIEW` status. Preview fields have metadata (name, provider info) but no boundary geometry. To fully activate a field: `POST /services/fields/api/users/{leafUserId}/fields/{id}/enableSync` The field status changes to `WAITING`, and its boundary is fetched in the next sync window. After processing, the status becomes `PROCESSED`. You can activate all fields under a grower at once using `POST /growers/enableSync` with a list of grower IDs. ## Finding fields by geometry `POST /services/fields/api/users/{leafUserId}/fields/intersects` Send a `MultiPolygon` geometry in the request body and Leaf returns all fields that overlap it. The `intersectionThreshold` parameter (default `0.01`, range 0.01–100) controls the minimum overlap percentage required. Leaf checks both "intersection by field" and "intersection by geometry" ratios, and returns the field if either exceeds the threshold. ## Deleting a field `DELETE /services/fields/api/users/{leafUserId}/fields/{fieldId}` Only manually created fields can be deleted. Provider-created fields cannot be deleted from the Leaf side. ## Uploading fields to a provider `POST /services/fields/api/users/{leafUserId}/fields/{fieldId}/integration/{providerName}` You can push a Leaf field boundary to John Deere or Climate FieldView. The field must already exist in Leaf. Leaf prevents sending a field back to the provider it came from to avoid recursive syncs. For John Deere, add the `organizationId` query parameter. ## What to do next * [Fields Overview](/fields/overview) — The Grower/Farm/Field hierarchy and boundary model. * [Uploading Boundaries](/fields/uploading-boundaries) — Create fields in bulk from shapefiles, GeoJSON, or KML. * [Growers](/fields/growers) — Manage the grower layer of the hierarchy. * [API Reference: Fields](/api-reference/fields) — Full endpoint reference with all parameters, query filters, pagination options, and response schemas. # Fields Overview Source: https://docs.withleaf.io/fields/overview How Leaf organizes field data using the Grower/Farm/Field hierarchy, how boundaries work, and how fields sync from John Deere, Climate FieldView, and CNHi. Leaf organizes agricultural land data into a three-level hierarchy: Grower → Farm → Field. Fields carry the boundary polygons that drive everything else in Leaf, from field operations to satellite imagery. ## The Grower / Farm / Field hierarchy Leaf uses a Grower/Farm/Field structure borrowed from how providers like John Deere organize data: * **Grower** — Represents the data owner. In Leaf, a grower maps to a Leaf user (the entity that holds provider credentials). When you connect a John Deere account, Leaf imports the "Client" entities as growers. * **Farm** — A grouping of fields under a grower. Farms are optional in Leaf. You can create fields without assigning them to a farm. * **Field** — A single parcel of land, identified by a field ID. Each field has one active boundary at any time. Machine files that Leaf merges into field operations are tied to fields through their boundaries. Growers and farms are primarily organizational. The field boundary is where the real work happens: it defines the geographic extent used for clipping operations, triggering satellite imagery, and calculating area. ## How boundaries work Every field can have zero or more boundaries, but only one is active at a time. The active boundary is the one Leaf uses for spatial operations. When you update a boundary, the previous one is not deleted. It becomes inactive, and Leaf keeps a full history of all boundaries ever associated with a field. Boundaries are GeoJSON `MultiPolygon` geometries. Each boundary has a `status` that tracks its lifecycle: | Status | Meaning | | ---------------------- | ---------------------------------------------------------------------------------- | | `ACTIVE` | Current boundary in use | | `INACTIVE` | Replaced by a newer boundary | | `OUTDATED_ON_PROVIDER` | Was edited on the provider side; Leaf created a new boundary to reflect the change | | `DELETED_ON_PROVIDER` | Was deleted on the provider; Leaf keeps the historical record | Leaf validates boundary geometry on creation. If a provider sends an invalid geometry (self-intersections, too few points, etc.), the boundary is stored with a `validity` flag indicating the issue. You can enable `automaticFixBoundary` in your configuration to have Leaf attempt automatic geometry repair. ## How fields sync from providers When you connect a provider to a Leaf user, Leaf automatically pulls the grower, farm, and field structure from that provider. This happens on the first sync and at least every 24 hours afterward. Each synced field carries metadata from the provider: `providerName`, `providerFieldId`, `providerFieldName`, and `organizationId`. If the same physical field exists in multiple providers, Leaf detects the overlap and creates a merged field that links back to the originals. Field types: * `ORIGINAL` — A field that came from a single provider or was manually created. * `MERGED` — A field Leaf created by detecting overlap between two or more original fields. If `fieldsAutoSync` is enabled (the default), syncing happens automatically. You can disable it and trigger syncs manually via `POST /users/{leafUserId}/fields/sync`. If `customDataSync` is enabled, Leaf initially fetches fields in `PREVIEW` mode: metadata only, no boundary geometry. You then selectively enable fields for full processing using the enable sync endpoint. ## Common use cases * **Import field boundaries from a provider**: Connect John Deere Operations Center, Climate FieldView, or CNHi and let Leaf auto-sync field boundaries for each grower. * **Create field boundaries manually**: Upload shapefiles, GeoJSON, or KML through the field upload service when growers don't use a cloud provider. * **Detect field overlaps**: Use the intersection endpoint to find which fields overlap a given geometry — useful for matching uploaded data to existing fields. * **Control sync scope**: Enable `customDataSync` to preview fields before committing to full boundary processing, keeping billing predictable. ## What to do next * [Managing Fields](/fields/managing-fields) — Create, update, delete, and sync fields. * [Uploading Boundaries](/fields/uploading-boundaries) — Create field boundaries from shapefiles, GeoJSON, or KML. * [Growers](/fields/growers) — Manage the grower layer of the hierarchy. * [API Reference: Fields](/api-reference/fields) — Full endpoint reference for fields, boundaries, and farms. # Uploading Boundaries Source: https://docs.withleaf.io/fields/uploading-boundaries Upload field boundaries in bulk using shapefiles, GeoJSON, or KML/KMZ files. Covers supported formats, file requirements, and tracking upload status. The field upload service creates field boundaries from spatial files. Instead of creating fields one at a time via the API, you can upload a zip file containing shapefiles, GeoJSON, or KML and Leaf creates all the fields at once. ## Supported formats Upload a `.zip` file containing one or more of: * **Shapefile** — Must include at least `.shp`, `.dbf`, and `.shx` files. * **GeoJSON** — Standard GeoJSON with Polygon or MultiPolygon geometries. * **KML/KMZ** — Google Earth format. The zip can contain multiple files in different formats. Leaf detects and processes each valid file independently. Nested directory structures inside the zip are fine. If a file includes a property or column called `name`, Leaf uses it as the field name. All geometries are projected to WGS 84 (EPSG:4326). ## Limits * Maximum file size: 3 GB * Maximum fields per upload: 100 ## Uploading a file `POST /services/uploadservice/api/upload?leafUserId={leafUserId}` Send the file as `multipart/form-data`. The `leafUserId` parameter is required. You can optionally pass `farmId` to assign all created fields to a specific farm. ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -F 'file=@boundaries.zip' \ 'https://api.withleaf.io/services/uploadservice/api/upload?leafUserId={leafUserId}' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/uploadservice/api/upload?leafUserId={leafUserId}' headers = {'Authorization': f'Bearer {TOKEN}'} files = {'file': open('boundaries.zip', 'rb')} response = requests.post(endpoint, headers=headers, files=files) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const FormData = require('form-data') const fs = require('fs') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/uploadservice/api/upload?leafUserId={leafUserId}' const form = new FormData() form.append('file', fs.createReadStream('boundaries.zip')) axios.post(endpoint, form, { headers: { Authorization: `Bearer ${TOKEN}`, ...form.getHeaders(), }, }) .then(res => console.log(res.data)) .catch(console.error) ``` The response includes an upload `id` you use to track progress. ## Tracking upload status `GET /services/uploadservice/api/upload/{uploadId}` | Status | Meaning | | ----------- | ------------------------------------------------------------ | | `RECEIVED` | Upload accepted, processing not started | | `PROCESSED` | All files processed, at least one field created successfully | | `FAILED` | No fields were created | ## Getting created fields `GET /services/uploadservice/api/upload/{uploadId}/entries` Returns the processing result for each file in the zip. Each entry includes: * `fieldId` — Array of field IDs created from that file. * `converterFormat` — Detected format: `SHAPEFILE`, `GEOJSON`, or `KML`. * `status` — Per-file status: `PROCESSING`, `CONVERTED`, `FINISHED`, `FAILED`, or `PARTIALLY_FINISHED`. * `createFieldErrorDetails` — Error messages for any fields that failed to create (e.g., self-intersecting geometry). Use the field IDs from the entries response to fetch full field details via `GET /services/fields/api/users/{leafUserId}/fields/{fieldId}`. ## What to do next * [Managing Fields](/fields/managing-fields) — Create and update individual fields via the API. * [Fields Overview](/fields/overview) — How boundaries fit into the Grower/Farm/Field model. * [API Reference: Field Upload](/api-reference/field-upload) — Full endpoint reference for the upload service. # Authentication Source: https://docs.withleaf.io/getting-started/authentication Authenticate with the Leaf API using JWT tokens. Get a token, understand expiration and renewal, and see example requests in cURL, Python, and JavaScript. Leaf uses JWT (JSON Web Token) authentication. You send your email and password to the authenticate endpoint, get back a token, and include that token as a Bearer header on every subsequent request. ## Get a token Send a `POST` request to the authenticate endpoint: ``` https://api.withleaf.io/api/authenticate ``` ```bash cURL theme={null} curl -X POST \ -H 'Content-Type: application/json' \ -d '{"username":"your-email@example.com","password":"your-password","rememberMe":"true"}' \ 'https://api.withleaf.io/api/authenticate' ``` ```python Python theme={null} import requests response = requests.post( "https://api.withleaf.io/api/authenticate", headers={"Content-Type": "application/json"}, json={ "username": "your-email@example.com", "password": "your-password", "rememberMe": "true" } ) token = response.json()["id_token"] ``` ```javascript JavaScript theme={null} const axios = require("axios"); axios.post("https://api.withleaf.io/api/authenticate", { username: "your-email@example.com", password: "your-password", rememberMe: "true", }) .then(({ data }) => { const token = data.id_token; console.log(token); }) .catch(console.error); ``` The response: ```json theme={null} { "id_token": "eyJhbGciOi..." } ``` ## Token lifecycle The `rememberMe` field controls how long your token lasts: | `rememberMe` | Token duration | | ------------ | -------------- | | `"true"` | 30 days | | `"false"` | 24 hours | When a token expires, request a new one from the same endpoint. There is no refresh token flow; you re-authenticate with credentials. ## Using the token Include the token in the `Authorization` header of every API request: ```bash cURL theme={null} curl -H 'Authorization: Bearer eyJhbGciOi...' \ 'https://api.withleaf.io/services/usermanagement/api/users' ``` ```python Python theme={null} response = requests.get( "https://api.withleaf.io/services/usermanagement/api/users", headers={"Authorization": f"Bearer {token}"} ) ``` ```javascript JavaScript theme={null} axios.get("https://api.withleaf.io/services/usermanagement/api/users", { headers: { Authorization: `Bearer ${token}` }, }) .then((response) => console.log(response.data)) .catch(console.error); ``` If the token is missing, expired, or invalid, the API returns a `401 Unauthorized` response. ## Multiple environments Leaf does not provide separate test and production environments. Instead, create distinct API owner accounts for each: * `leaf-test@yourcompany.com` for development and testing * `leaf-prod@yourcompany.com` for production Each API owner has its own token, Leaf users, configurations, and billing. This keeps test data isolated from production. Your contract may include a testing acre allotment. Make sure all test-related API calls use your test API owner account so testing usage is tracked separately. ## What to do next * [Authentication API Reference](/api-reference/authentication): Endpoint details, request/response shapes, and error codes. * [Quickstart](/getting-started/quickstart): Use your token to create a Leaf user and start pulling data. * [Core Concepts](/getting-started/core-concepts): Understand the data pipeline before building. # Core Concepts Source: https://docs.withleaf.io/getting-started/core-concepts How data flows from providers through machine file conversion into standardized field operations, and the key terminology used throughout the Leaf API. This page covers the mental model for how Leaf works. If you understand the pipeline and the terms on this page, the rest of the docs will make sense. ## The data pipeline Data moves through Leaf in a fixed sequence: **Providers → Machine files → Conversion → Field operations** 1. **Data enters Leaf.** Either through a provider connection (John Deere, Climate FieldView, CNHi, CNHI FieldOps, Trimble, AgLeader) or through direct file upload. Provider connections sync automatically; uploads are processed on receipt. 2. **Machine file conversion.** Leaf takes proprietary machine file formats (ISOXML, GEN4, .DAT, etc.) and converts them into a standard canonical format. The converted data is available as either GeoJSON or GeoParquet — same structure, your choice of format. Conversion happens in stages: * **Standard GeoJSON**: Cleaned and standardized into Leaf's common schema. Property names, units, and structure are consistent regardless of source. * **Filtered GeoJSON** (optional): Invalid data points removed based on configurable rules. Enabled via configuration. 3. **Field operation creation.** Leaf takes the converted machine files and merges them with field boundaries to produce field operations. A single planting or harvest event might be spread across dozens of machine files from different swath passes. Leaf identifies which files belong together, merges them, and clips them to field boundaries. This produces field operations for planting, harvest, application, and tillage activities. 4. **Summaries and images.** For each machine file and field operation, Leaf generates a summary (averages, min/max, standard deviations for key properties) and optionally produces property map images. Field operations require field boundaries. Without a boundary, Leaf still converts machine files and produces file summaries, but cannot create field operations. Boundaries can come from a provider or be created manually through the API. ## Key terminology | Term | Definition | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **API owner** | Your Leaf account, identified by an email address. Owns all Leaf users beneath it. Use separate API owners for test vs. production. | | **Leaf user** | Represents a grower (or region, sub-customer, etc.) under your API owner. Holds provider credentials, fields, machine files, and field operations. Data is isolated per Leaf user. | | **Provider credentials** | OAuth2 tokens or API keys attached to a Leaf user that authorize Leaf to pull data from a specific provider (e.g., John Deere, Climate FieldView). | | **Machine file** | A raw data file from a provider or direct upload. Contains GPS-tagged point data from field equipment. Leaf converts these into a standard canonical format, accessible as GeoJSON or GeoParquet. | | **Field operation** | The merged, boundary-clipped output from one or more machine files. Represents a single activity (planting, harvest, application, tillage) on a specific field. | | **Field boundary** | The geographic polygon defining a field. One active boundary per field. Required for creating field operations. | | **Configurations** | Settings that control data ingestion, processing, and output. Applied at the API owner level (inherited by all Leaf users) or overridden per Leaf user. | | **Alerts** | Webhook notifications triggered by events like new field operations, credential expiration, or boundary changes. | | **Custom data sync** | A configuration (`customDataSync`) that limits Leaf to fetching field metadata only. You then select which fields to fully process. Useful for controlling costs and scope. | | **Magic Link** | A hosted web widget you can send to growers. They click it and connect their provider account or upload files, without needing to interact with the API directly. | ## Account structure The hierarchy is: **API owner → Leaf users → Fields, Machine files, Field operations.** Most implementations use one API owner per environment, with one Leaf user per grower. A Leaf user can be connected to multiple providers but only one credential set per provider. Configurations cascade: settings on the API owner apply to all Leaf users unless a Leaf user has its own override. Configuration changes are not retroactive. ## What to do next * [Quickstart](/getting-started/quickstart): Walk through the setup end to end. * [Authentication](/getting-started/authentication): Token lifecycle and request examples. * [Configuration](/configuration/overview): Control how Leaf processes data. # Quickstart Source: https://docs.withleaf.io/getting-started/quickstart Get data flowing through the Leaf API in 15 minutes. Create an account, authenticate, connect a provider or upload a file, and retrieve field operations. This guide takes you from zero to retrieving processed agricultural data. You'll create an account, get a token, create a Leaf user, and connect a data source. ## Before you begin You need: 1. **A Leaf account.** [Contact Sales](https://withleaf.io/account/get-a-demo) or your Customer Success representative to register an account. 2. **Provider API credentials** (if connecting to a provider). Complete the provider's developer/partner agreement and obtain your client ID and secret. Leaf can help with introductions if needed. 3. **An HTTP client.** cURL works, or use the [Leaf Postman Collection](https://github.com/Leaf-Agriculture/Leaf-API-Postman-Collection). Leaf does not have a separate test environment. Create distinct API owner accounts for testing and production, e.g. `leaf-test@yourcompany.com` and `leaf-prod@yourcompany.com`. ## Step 1: Get your token After registering, authenticate to get a JWT token. This token goes in the `Authorization: Bearer ` header of every API request. ```bash cURL theme={null} curl -X POST \ -H 'Content-Type: application/json' \ -d '{"username":"your-email@example.com","password":"your-password","rememberMe":"true"}' \ 'https://api.withleaf.io/api/authenticate' ``` ```python Python theme={null} import requests response = requests.post( "https://api.withleaf.io/api/authenticate", headers={"Content-Type": "application/json"}, json={ "username": "your-email@example.com", "password": "your-password", "rememberMe": "true" } ) token = response.json()["id_token"] ``` ```javascript JavaScript theme={null} const axios = require("axios"); axios.post("https://api.withleaf.io/api/authenticate", { username: "your-email@example.com", password: "your-password", rememberMe: "true", }) .then(({ data }) => { const token = data.id_token; console.log(token); }) .catch(console.error); ``` The response contains your token: ```json theme={null} { "id_token": "eyJhbGciOi..." } ``` With `rememberMe` set to `"true"`, the token lasts 30 days. Set it to `"false"` for a 24-hour token. See [Authentication](/getting-started/authentication) for full details. ## Step 2: Create a Leaf user A Leaf user typically represents a grower. Each Leaf user holds provider credentials, fields, and field operations. Create one with a `POST` request: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{"name":"Test Grower","email":"grower@example.com"}' \ 'https://api.withleaf.io/services/usermanagement/api/users' ``` ```python Python theme={null} response = requests.post( "https://api.withleaf.io/services/usermanagement/api/users", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json" }, json={"name": "Test Grower", "email": "grower@example.com"} ) leaf_user = response.json() leaf_user_id = leaf_user["id"] ``` ```javascript JavaScript theme={null} axios.post( "https://api.withleaf.io/services/usermanagement/api/users", { name: "Test Grower", email: "grower@example.com" }, { headers: { Authorization: `Bearer ${token}` } } ) .then((userResponse) => { const leafUserId = userResponse.data.id; console.log(leafUserId); }) .catch(console.error); ``` Save the returned `id`. You'll use it in every subsequent call for this grower. ## Step 3: Connect data You have two options for getting data into Leaf: **Option A: Connect a provider.** Attach provider credentials to the Leaf user so Leaf automatically syncs their data. Each provider has a specific credentials schema. See the [provider authentication docs](/providers/overview) for details. **Option B: Upload files directly.** If the grower has machine files on a USB drive or local storage, you can upload them through the API or use Leaf's Magic Link file upload widget. During development, avoid repeatedly connecting large provider accounts. This consumes your testing acre allotment. Use the `customDataSync` configuration to limit which fields Leaf processes. ## Step 4: Retrieve your data Once Leaf processes the connected or uploaded data, you can retrieve machine files and field operations. List machine files for the Leaf user: ```bash cURL theme={null} curl -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/files?leafUserId=LEAF_USER_ID' ``` ```python Python theme={null} files = requests.get( "https://api.withleaf.io/services/operations/api/files", headers={"Authorization": f"Bearer {token}"}, params={"leafUserId": leaf_user_id} ).json() ``` ```javascript JavaScript theme={null} axios.get("https://api.withleaf.io/services/operations/api/files", { headers: { Authorization: `Bearer ${token}` }, params: { leafUserId: leafUserId }, }) .then((files) => console.log(files.data)) .catch(console.error); ``` List field operations for the Leaf user: ```bash cURL theme={null} curl -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/operations/api/operations?leafUserId=LEAF_USER_ID' ``` ```python Python theme={null} operations = requests.get( "https://api.withleaf.io/services/operations/api/operations", headers={"Authorization": f"Bearer {token}"}, params={"leafUserId": leaf_user_id} ).json() ``` ```javascript JavaScript theme={null} axios.get("https://api.withleaf.io/services/operations/api/operations", { headers: { Authorization: `Bearer ${token}` }, params: { leafUserId: leafUserId }, }) .then((operations) => console.log(operations.data)) .catch(console.error); ``` Field operations require field boundaries. Without boundaries, Leaf converts machine files and produces file summaries, but cannot create operations. You can sync boundaries from a provider or create them manually via the API. If this worked, the machine files request returns a response object whose `operations` array contains the discovered machine files for the Leaf user. Once Leaf has both files and field boundaries, the operations request returns standardized operations for that grower. ## Step 5: Set up alerts Instead of polling the API, set up webhooks to get notified when new data is ready. At a minimum, subscribe to field events, field boundary events, machine file events, and field operation events. See the [Alerts documentation](/alerts/overview) for the full list of available events and setup instructions. ## What you built You now have a working Leaf integration: an authenticated API owner, a Leaf user representing a grower, a connected data source, and the ability to retrieve processed field operations. From here: * [Core Concepts](/getting-started/core-concepts): Understand how the data pipeline works. * [Provider Authentication](/providers/overview): Connect to John Deere, Climate FieldView, CNHi, CNHI FieldOps, and other providers. * [Machine Data](/machine-data/overview): Understand how Leaf processes machine files into field operations. * [Fields](/fields/overview): Manage field boundaries from providers or create your own. * [Configurations](/configuration/overview): Control how Leaf processes and syncs data. # Welcome to Leaf Source: https://docs.withleaf.io/getting-started/welcome Build with standardized agricultural data from John Deere, Climate FieldView, CNHi, CNHI FieldOps, Trimble, AgLeader, and other providers using Leaf. Leaf is an API for agricultural data. You connect to farm data providers like John Deere, Climate FieldView, CNHi, CNHI FieldOps, Trimble, and AgLeader, and Leaf gives you back clean, standardized data you can actually use. ## What Leaf does Farm machinery generates planting, harvest, application, and tillage data. That data lives in proprietary formats across different provider platforms, each with its own API, authentication flow, and data schema. Leaf handles all of it: pulling data from providers, converting proprietary machine file formats into a standard canonical schema, merging files into field operations, and mapping everything to field boundaries. You retrieve the processed data as GeoJSON or GeoParquet — same structure, your choice of format. You get a single REST API that returns the same data format regardless of which brand of equipment or software a grower uses. ## Who it's for Leaf is built for developers at agtech companies, crop insurance providers, farm management platforms, sustainability programs, and managed service providers. If your product needs grower field data from multiple sources, Leaf replaces the work of building and maintaining individual provider integrations. ## What you can build With Leaf, you can pull field boundaries, machine files, and field operations from any connected provider. You can also access satellite imagery (Sentinel and Planet), weather data (historical and forecast), and set up webhooks to react to new data as it arrives. Common use cases: * **Crop insurance**: Automate acreage reporting and production data collection at scale. * **Farm management (FMIS)**: Import field records, operation maps, and boundaries from all major brands into one platform. * **Sustainability programs**: Collect tillage, planting, and application data to populate carbon models or verify practices. * **Managed services**: Process data for multiple clients from a single integration. ## How data flows through Leaf At a high level: **Providers → Machine files → Conversion → Field operations → Your app.** Growers connect their provider accounts (or upload files directly). Leaf pulls machine files, converts them into a standard canonical format, and merges them with field boundaries to create field operations. The converted data is accessible as either GeoJSON or GeoParquet. You retrieve the processed data through the API or get notified via webhooks. For a deeper look at this pipeline, see [Core Concepts](/getting-started/core-concepts). ## What to do next * [Quickstart](/getting-started/quickstart): Get your first data flowing in about 15 minutes. * [Core Concepts](/getting-started/core-concepts): Understand the data pipeline and key terminology. * [Authentication](/getting-started/authentication): Learn how token auth works. # Crop Insurance Source: https://docs.withleaf.io/guides/by-use-case/crop-insurance Configure Leaf for crop insurance: acreage reporting, production data validation, and claims workflows using planting and harvest field operations. This guide covers how to configure Leaf for crop insurance programs that automate acreage reporting, production data collection, and claims validation. It replaces manual data entry and paperwork while improving the grower experience. ## Account structure Use a single API owner per environment (production, test, staging). Each Leaf user represents one insured — a grower or policyholder. This approach: * Makes grower onboarding simple — send a Magic Link or connect provider credentials manually. * Keeps billing clear, since each Leaf user's acres and field operations are tracked separately. * Lets you apply different configurations per Leaf user when needed (e.g., different `operationsProcessingRange` values for growers in different regions or crop cycles). ## Data needs Crop insurance programs typically need two types of field operations: **Planting operations** — plant dates and boundaries for acreage reporting. You need to know what was planted, when, and where. **Harvest operations** — yield or production data for validating claims at the end of the season. In many cases, you don't need point-level GeoJSON. Leaf's operation summaries provide the field-level totals (crop, area, yield) that feed acreage and production reports. If you do need to verify precise yield values or perform deeper analysis, the `standardGeoJSON` and `filteredGeoJSON` outputs are available. ## Processing rules If your current workflow relies on SMS or a similar tool, you'll want Leaf's processed output to match as closely as possible. The configurations that have the most impact on final yield values are: **`cleanupStandardGeojson`** (default: `true`) — removes points marked as invalid from the standard GeoJSON output. Leave this enabled. **`cleanupRules`** — defines which data points are considered valid. The defaults filter out zero-yield points, negative values, and points where recording status is off. If your current pipeline uses different thresholds, you can customize these rules. Setting custom rules replaces the entire default rule set — include every rule you need. **`operationsRemoveOutliers`** (default: `true`) — removes harvest points where yield falls outside the standard deviation threshold. Works with `operationsOutliersLimit`. **`operationsOutliersLimit`** (default: `3`) — the number of standard deviations from the mean beyond which harvest yield points are flagged as outliers. Adjust this if your current pipeline uses a different threshold. **`unitMeasurement`** — set to `IMPERIAL` or `METRIC` to get consistent units across all providers. Using `DEFAULT` preserves whatever the source data uses, which can vary. For additional processing customization beyond configurations, ask your Leaf CSM about Workflows. ## Sync granularity Controlling which data Leaf pulls is critical for crop insurance. You're connecting grower accounts that may have years of history across many fields, but you probably only need the current season. **`operationsProcessingRange`** — the lookback period in months. The default of 12 captures the rolling past year. Set this to match your reporting window. If you only need the current crop year, a shorter window reduces processing time and billing. **`customDataSync`** — when set to `true`, Leaf fetches fields in preview mode only (no boundaries, no files). You then select which fields to fully process. Use this when you only need specific fields rather than an entire farm. **`organizationDataSync`** — controls which John Deere organizations are included. Set to `SELECTED_ONLY` if you're connecting grower accounts that have access to multiple organizations and you only need data from specific ones. Configuration changes are not retroactive. Set your configurations before connecting grower accounts. If you need to reprocess data after a configuration change, use the Reprocess Operation endpoint. ## What to read next * [Integration Planning](/guides/planning/integration-planning) — the full planning checklist * [Data Sync Customization](/guides/planning/data-sync-customization) — detailed control over what Leaf processes * [Configuration reference](/configuration/overview) — all available configuration options # Farm Management (FMIS) Source: https://docs.withleaf.io/guides/by-use-case/fmis Configure Leaf for farm management information systems (FMIS): field boundary syncing, operations, satellite imagery, and weather from connected providers. This guide covers how to configure Leaf as the data infrastructure for a farm management information system. Leaf fetches and standardizes field boundaries, machine files, and field operations from multiple machinery brands and provides satellite imagery, weather data, and grower connectivity tools. ## Account structure Use a single API owner per environment (production, test, staging). Each Leaf user represents one grower account in your FMIS. This maps cleanly to how most FMIS platforms work: * Each Leaf user corresponds to your internal representation of a customer. * Adding a new grower means creating a new Leaf user. * Usage and billing metrics are available per Leaf user, useful if you pass costs through to growers. If your FMIS supports sub-accounts or partner access, you can create multiple API owners (e.g., one per region or partner). In practice, most FMIS integrations only need separate API owners for test vs. production environments. ## Data needs FMIS platforms typically consume several Leaf data products: **Field boundaries** — synced automatically from connected providers. Leaf normalizes boundaries across John Deere, Climate FieldView, CNHi, and others into a consistent format. **Field operation summaries and images** — planting, harvest, and application operations with field-level totals. Enable `operationsImageCreation` if you want map images generated for each operation. **Satellite imagery** — in-season NDVI and other vegetation indices from Sentinel and Planet. Configured separately from machine data. **Weather data** — historical and forecasted weather at the field or coordinate level. Some FMIS platforms also need point-level data (`standardGeoJSON` or `filteredGeoJSON`) for custom map rendering or advanced analytics. ## Processing rules FMIS platforms generally want clean, ready-to-display data. The default configurations work well for most cases: **`cleanupStandardGeojson`** (default: `true`) — leave enabled to get cleaned point data. **`fieldOperationCreation`** (default: `true`) — leave enabled so Leaf automatically merges machine files into field operations. **`operationsFilteredGeojson`** (default: `true`) — provides an additional filtered output with outliers and non-representative points removed. Useful for map rendering. **`operationsImageCreation`** (default: `false`) — enable if you want Leaf to generate PNG images for each operation. Configure `operationsImageAttributeCreation` to control which attributes get images (e.g., yield, moisture, seed rate). **`operationsImageAsGeoTiff`** (default: `false`) — enable if you need GeoTIFF format for integration with GIS tools. **`unitMeasurement`** — set to `IMPERIAL` or `METRIC` for consistent units across providers. **`operationsMergeRange`** and **`operationsMergeRangeHarvested`** — control how Leaf groups machine files into single operations. Defaults are 5 days for non-harvest and 21 days for harvest. Adjust if your growers' operations span different time windows. ## Sync granularity FMIS platforms typically track multi-year field histories. Configure sync settings to match how far back your growers need data: **`operationsProcessingRange`** — set this to cover the number of seasons your FMIS displays. If growers want 2-3 years of history, adjust accordingly. **`customDataSync`** — consider enabling this to limit which fields Leaf fully processes, especially during initial onboarding when you're connecting accounts with hundreds of fields. **`organizationDataSync`** — relevant if you're connecting growers with large John Deere accounts that span multiple organizations. Set to `SELECTED_ONLY` to process only the organizations you need. Set up alerts for field events, machine file events, and field operation events. This lets your FMIS update in near real-time as new data flows through Leaf, rather than polling on a schedule. ## What to read next * [Integration Planning](/guides/planning/integration-planning) — the full planning checklist * [Data Sync Customization](/guides/planning/data-sync-customization) — detailed control over what Leaf processes * [Configuration reference](/configuration/overview) — all available configuration options # Managed Service Provider Source: https://docs.withleaf.io/guides/by-use-case/managed-service-provider Configure Leaf for managed service providers (MSPs) building agricultural data solutions for multiple client organizations with isolated data and billing. This guide covers how to structure Leaf when you serve multiple client organizations — agribusinesses, co-ops, ag retailers, or other agtech companies. As a managed service provider, you need data isolation between clients, independent billing, separate provider credentials per client, and the flexibility to configure processing differently for each one. ## Account structure Create a separate API owner for each major client, in each environment. This is the key structural difference from FMIS or crop insurance integrations, where a single API owner usually covers everything. Each API owner holds the Leaf users for that client's growers. This structure provides: **Data isolation** — if a grower revokes access for one client, it doesn't affect their data sharing with other clients. Provider credentials are scoped to the API owner level. **Per-client configuration** — Client A can receive cleaned, filtered GeoJSON while Client B gets raw data. Each API owner has its own default configurations that apply to all Leaf users underneath it. **Independent billing** — usage is tracked per API owner, so you can see exactly how many acres each client consumes. This makes cost pass-through straightforward. If a client is small enough that it doesn't warrant a separate API owner, you can group multiple small clients under one API owner and use Leaf user-level configurations to differentiate their processing. The trade-off is less billing granularity. ## Data needs Data needs vary by client. Some common patterns: * **Agronomic analysis** clients need point-level GeoJSON, yield maps, and satellite imagery. * **Record-keeping** clients need field boundaries, operation summaries, and crop/product data. * **Field trial analysis** clients need high-resolution point data with precise spatial allocation. * **Sustainability** clients need tillage operations and potentially application data. Discuss specific data requirements with each client and configure their API owner accordingly. Your Leaf Customer Success representative can help map client needs to Leaf's data products. ## Processing rules Since each client gets its own API owner, you can set processing configurations independently. Some considerations: **Clients coming from SMS workflows** may want `cleanupRules` tuned to match their existing data cleaning thresholds. See the [Crop Insurance](/guides/by-use-case/crop-insurance) guide for details on these configurations. **Clients who want raw data** can have `cleanupStandardGeojson` set to `false` at the API owner level. **Clients with specific unit preferences** — set `unitMeasurement` per API owner to match what each client expects. **Clients who need images** — enable `operationsImageCreation` and configure `operationsImageAttributeCreation` per API owner. Within a single API owner, you can still override configurations at the Leaf user level for individual growers who need different processing. ## Sync granularity Sync settings are also per API owner. Configure each client based on their requirements: **`operationsProcessingRange`** — some clients need several years of history, others only need the current season. **`customDataSync`** — useful for clients who are cost-sensitive and only want to process specific fields. **`organizationDataSync`** — important for clients whose growers have complex John Deere organization structures. Each API owner has its own acre allotment and billing. The same grower connected under two different API owners counts separately for billing purposes. Factor this into your pricing if multiple clients serve overlapping grower populations. ## What to read next * [Integration Planning](/guides/planning/integration-planning) — the full planning checklist * [Data Sync Customization](/guides/planning/data-sync-customization) — detailed control over what Leaf processes * [Configuration reference](/configuration/overview) — all available configuration options # Customizing Data Sync Source: https://docs.withleaf.io/guides/planning/data-sync-customization Control which boundaries, files, and operations Leaf syncs from providers, including organization filtering, custom data sync, and processing range options. Leaf's configurations let you control which field boundaries, machine files, and field operations are pulled from providers, how broadly it syncs across organizations, and how far back it looks for historical data. These settings directly affect processing volume and billing, so getting them right matters. Configurations can be set at the API owner level (defaults for all Leaf users) or overridden per Leaf user. Changes are not retroactive — they only affect data processed after the change. ## Controlling field synchronization ### fieldsAutoSync Default: `true` When enabled, Leaf automatically syncs field boundaries from connected providers. The initial sync happens when a Leaf user's provider credentials are connected, then at least every 24 hours (or sooner if the provider supports event-driven updates). Set to `false` if you want to trigger sync manually through the Manual Sync endpoint. ### customDataSync Default: `true` This is Leaf's most powerful tool for controlling data volume. When enabled, Leaf fetches fields in preview mode only — no boundaries are fully resolved, no machine files are processed. You then select which fields to fully process. This is useful when: * You only need a subset of a grower's fields. * You're connecting accounts with hundreds of fields and want to avoid processing (and paying for) all of them. * You want to preview what's available before committing to full processing. When you change this from `true` to `false`, previously previewed fields can be fully processed using the Manual Sync endpoint. ### fieldsAutoMerge and fieldsMergeIntersection `fieldsAutoMerge` (default: `false`) — when enabled, Leaf automatically merges fields whose boundaries intersect above the threshold defined by `fieldsMergeIntersection`. `fieldsMergeIntersection` (default: `0.01`) — the minimum intersection percentage required to trigger a merge. When fields meet this threshold and auto-merge is enabled, Leaf creates a new `MERGED` field. The original fields remain inactive for historical reference. ### fieldsAttachIntersection Default: `0.01` The minimum intersection percentage required to link a machine file to a field. If the overlap between the file's data points and a field boundary exceeds this threshold, Leaf associates the file with that field. `0.01` represents the smallest possible overlap. ### automaticFixBoundary Default: `true` Leaf automatically attempts to correct invalid boundary geometries received from providers (self-intersections, duplicate vertices, etc.). Leave this enabled unless you have a specific reason to preserve raw geometries. ## Controlling organization scope ### organizationDataSync Default: `ALL` This setting matters for John Deere accounts where a single grower may have access to multiple organizations. By default, Leaf syncs data from all of them. Set to `SELECTED_ONLY` to sync only from organizations you explicitly mark as `SELECTED` through the Organization Sync endpoints. This prevents Leaf from processing data across organizations the grower has access to but you don't need. ### syncPartnerData Default: `false` When enabled, Leaf fetches shared or partner data — for example, from John Deere Operations Center partnerships or Ag Leader shared accounts. Only works if the necessary permissions are granted. Leave this `false` unless you specifically need partner-shared data. Enabling it can significantly increase the volume of data Leaf processes. ## Controlling operation processing ### operationsAutoSync Default: `true` When enabled, Leaf automatically syncs operations from connected providers. Disable if you only want field boundaries and plan to upload machine files manually. ### operationsProcessingRange Default: 12 months The lookback period for fetching operations from providers. Leaf only processes operations created or updated within this window. Shorter windows mean less data to process and lower billing. Longer windows give you more historical coverage. Set this based on your use case: crop insurance programs might need only the current season (6-12 months), while FMIS platforms might need 2-3 years. ### fieldOperationCreation Default: `true` When enabled, Leaf automatically creates field operations by merging machine files with field boundaries. Disable if you only need raw machine file outputs without the spatial allocation to fields. ### operationsMergeRange Default: 5 days The time window for grouping machine files into a single non-harvest field operation. Files for the same field, crop, and operation type within this window are merged into one operation. ### operationsMergeRangeHarvested Default: 21 days The time window for grouping machine files into a single harvest field operation. Longer than the non-harvest window because harvest operations commonly span more days. ### splitOperationsByField Default: `true` Creates separate field operations for each distinct field boundary that intersects with the machine data. If disabled, data intersecting multiple boundaries may be combined into a single operation. ### splitOperationsByProvider Default: `true` Groups machine files by provider when creating field operations. If disabled, Leaf merges data from different providers into the same field operation when other criteria (field, crop, operation type, date) match. ### splitOperationsByTillType Default: `false` Creates separate field operations for each unique tillage type found in the machine data. Enable if your application needs to distinguish between tillage methods. ## Controlling other data types ### implementsAutoSync, machinesAutoSync, operatorsAutoSync, productsAutoSync, zonesAutoSync All default to `false`. Enable these to automatically sync the corresponding metadata from providers. `productsAutoSync` currently applies to John Deere only. ## Practical patterns **Minimize billing during development:** Set `customDataSync` to `true` and `organizationDataSync` to `SELECTED_ONLY`. Only fully process the few fields you need for testing. **Full automation for production:** Set `customDataSync` to `false` (or selectively enable fields after preview), `fieldsAutoSync` to `true`, and `operationsAutoSync` to `true`. Configure `operationsProcessingRange` to cover the historical window you need. **Selective field processing:** Enable `customDataSync`, preview all available fields, then use the Manual Sync endpoint to fully process only the ones your application needs. ## What to read next * [Integration Planning](/guides/planning/integration-planning) — the full planning checklist * [Configuration reference](/configuration/overview) — all available configuration options # Preparing Files for Upload Source: https://docs.withleaf.io/guides/planning/file-preparation Prepare machine files for manual upload to Leaf. Covers folder structure, supported formats, and shapefile exports organized by equipment manufacturer. This guide covers how to prepare machine files for manual upload to Leaf. Following the correct folder structure and format for each equipment type ensures your data processes successfully. ## General requirements All files must be uploaded as ZIP files. Leaf extracts and processes the contents automatically. Maximum file size is 3 GB. **ZIP the folder directly from your monitor or USB drive.** Each equipment manufacturer uses specific folder names and structures that must stay intact. Don't create new folders, rename folders, or move files around. Leaf expects these specific structures and looks for files in the correct locations. Leaf looks up to two levels deep inside nested ZIP files, so you can upload a single ZIP containing multiple folders or a ZIP containing other ZIP files. ## Use original monitor files when possible Original monitor files contain richer data and process more reliably than exported shapefiles. If you're using Ag Leader SMS, export the native `.agdata`, `.ilf`, or `.yld` files from your monitor rather than creating shapefile exports. ## Equipment-specific formats ### John Deere **GreenStar 2 (2600)** ```text theme={null} RCD/ ├── *.fdd └── *.fdl ``` Locate the `RCD` folder on your USB drive, ZIP it, and upload. **GreenStar 3 (2630)** ```text theme={null} GS3_2630/ └── RCD/ └── EIC/ └── global.ver/ └── documentation/ └── .../ ├── *.fdd └── *.fdl ``` Locate the `GS3_2630` or `RCD` folder on your USB drive, ZIP it with the complete folder hierarchy, and upload. **GreenStar 4 (Gen 4 — 4600/4630)** ```text theme={null} JD-Data/ └── log/ └── *.jdl ``` Locate the `JD-Data` folder, ZIP it, and upload. MyJohnDeere shapefile exports are supported, but native monitor files are preferred. ### Climate FieldView / Precision Planting These are the same 20|20 monitors listed under both brands. **20|20 SeedSense Generation 1 and 2** ```text theme={null} ├── harvest_*.dat ├── field_map_*.dat └── liquid_map_*.dat ``` **20|20 SeedSense Generation 3** ```text theme={null} └── *.2020 ``` Locate the folder containing these files, ZIP it, and upload. ### CNHi (Case IH / New Holland) **Pro 700 / IntelliView IV (Voyager 2)** ```text theme={null} .cn1/ ├── index.vy1 └── (other data files) ``` The `.cn1` folder contains all operation data. ZIP the entire folder and upload. **Pro 1200 / IntelliView 12 (ISOXML)** ```text theme={null} TASKDATA/ ├── TASKDATA.XML └── *.bin ``` Locate the `TASKDATA` folder, ZIP it (keeping the folder name), and upload. ### Ag Leader **INTEGRA (v3.5+), VERSA, or COMPASS** ```text theme={null} ├── *.agdata └── *.agsetup ``` Both files must be present together. ZIP the folder containing both files and upload. **Edge, Insight, or INTEGRA (v3.4)** ```text theme={null} └── *.ilf ``` **PF Advantage, PF 3000, PF 3000 Pro, YM2000** ```text theme={null} └── *.yld ``` If you must export from SMS, use the native monitor file formats above instead of shapefiles. ### Trimble **FMX or CFX monitors (AgData format)** ```text theme={null} Agdata/ ├── Fields/ (*.agf) ├── implements/ (*.agi) ├── prescriptions/ (*.agm) ├── Tasks/ (*.agt) ├── Users/ (*.agu) └── vehicles/ (*.agv) ``` Locate the `Agdata` folder, ZIP it with the complete folder structure, and upload. **GFX-750, TMX-2050 monitors (AgGPS format)** ```text theme={null} AgGPS/ └── Data/ └── Grower/ └── Farm/ └── Field/ └── Task/ ├── *.cpg ├── *.dbf ├── *.shp └── *.shx ``` Locate the operation folder, ZIP it, and upload. See the shapefile requirements section below. ### Raven Slingshot **Raven FMIS** ```text theme={null} ├── *.xml └── *.tab ``` Both `.xml` and `.tab` files should be present. **Raven JDP** ```text theme={null} └── *.jdp ``` ### ISOXML equipment Supported brands include CLAAS, Kuhn, Kverneland Group, Müller-Elektronik, Teknomika, and Topcon. ```text theme={null} TASKDATA/ ├── *.XML └── *.bin ``` The folder must be named `TASKDATA`. Don't rename it. ## Shapefile exports from SMS If you don't have original monitor files, you can export shapefiles from Ag Leader SMS. Original monitor files are strongly preferred — they contain richer data and produce fewer processing issues. ### Required shapefile components ```text theme={null} ├── *.shp (geometry — required) ├── *.dbf (attributes — required) ├── *.shx (index — required) ├── *.prj (projection — required) └── *.cpg (encoding — optional) ``` All four required files must be present, must share the same base name (e.g., `field_harvest.shp`, `field_harvest.dbf`, etc.), and must be placed at the root level of the ZIP — not nested in subfolders. ### Recognized column names The `.dbf` file must include columns that Leaf can map to standard properties. SMS truncates column names to 10 characters, so many names below reflect that truncation. If your column names don't match any listed here, contact support. **Harvest operations:** * **Crop** (required for moisture-based yield calculations): `crop`, `Crop`, `Crop_Type`, `CROP_NM`, `Product_Pr`, `Product___`, `h_crop` * **Moisture**: `Moisture__`, `Moisture`, `moisture`, `MOISTURE`, `Moisture_P`, `moisture_p`, `013A` * **Yield — volume per area** (e.g., bu/ac): `Yld_Vol_We`, `Yield__Wet`, `WET_YIELD` * **Yield — mass per area** (e.g., lb/ac): `Yield_Mass`, `Yld_Mass_W` (wet); `Yld_Mass_D`, `dryyldlba` (dry) * **Yield — total wet mass** (e.g., lb): `WetMass`, `wetMass`, `Harvest_We` * **Yield — total wet volume** (e.g., bu): `wetVolume` * **Yield — dry volume per area** (e.g., bu/ac): `Yld_Vol_Dr`, `DryYldbuac` Leaf calculates missing dry/wet yield properties automatically when a crop column is present. **Planting operations:** * **Crop**: same names as harvest * **Seed rate**: `seedRate`, `AmntPerAc`, `SeedCount`, `Seed Count`, `Rt_Apd_Ct_`, `SeedFlow_k`, `SeedFlow__`, `SeedFlow_s`, `Seed_Cnt__`, `AVE_SEEDS`, `Count/Time Act`, `Application Count` **Application operations:** * **Applied rate**: `AppliedRat`, `Liq_Rt_ga`, `Rt_Apd_Liq`, `Rt_Apd_Ms_`, `actualRate`, `Application Mass`, `Application Volume` * **Product**: `product`, `Product`, `Products`, `Product___`, `ProductName` ### Exporting from SMS 1. In SMS Project Workspace, right-click the operation and select **Export**, then click the paper icon for "Export to a Generic File Format." 2. Choose **Generic** and **Shape** as the file type. 3. Click **Export Selections and Settings** to verify column names. You can rename columns here if needed — for example, add Crop Type and rename it to "Crop." 4. Save to your Desktop or Documents folder. You'll get four files: `.shp`, `.dbf`, `.prj`, `.shx`. 5. Select all four files, compress into a ZIP, and upload. ## Common issues **Upload fails** — confirm the file is a ZIP, contains all required components for the file type, and is under 3 GB. For shapefiles, check that the `.dbf` includes required columns. **Missing or incomplete data** — verify original folder structure is intact, folder names haven't been renamed, and files aren't nested too deeply (Leaf checks up to 2 levels). For Ag Leader INTEGRA, both `.agdata` and `.agsetup` must be present. For ISOXML, both `.XML` and `.bin` files must be in the `TASKDATA` folder. **Processing errors** — double-check the folder structure matches the expected format for your equipment. Don't manually reorganize files or flatten the hierarchy. # Planning Your Integration Source: https://docs.withleaf.io/guides/planning/integration-planning Plan your Leaf API integration: account structure, data needs, processing rules, and sync configuration decisions to make before writing code. Before writing code against the Leaf API, you need to make decisions in four areas: account structure, data needs, processing rules, and sync granularity. Getting these right upfront saves significant rework later. ## The four planning decisions **Account structure** — how you map API owners and Leaf users to your own accounts and growers. Nearly every Leaf implementation uses one Leaf user per grower. If your use case calls for something different, talk to your Customer Success representative first. **Data needs** — which Leaf data products you consume. Field boundaries? Operation summaries? Point-level GeoJSON? Satellite imagery? Weather? The answer drives which APIs you call and which configurations you enable. **Processing rules** — how Leaf cleans, filters, and merges your data. Configurations like `cleanupStandardGeojson`, `cleanupRules`, `operationsRemoveOutliers`, and `operationsMergeRange` control the output. Defaults work for most cases, but if you need results that match an existing pipeline (like SMS exports), you'll need to tune these. **Sync granularity** — which data Leaf actually pulls from providers. Configurations like `customDataSync`, `organizationDataSync`, and `operationsProcessingRange` control the scope. This directly affects billing, since Leaf charges by spatially unique acres. ## Recommended order of operations 1. **Create your API owner(s).** One per environment is typical (production, test, staging). Use clear naming — `leaf-test@company.com`, `leaf-prod@company.com` — so billing and logs are easy to distinguish. 2. **Set API owner-level configurations.** These become the defaults for every Leaf user you create. Get them right before connecting growers, because configuration changes are not retroactive. 3. **Create a single Leaf user and connect one provider account.** Start with a provider account you know well, with a small number of fields. This lets you validate your configuration choices before scaling. 4. **Set up alerts.** At minimum, subscribe to field events, field boundary events, machine file events, and field operation events. Alerts eliminate the need to poll Leaf for updates. 5. **Validate the data.** Confirm that fields, machine files, and field operations are coming through as expected. Adjust configurations if needed. 6. **Scale.** Connect more Leaf users, either through the API directly or through Magic Links / Leaf Link widgets. Do not connect large provider accounts during development. Each connection processes fields and files, consuming your testing acre allotment. Use `customDataSync` to limit which fields Leaf processes, and `organizationDataSync` to limit which John Deere organizations are included. ## Account structure patterns **Single API owner, many Leaf users** — the most common pattern. One API owner per environment, one Leaf user per grower. Simple, clean billing, per-user configuration overrides when needed. **Multiple API owners** — useful for managed service providers who serve distinct client organizations. Each client gets its own API owner, isolating data, credentials, and billing. Each API owner then holds Leaf users for that client's growers. Leaf does not provide a separate test environment. Create distinct API owners for test and production instead. ## Configuration inheritance Configurations set at the API owner level apply to all Leaf users under that API owner by default. You can override any configuration at the Leaf user level for individual growers. One important detail: once you set a configuration on a Leaf user, it no longer inherits from the API owner for that setting. If you later change the API owner's configuration, the Leaf user keeps its own value. ## What to read next The use-case guides cover specific configuration recommendations: * [Crop Insurance](/guides/by-use-case/crop-insurance) — acreage reporting, production data, claims validation * [FMIS](/guides/by-use-case/fmis) — field records, operation summaries, satellite and weather data * [Managed Service Provider](/guides/by-use-case/managed-service-provider) — multi-client data isolation and billing For controlling what data Leaf syncs and processes, see [Data Sync Customization](/guides/planning/data-sync-customization). For understanding how Leaf's processing rules affect output, see the [Configuration reference](/configuration/overview). # Why Yield Values Differ Between Platforms Source: https://docs.withleaf.io/guides/planning/yield-differences Why yield values differ between platforms even from the same harvest data, and what drives those differences in Leaf's processing pipeline. It's normal to see small differences in yield values across platforms, even when they originate from the same harvest data. "Field yield" is not a single raw measurement — it's the result of a processing pipeline, and each platform makes slightly different choices about how to clean, filter, correct, and summarize harvest data into a final bu/ac value. ## Do the differences matter? Small absolute differences (a few percentage points) typically do not affect insights or recommendations. What matters is consistency: when all fields and all data sources run through the same pipeline, relative differences between fields reflect real agronomic variation — weather, soil, management — rather than artifacts of processing rules. Leaf processes all harvest data through a single, standardized pipeline regardless of provider or upload source. Comparisons within Leaf are internally consistent even when the source providers may differ. ## What's happening behind the scenes Think of yield reporting as a pipeline with eight steps. Small differences at any step shift the final number. 1. **Data collection** — the combine monitor records points with yield, moisture, speed, and location. 2. **Data receipt** — Leaf receives the data from a provider API or through file upload. 3. **Point cleaning and filtering** — non-representative or invalid points are removed. 4. **Boundary alignment** — points are matched to a field boundary to determine what's "in the field." 5. **Overlap handling** — headland and point row overlap is resolved. 6. **Moisture correction** — yield is standardized to a reference moisture (e.g., 15% for corn, 13% for soybeans). 7. **Area calculation** — harvested area is determined from boundary or pass data. 8. **Summarization** — points are aggregated into a single field-level yield value. ## Common drivers of differences ### Provider API data vs. provider UI data The data exposed by a provider's API is not always identical to what their application displays. Providers may process or edit values in their UI in ways that don't propagate through the API. If two systems start with slightly different versions of the harvest data, their final yields will differ. ### Field boundary differences Even small differences in boundary polygons cause points near edges to be included or excluded. A few rows of combine passes in or out shifts the average, especially on smaller fields. This can range from minor to several bu/ac depending on edge variability. ### Outlier filtering Each platform has its own logic for removing non-representative points: slowdowns into turns, start/stop events, unrealistic yield or moisture values, speed thresholds, recording status. This is often one of the largest contributors to yield differences. ### Overlap handling Overlapping coverage is common in headlands and point rows. Platforms differ in whether they average the overlap, take the most recent pass, or discard duplicates. The effect is outsized on irregular fields and complex boundaries. ### Moisture correction Yield is typically corrected to a standard reference moisture. Differences arise from which moisture readings are used (raw vs. smoothed) and when the correction is applied (before vs. after aggregation). Usually modest, but meaningful when moisture varies across the field. ### Area calculation Two systems can report different bu/ac even with similar total bushels if they calculate acres differently. Boundary acres (static) vs. pass-derived harvested acres (dynamic) produce different denominators. ### Summarization and aggregation Platforms summarize monitor points differently: simple averaging vs. weighting by area, time, or distance. Different handling of partial passes and short segments. Often small, but noticeable on small fields or variable harvest patterns. ### Calibration and farmer edits Some platforms let farmers adjust yield values in the UI, but those edits may not flow through the API. Climate FieldView is a notable example — calibrations made in FieldView do not propagate to API consumers. John Deere does pass calibrations through. ## Analogy Step tracking is a useful comparison. Your phone and watch observe the same walk, but each uses different rules to convert sensor data into a step count. The totals differ slightly, but trends over time within one device remain meaningful because the rules are consistent. ## Troubleshooting larger-than-expected differences If differences between platforms seem too large, work through these checks: 1. **Boundary parity** — confirm the same polygon and acres are being used in both systems. 2. **File completeness** — verify all expected harvest files and segments were ingested. 3. **Filtering differences** — compare points in vs. points out. 4. **Overlap zones** — review headlands and point rows. 5. **Moisture correction** — check reference moisture assumptions. 6. **Provider-side edits** — ask whether the grower adjusted yield in the provider UI. A targeted field-level comparison can usually identify the primary driver of the gap. # Integrate Leaf Data with ArcGIS Source: https://docs.withleaf.io/guides/tutorials/arcgis-integration Use Leaf agricultural data in ArcGIS Pro and ArcGIS Enterprise. Consume field boundaries, satellite imagery, and field operation data as GIS layers. Leaf's API outputs GeoJSON, GeoTIFF, and PNG data that maps directly into ArcGIS workflows. This tutorial covers two integration patterns: consuming data in ArcGIS Pro with Python toolboxes, and automating data ingest in ArcGIS Enterprise with geoprocessing services. ## Before you start * A Leaf account with API credentials and at least one connected provider or sample data. * ArcGIS Pro 3.x or ArcGIS Enterprise 11.x (or later). * Python 3.9+ with `requests` and `arcpy` available (bundled with ArcGIS Pro). * The sample toolboxes from [Leaf's ArcGIS GitHub repo](https://github.com/leafagriculturedemo/arcgis-with-leaf-samples). ## ArcGIS Pro ### Step 1: Set up authentication Communication with the Leaf API requires a bearer token. The sample toolbox includes an authentication tool that stores the token in a temporary table for use by other tools. ```python theme={null} import requests response = requests.post( "https://api.withleaf.io/api/authenticate", json={"username": "your-email@example.com", "password": "your-password"} ) token = response.json()["id_token"] ``` Run the **Leaf Authentication** tool in the sample toolbox, or store the token programmatically. Other toolbox tools check for a valid token before executing. ### Step 2: Fetch field boundaries The **Get Field Boundaries** tool combines data from multiple Leaf endpoints (growers, farms, fields, and boundaries) and converts them into a feature layer in your ArcGIS Pro map. The Leaf API returns boundaries as GeoJSON. The toolbox converts them to features using `arcpy.JSONToFeatures_conversion`. Fields from different providers appear in a single layer with attributes like provider, farm name, and field name. ```python theme={null} import requests, json headers = {"Authorization": f"Bearer {token}"} fields = requests.get( "https://api.withleaf.io/services/fields/api/fields", headers=headers, params={"leafUserId": leaf_user_id, "size": 100} ).json() for field in fields: boundary = requests.get( f"https://api.withleaf.io/services/fields/api/users/{leaf_user_id}/fields/{field['id']}/boundary", headers=headers ).json() # Convert to feature using arcpy ``` ### Step 3: Display satellite imagery Leaf's crop monitoring endpoints return GeoTIFF and PNG images for NDVI, NDRE, and RGB. You can download these and add them as raster layers. ```python theme={null} images = requests.get( f"https://api.withleaf.io/services/satellite/api/fields/{satellite_field_id}/processes", headers=headers, params={"startDate": "2025-01-01", "endDate": "2025-12-31"} ).json() for image in images: ndvi_image = next( (img for img in image.get("images", []) if img.get("type") == "tif_colorized"), None ) if ndvi_image: ndvi_url = ndvi_image["downloadUrl"] response = requests.get(ndvi_url, headers=headers) with open(f"/tmp/ndvi_{image['date']}.tif", "wb") as f: f.write(response.content) ``` Add the downloaded GeoTIFFs to your map as raster layers. Leaf images are already georeferenced and clipped to the field boundary. ### Step 4: Load field operations Field operations expose a standard GeoJSON download URL. Fetch the download URL first, then download the actual GeoJSON and convert it to a feature layer: ```python theme={null} op_id = "your-operation-id" geojson_ref = requests.get( f"https://api.withleaf.io/services/operations/api/operations/{op_id}/standardGeojson", headers=headers ).json() geojson = requests.get( geojson_ref["downloadStandardGeojson"], headers=headers ).json() with open("/tmp/operation.geojson", "w") as f: json.dump(geojson, f) arcpy.conversion.JSONToFeatures("/tmp/operation.geojson", "operation_layer") ``` The GeoJSON features have standardized properties like `yieldVolume`, `seedRate`, and `appliedRate` that you can use for symbology and analysis. ## ArcGIS Enterprise For automated workflows, publish a geoprocessing service that acts as a webhook for Leaf alerts. When new data arrives, Leaf sends an alert to your geoprocessing service, which downloads and processes the data automatically. ### Step 5: Create a geoprocessing webhook Write a Python toolbox that accepts the parameters for the specific Leaf event you subscribe to. For `newSatelliteImage`, the payload includes: | Parameter | Type | Description | | ------------------ | ------ | ------------------------------------------- | | `externalId` | String | Your satellite field external ID | | `processId` | String | The satellite process ID | | `type` | String | Alert type, for example `newSatelliteImage` | | `timestamp` | String | When the alert fired | | `X-Leaf-Signature` | String | HMAC signature for validation | ### Step 6: Validate the signature Every alert from Leaf includes an `X-Leaf-Signature` header. Validate it against your secret to confirm the request came from Leaf: ```python theme={null} import base64 import hmac import hashlib def validate_signature(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).digest() provided = base64.b64decode(signature) return hmac.compare_digest(expected, provided) ``` ### Step 7: Publish and register 1. Publish the geoprocessing service following [ArcGIS Pro's publishing guide](https://pro.arcgis.com/en/pro-app/latest/help/analysis/geoprocessing/share-analysis/publishing-geoprocessing-service-in-arcgis-pro.htm). 2. The service must be publicly accessible (Leaf needs to reach it). 3. Register the service URL as a Leaf alert endpoint: ```bash theme={null} curl -X POST "https://api.withleaf.io/services/alerts/api/alerts/arcgis" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "events": ["newSatelliteImage"], "name": "Satellite images listener", "url": "https://your-arcgis-server.com/arcgis/rest/services/LeafWebhook/GPServer/LeafWebhook/submitJob?f=json", "secret": "your-random-secret" }' ``` Use `/submitJob?f=json` for asynchronous geoprocessing or `/execute?f=json` for synchronous. ## What you built You connected Leaf's API to ArcGIS for two workflows: interactive data exploration in ArcGIS Pro, and automated data ingest in ArcGIS Enterprise via webhook-driven geoprocessing. The sample toolboxes on [GitHub](https://github.com/leafagriculturedemo/arcgis-with-leaf-samples) provide working implementations of these patterns. Adapt them to your specific requirements. # Connect Leaf's MCP Server in Claude Code Source: https://docs.withleaf.io/guides/tutorials/claude-code-mcp Connect Leaf's MCP server to Claude Code to query Leaf users, fields, operations, and weather data from the terminal using natural language. Leaf's MCP server works with Claude Code, giving you Leaf API access from the command line through natural language. You can list users, browse fields, pull operations data, and read API documentation without writing HTTP requests. ## Before you start * A Leaf account with API credentials. * Claude Code installed. See [Anthropic's Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code) for installation. * A valid Leaf API token: ```bash theme={null} curl -X POST "https://api.withleaf.io/api/authenticate" \ -H "Content-Type: application/json" \ -d '{"username": "your-email@example.com", "password": "your-password"}' ``` Save the `id_token`. ## Step 1: Configure the MCP server Add the Leaf MCP server to your Claude Code configuration. Create or edit `~/.claude/claude_code_config.json`: ```json theme={null} { "mcpServers": { "leaf-mcp": { "type": "url", "url": "https://mcp.withleaf.io/mcp", "headers": { "LEAF_TOKEN": "your-leaf-api-token" } } } } ``` For project-level configuration, add the same block to `.mcp.json` in your project root instead. Your Leaf token expires periodically. If you get authentication errors, generate a fresh token and update the config. ## Step 2: Verify the connection Launch Claude Code and ask: ``` List available Leaf API documentation ``` Claude Code calls `get_docs_index` and returns the documentation index. If you see the list, the MCP server is connected. ## Step 3: Query your data The same tools available in Cursor work here. A few examples: **List Leaf users:** ``` Show me all Leaf users ``` **Query fields:** ``` List fields for Leaf user ``` **Pull harvest operations:** ``` Show harvested operations for Leaf user ``` **Get an operation summary:** ``` Get the summary for operation ``` **Check weather:** ``` What's the daily weather forecast for field under Leaf user ? ``` ## Step 4: Use documentation tools for context Claude Code can pull Leaf API documentation inline to answer questions about endpoints, schemas, and behavior: ``` Show me the Leaf documentation for field operations endpoints ``` This calls `get_leaf_doc` with the path `API_Reference/Field_Operations/operations_endpoints` and returns the full endpoint documentation, which Claude Code can then reference when helping you write integration code. ## Available tools | Category | Tools | | ------------- | ---------------------------------------------------------------------------------- | | Users | `list_users` | | Fields | `list_fields`, `get_field`, `get_field_boundary` | | Operations | `list_operations`, `get_operation`, `get_operation_summary`, `get_operation_units` | | Machine files | `list_files`, `get_file`, `get_file_summary`, `get_file_status`, `get_file_units` | | Batches | `list_batches`, `get_batch`, `get_batch_status` | | Weather | Forecast and historical, daily and hourly, by field or lat/lon | | Billing | `list_billing_contracts`, `get_contract_consumption` | | Configuration | `get_api_owner_configuration`, `get_leaf_user_configuration` | | Credentials | Provider credential event tools for John Deere, Climate FieldView, CNHi | | Documentation | `get_docs_index`, `get_leaf_doc` | ## What you built You connected Leaf's MCP server to Claude Code and queried Leaf data from the terminal. This setup is useful for exploring customer data, debugging integrations, and getting contextual API documentation while writing code. For the same workflow in Cursor IDE, see [Connect Leaf's MCP Server in Cursor](/guides/tutorials/cursor-mcp). # Connect to AgLeader AgFiniti API Source: https://docs.withleaf.io/guides/tutorials/connect-agleader Connect AgLeader AgFiniti to Leaf: get developer credentials, complete the OAuth flow, and attach credentials to a Leaf user. This tutorial walks through connecting AgLeader's AgFiniti platform to Leaf to sync machine files. You'll get developer credentials, run the OAuth flow to obtain tokens, and attach provider credentials to a Leaf user. [Magic Link](/components/magic-link) and [Leaf Link](/components/leaf-link) handle the OAuth UI for you. This tutorial covers the manual flow for developers building it into their own application. ## Before you start * A Leaf account with a valid API token. * A Leaf user created. * An AgLeader developer account. Complete the [developer registration form](https://www.agleader.com/developers/). AgLeader requires proof of liability insurance and a one-time fee before providing developer credentials. ## Step 1: Get your AgLeader credentials Log in to [AgFiniti](https://www.agfiniti.com), navigate to the **Consumer Keys** tab, and note your: * **Public Key** * **Private Key** Configure the **Redirection URL(s)** on the application page to include the callback URL that will receive the authorization code. ## Step 2: Get the authorization code Redirect the grower to the AgFiniti authorization URL. Replace `{your_public_key}` and `{redirect_uri}` with your values: ``` https://www.agfiniti.com/Account/Authorize?response_type=code&client_id={your_public_key}&redirect_uri={redirect_uri}&scope=read%20readwrite ``` This URL includes the `read` and `readwrite` scopes required for Leaf integration. If you plan to upload prescriptions, add the `fileupload` scope. After the grower authenticates, AgFiniti redirects to your `redirect_uri` with a `code` parameter. ## Step 3: Exchange the code for tokens POST the code to AgFiniti's token endpoint. The `Authorization` header requires Base64-encoded `publicKey:privateKey`: ```bash cURL theme={null} curl -X POST "https://www.agfiniti.com/api/token" \ -H "Authorization: Basic $(echo -n 'yourPublicKey:yourPrivateKey' | base64)" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code&code=THE_CODE_FROM_REDIRECT&redirect_uri=https://your-app.com/callback" ``` ```python Python theme={null} import requests, base64 auth_string = base64.b64encode( f"{public_key}:{private_key}".encode() ).decode() token_response = requests.post( "https://www.agfiniti.com/api/token", headers={"Authorization": f"Basic {auth_string}"}, data={ "grant_type": "authorization_code", "code": "THE_CODE_FROM_REDIRECT", "redirect_uri": "https://your-app.com/callback" } ) refresh_token = token_response.json()["refresh_token"] ``` ```javascript JavaScript theme={null} const authString = btoa(`${publicKey}:${privateKey}`); const tokenRes = await fetch("https://www.agfiniti.com/api/token", { method: "POST", headers: { Authorization: `Basic ${authString}`, "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", code: "THE_CODE_FROM_REDIRECT", redirect_uri: "https://your-app.com/callback", }), }); const { refresh_token } = await tokenRes.json(); ``` Save the `refresh_token`. ## Step 4: Attach credentials to the Leaf user ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "publicKey": "your-agleader-public-key", "privateKey": "your-agleader-private-key", "refreshToken": "the-refresh-token-from-step-3" }' ``` ```python Python theme={null} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/ag-leader-credentials", headers={"Authorization": f"Bearer {leaf_token}"}, json={ "publicKey": public_key, "privateKey": private_key, "refreshToken": refresh_token } ) print(response.json()) ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/ag-leader-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ publicKey: publicKey, privateKey: privateKey, refreshToken: refresh_token, }), } ); console.log(await res.json()); ``` Leaf manages token refresh and begins syncing machine files. AgLeader currently supports machine file data only (no direct field boundary sync). ## Step 5: Confirm the credentials are attached Check the stored credentials for the Leaf user: ```bash cURL theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" ``` If this worked, Leaf returns the AgLeader credential object for that user. ## What you built You connected AgLeader AgFiniti to a Leaf user. Leaf now fetches and standardizes machine files from AgFiniti. Query the data through the [machine files API](/api-reference/files) and [field operations quickstart](/guides/tutorials/field-operations-quickstart). For the credentials schema and management endpoints, see the [AgLeader provider guide](/providers/agleader) and the [provider credentials API reference](/api-reference/providers). # Connect to Climate FieldView API Source: https://docs.withleaf.io/guides/tutorials/connect-climate-fieldview Connect Climate FieldView to Leaf: register as a developer, complete the OAuth flow, and attach credentials to a Leaf user. This tutorial walks through connecting Climate FieldView to Leaf so you can sync field boundaries, machine files, and field operations. You'll obtain developer credentials, run the OAuth flow, and attach the resulting provider credentials to a Leaf user. [Magic Link](/components/magic-link) and [Leaf Link](/components/leaf-link) handle the OAuth UI for you. This tutorial is for developers building the flow into their own application. ## Before you start * A Leaf account with a valid API token. * A Leaf user created. * A Climate FieldView developer account. Register at [dev.fieldview.com](https://dev.fieldview.com/join-us/). * After approval, you'll receive a `clientId`, `clientSecret`, and `apiKey`. Before proceeding, email [developer@climate.com](mailto:developer@climate.com) to verify you have the correct scopes: `asHarvested:read`, `asPlanted:read`, `asApplied:read`, `fields:read`, `resourceOwners:read`, `farmOrganizations:read`. ## Step 1: Get the authorization URL Leaf constructs the OAuth URL for you. Send a POST to the Climate FieldView credentials endpoint: ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-cfv-client-id", "redirect_uri": "https://your-app.com/callback" }' ``` ```python Python theme={null} import requests headers = {"Authorization": f"Bearer {leaf_token}"} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/climate-field-view-credentials", headers=headers, json={ "client_id": "your-cfv-client-id", "redirect_uri": "https://your-app.com/callback" } ) auth_url = response.json()["url"] ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/climate-field-view-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ client_id: "your-cfv-client-id", redirect_uri: "https://your-app.com/callback", }), } ); const { url } = await res.json(); ``` Leaf returns a URL with the minimum required scopes. If you need write access (for prescriptions, soil data, or imagery upload), add a `scope` array to the request body: ```json theme={null} { "client_id": "your-cfv-client-id", "scope": [ "asHarvested:read", "asPlanted:read", "asApplied:read", "fields:read", "resourceOwners:read", "farmOrganizations:read", "fields:write", "rx:write", "soil:write", "imagery:write" ], "redirect_uri": "https://your-app.com/callback" } ``` The write scopes (`fields:write`, `rx:write`, `soil:write`, `imagery:write`) may require additional permissions from Climate FieldView. Confirm you're allowed to request them before generating a URL with them. Redirect the grower to the returned URL. After authorization, Climate FieldView redirects back to your `redirect_uri` with a `code` parameter. ## Step 2: Exchange the code for tokens POST to Climate FieldView's token endpoint with the code from the redirect: ```bash cURL theme={null} curl -X POST "https://api.climate.com/api/oauth/token" \ -H "Authorization: Basic $(echo -n 'clientId:clientSecret' | base64)" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "code=THE_CODE_FROM_REDIRECT&redirect_uri=https://your-app.com/callback&grant_type=authorization_code" ``` ```python Python theme={null} import base64 auth_string = base64.b64encode( f"{cfv_client_id}:{cfv_client_secret}".encode() ).decode() token_response = requests.post( "https://api.climate.com/api/oauth/token", headers={"Authorization": f"Basic {auth_string}"}, data={ "code": "THE_CODE_FROM_REDIRECT", "redirect_uri": "https://your-app.com/callback", "grant_type": "authorization_code" } ) refresh_token = token_response.json()["refresh_token"] ``` ```javascript JavaScript theme={null} const authString = btoa(`${cfvClientId}:${cfvClientSecret}`); const tokenRes = await fetch("https://api.climate.com/api/oauth/token", { method: "POST", headers: { Authorization: `Basic ${authString}`, "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ code: "THE_CODE_FROM_REDIRECT", redirect_uri: "https://your-app.com/callback", grant_type: "authorization_code", }), }); const { refresh_token } = await tokenRes.json(); ``` Save the `refresh_token`. ## Step 3: Attach credentials to the Leaf user ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": "your-cfv-client-id", "clientSecret": "your-cfv-client-secret", "apiKey": "your-cfv-api-key", "refreshToken": "the-refresh-token-from-step-2" }' ``` ```python Python theme={null} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/climate-field-view-credentials", headers=headers, json={ "clientId": cfv_client_id, "clientSecret": cfv_client_secret, "apiKey": cfv_api_key, "refreshToken": refresh_token } ) print(response.json()) ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/climate-field-view-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ clientId: cfvClientId, clientSecret: cfvClientSecret, apiKey: cfvApiKey, refreshToken: refresh_token, }), } ); console.log(await res.json()); ``` Leaf manages token refresh automatically from this point. ## Step 4: Confirm the credentials are attached ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" ``` If this worked, Leaf returns the Climate FieldView credential object for the Leaf user. ## What you built You connected Climate FieldView to a Leaf user through the OAuth flow. Leaf now syncs field boundaries, machine files, and field operations from Climate FieldView. Query the data through the [field operations quickstart](/guides/tutorials/field-operations-quickstart) or the [machine files API](/api-reference/files). For the credentials schema and management endpoints, see the [Climate FieldView provider guide](/providers/climate-fieldview) and the [provider credentials API reference](/api-reference/providers). # Connect to CNHi API (AFS Connect) Source: https://docs.withleaf.io/guides/tutorials/connect-cnhi Connect CNHi (Case IH, New Holland) data to Leaf. Create a developer application, complete the OAuth flow, and attach credentials to a Leaf user. This tutorial covers the **legacy CNHi (AFS Connect)** integration. For CNH Industrial's newer FieldOps API, see [Connect CNHI FieldOps](/guides/tutorials/connect-cnhi-fieldops). This tutorial walks through connecting CNHi to Leaf so you can sync field boundaries, machine files, and field operations from Case IH and New Holland equipment. You'll create a developer application, run the OAuth flow to get user tokens, and attach provider credentials to a Leaf user. [Magic Link](/components/magic-link) and [Leaf Link](/components/leaf-link) handle the OAuth UI for you. This tutorial covers the manual flow for developers building it into their own application. ## Before you start * A Leaf account with a valid API token. * A Leaf user created. * A CNHi developer account. Register at [develop.cnh.com](https://develop.cnh.com/). ## Step 1: Create a CNHi application Sign in to the [CNHi developer portal](https://develop.cnh.com/), go to **Account Dashboard**, and click **Add Application**. After creating the application, go to **App Registrations**, click your application name, then **API Information** to find your: * **Client ID** * **Client Secret** * **Subscription Key** ## Step 2: Get the authorization URL Leaf provides a helper that constructs the CNHi authorization URL: ```bash cURL theme={null} curl -X POST "https://cnhi-oauth2-helper.withleaf.io/get_url" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-cnhi-client-id", "client_secret": "your-cnhi-client-secret", "subscription_key": "your-cnhi-subscription-key", "client_redirect_url": "https://your-app.com/callback", "production": false }' ``` ```python Python theme={null} import requests response = requests.post( "https://cnhi-oauth2-helper.withleaf.io/get_url", json={ "client_id": "your-cnhi-client-id", "client_secret": "your-cnhi-client-secret", "subscription_key": "your-cnhi-subscription-key", "client_redirect_url": "https://your-app.com/callback", "production": False } ) auth_url = response.json()["url"] ``` ```javascript JavaScript theme={null} const res = await fetch("https://cnhi-oauth2-helper.withleaf.io/get_url", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "your-cnhi-client-id", client_secret: "your-cnhi-client-secret", subscription_key: "your-cnhi-subscription-key", client_redirect_url: "https://your-app.com/callback", production: false, }), }); const { url } = await res.json(); ``` Set `production` to `false` for the CNHi staging environment (the default for new apps). Set it to `true` once your app is promoted to production. While in the CNHi staging environment, only staging test accounts work. Production customer accounts won't authenticate. The reverse is true in production. Redirect the grower to the returned URL. After authorization, CNHi redirects to your `client_redirect_url` with a `code` in the URL. ## Step 3: Exchange the code for a refresh token ```bash cURL theme={null} curl -X POST "https://cnhi-oauth2-helper.withleaf.io/get_token" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-cnhi-client-id", "client_secret": "your-cnhi-client-secret", "subscription_key": "your-cnhi-subscription-key", "response_url": "https://your-app.com/callback?code=abc123", "client_redirect_url": "https://your-app.com/callback" }' ``` ```python Python theme={null} tokens = requests.post( "https://cnhi-oauth2-helper.withleaf.io/get_token", json={ "client_id": "your-cnhi-client-id", "client_secret": "your-cnhi-client-secret", "subscription_key": "your-cnhi-subscription-key", "response_url": "https://your-app.com/callback?code=abc123", "client_redirect_url": "https://your-app.com/callback" } ).json() refresh_token = tokens["refresh_token"] ``` ```javascript JavaScript theme={null} const tokenRes = await fetch( "https://cnhi-oauth2-helper.withleaf.io/get_token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "your-cnhi-client-id", client_secret: "your-cnhi-client-secret", subscription_key: "your-cnhi-subscription-key", response_url: "https://your-app.com/callback?code=abc123", client_redirect_url: "https://your-app.com/callback", }), } ); const { refresh_token } = await tokenRes.json(); ``` The `response_url` is the full URL the grower was redirected to, including the `code` parameter that CNHi appended. ## Step 4: Attach credentials to the Leaf user ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": "your-cnhi-client-id", "clientSecret": "your-cnhi-client-secret", "subscriptionKey": "your-cnhi-subscription-key", "refreshToken": "the-refresh-token-from-step-3", "clientEnvironment": "STAGE" }' ``` ```python Python theme={null} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/cnhi-credentials", headers={"Authorization": f"Bearer {leaf_token}"}, json={ "clientId": "your-cnhi-client-id", "clientSecret": "your-cnhi-client-secret", "subscriptionKey": "your-cnhi-subscription-key", "refreshToken": refresh_token, "clientEnvironment": "STAGE" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/cnhi-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ clientId: "your-cnhi-client-id", clientSecret: "your-cnhi-client-secret", subscriptionKey: "your-cnhi-subscription-key", refreshToken: refresh_token, clientEnvironment: "STAGE", }), } ); console.log(await res.json()); ``` Set `clientEnvironment` to `PRODUCTION` once your CNHi application is promoted to production. Leaf handles token refresh automatically. ## Step 5: Confirm the credentials are attached ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" ``` If this worked, Leaf returns the CNHi credential object for the Leaf user. ## What you built You connected CNHi to a Leaf user. Leaf now syncs field boundaries, machine files, and field operations from Case IH and New Holland equipment. Query the data through the [field operations](/guides/tutorials/field-operations-quickstart) endpoints. For the credentials schema and management endpoints, see the [CNHi provider guide](/providers/cnhi) and the [provider credentials API reference](/api-reference/providers). # Connect to CNHI FieldOps API Source: https://docs.withleaf.io/guides/tutorials/connect-cnhi-fieldops Connect CNHI FieldOps (Case IH, New Holland) data to Leaf. Create a developer application, complete the OAuth flow, and attach credentials to a Leaf user. This tutorial walks through connecting CNHI FieldOps to Leaf so you can sync field boundaries, machine files, and field operations from Case IH and New Holland equipment. You'll create a developer application, run the OAuth flow to get user tokens, and attach provider credentials to a Leaf user. [Magic Link](/components/magic-link) and [Leaf Link](/components/leaf-link) handle the OAuth UI for you. This tutorial covers the manual flow for developers building it into their own application. CNH Industrial has two API platforms. This tutorial covers **CNHI FieldOps** (the current platform). For the legacy AFS Connect API, see [Connect CNHI (AFS Connect)](/guides/tutorials/connect-cnhi). ## Before you start * A Leaf account with a valid API token. * A Leaf user created. * A CNH developer account registered with a **company-domain email** (Gmail, Hotmail, and other generic domains are not supported). Register at [develop.cnh.com](https://develop.cnh.com/). ## Step 1: Create a CNHI FieldOps application 1. Sign in to the [CNH developer portal](https://develop.cnh.com/) (or create an account if you don't have one). 2. Navigate to **Account**, then select **My Applications** in the left-hand menu. 3. Click **Add New Application**. 4. Fill out the Application Information form: * **Application Name** — a label for your integration (e.g., "My Company - Leaf Integration"). * **Portal** — select **FieldsOps Portal**. * **Region** — select the region your growers operate in. * **OAuth Callback URL(s)** — enter the URL where CNHI should redirect growers after they authorize. If you're using **Magic Link or Leaf Link**, enter `https://widget.withleaf.io`. If you're building your own OAuth flow, enter your application's callback URL (the same URL you'll pass as `client_redirect_url` in Step 2). * Fill in the remaining required fields (description, icon, contact info) and click **Submit**. 5. Once the application is created, go to **My Applications**, click your application name, then find your **Client ID**, **Client Secret**, and **Subscription Key** in the API Information section. These credentials are specific to the FieldOps API. Existing CNHI (AFS Connect) credentials will not work. You must create a new application in the developer portal. ## Step 2: Get the authorization URL Leaf provides a helper that constructs the CNHI FieldOps authorization URL: ```bash cURL theme={null} curl -X POST "https://cnhi-oauth2-helper.withleaf.io/fieldops/get_url" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-cnhi-client-id", "client_redirect_url": "https://your-app.com/callback", "production": false }' ``` ```python Python theme={null} import requests response = requests.post( "https://cnhi-oauth2-helper.withleaf.io/fieldops/get_url", json={ "client_id": "your-cnhi-client-id", "client_redirect_url": "https://your-app.com/callback", "production": False } ) auth_url = response.json()["url"] ``` ```javascript JavaScript theme={null} const res = await fetch("https://cnhi-oauth2-helper.withleaf.io/fieldops/get_url", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "your-cnhi-client-id", client_redirect_url: "https://your-app.com/callback", production: false, }), }); const { url } = await res.json(); ``` Set `production` to `false` for the CNHI staging environment (the default for new apps). Set it to `true` once your app is promoted to production. While in the CNHI staging environment, only staging test accounts work. Production customer accounts won't authenticate. The reverse is true in production. Redirect the grower to the returned URL. After authorization, CNHI redirects to your `client_redirect_url` with a `code` in the URL. ## Step 3: Exchange the code for a refresh token ```bash cURL theme={null} curl -X POST "https://cnhi-oauth2-helper.withleaf.io/fieldops/get_token" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-cnhi-client-id", "client_secret": "your-cnhi-client-secret", "response_url": "https://your-app.com/callback?code=abc123", "client_redirect_url": "https://your-app.com/callback" }' ``` ```python Python theme={null} tokens = requests.post( "https://cnhi-oauth2-helper.withleaf.io/fieldops/get_token", json={ "client_id": "your-cnhi-client-id", "client_secret": "your-cnhi-client-secret", "response_url": "https://your-app.com/callback?code=abc123", "client_redirect_url": "https://your-app.com/callback" } ).json() refresh_token = tokens["refresh_token"] ``` ```javascript JavaScript theme={null} const tokenRes = await fetch( "https://cnhi-oauth2-helper.withleaf.io/fieldops/get_token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "your-cnhi-client-id", client_secret: "your-cnhi-client-secret", response_url: "https://your-app.com/callback?code=abc123", client_redirect_url: "https://your-app.com/callback", }), } ); const { refresh_token } = await tokenRes.json(); ``` The `response_url` is the full URL the grower was redirected to, including the `code` parameter that CNHI appended. ## Step 4: Attach credentials to the Leaf user ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": "your-cnhi-client-id", "clientSecret": "your-cnhi-client-secret", "subscriptionKey": "your-cnhi-subscription-key", "refreshToken": "the-refresh-token-from-step-3", "clientEnvironment": "STAGE" }' ``` ```python Python theme={null} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/cnhi-field-ops-credentials", headers={"Authorization": f"Bearer {leaf_token}"}, json={ "clientId": "your-cnhi-client-id", "clientSecret": "your-cnhi-client-secret", "subscriptionKey": "your-cnhi-subscription-key", "refreshToken": refresh_token, "clientEnvironment": "STAGE" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/cnhi-field-ops-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ clientId: "your-cnhi-client-id", clientSecret: "your-cnhi-client-secret", subscriptionKey: "your-cnhi-subscription-key", refreshToken: refresh_token, clientEnvironment: "STAGE", }), } ); console.log(await res.json()); ``` Set `clientEnvironment` to `PRODUCTION` once your CNHI FieldOps application is promoted to production. Leaf handles token refresh automatically. ## Step 5: Confirm the credentials are attached ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" ``` If this worked, Leaf returns the CNHI FieldOps credential object for the Leaf user. ## What you built You connected CNHI FieldOps to a Leaf user. Leaf now syncs field boundaries, machine files, and field operations from Case IH and New Holland equipment. Query the data through the [field operations](/guides/tutorials/field-operations-quickstart) endpoints. For the credentials schema and management endpoints, see the [CNHI FieldOps provider guide](/providers/cnhi-fieldops) and the [provider credentials API reference](/api-reference/providers). # Connect to John Deere Operations Center API Source: https://docs.withleaf.io/guides/tutorials/connect-john-deere Connect John Deere Operations Center to Leaf. Create a developer app, complete the OAuth flow, and attach credentials to a Leaf user. This tutorial walks through the John Deere OAuth flow: creating a developer application, obtaining user tokens, and attaching credentials to a Leaf user so Leaf can sync field boundaries, machine files, and field operations from John Deere Operations Center. The fastest way to connect John Deere is through [Magic Link](/components/magic-link) or [Leaf Link](/components/leaf-link), which handle the OAuth UI for you. This tutorial is for developers who need to build the OAuth flow into their own application. ## Before you start * A Leaf account with API credentials and a valid token. * A Leaf user created (see [Field Operations Quickstart](/guides/tutorials/field-operations-quickstart)). * A John Deere developer account. Register at [developer.deere.com](https://developer.deere.com/) if you don't have one. ## Step 1: Create a John Deere application Sign in to the [John Deere developer portal](https://developer.deere.com/), navigate to **My Applications**, and click **Create Application**. Fill in your company information and select the APIs you need: | Leaf product | Required John Deere APIs | | -------------------------------- | -------------------------------- | | Base | Organizations, Webhook | | Field boundaries | Clients, Farm, Field, Boundaries | | Machine files / field operations | Field Operations, Files | | Prescriptions (beta) | Files | | Machines (beta) | Machines | After creation, note your **App ID** and **Shared Secret**. John Deere may take some time to approve API access. ### Enable webhook permissions Leaf uses John Deere webhooks to receive real-time notifications when grower data changes, which means faster data delivery than polling alone. You need to explicitly request webhook access for your application: 1. In **My Applications**, select your application and click **Request Access**. 2. Navigate to **Precision Tech → Application**, open the **Operations Center - Webhook** menu. 3. Check both **Webhook Read** and **Webhook Write**. 4. Click **Submit Request**. Approval typically takes a few hours. Without webhook permissions, Leaf still syncs data on a polling schedule (at least every 24 hours), but new data won't arrive in near-real-time. ## Step 2: Get the authorization URL Redirect the grower to John Deere's OAuth consent page. Leaf provides a helper endpoint that constructs the URL: ```bash cURL theme={null} curl -X POST "https://johndeere-oauth2-helper.withleaf.io/get_url" \ -H "Content-Type: application/json" \ -d '{ "clientKey": "your-john-deere-app-id", "clientSecret": "your-john-deere-secret", "clientRedirectUrl": "https://your-app.com/callback" }' ``` ```python Python theme={null} import requests response = requests.post( "https://johndeere-oauth2-helper.withleaf.io/get_url", json={ "clientKey": "your-john-deere-app-id", "clientSecret": "your-john-deere-secret", "clientRedirectUrl": "https://your-app.com/callback" } ) auth_url = response.json()["url"] ``` ```javascript JavaScript theme={null} const res = await fetch("https://johndeere-oauth2-helper.withleaf.io/get_url", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientKey: "your-john-deere-app-id", clientSecret: "your-john-deere-secret", clientRedirectUrl: "https://your-app.com/callback", }), }); const { url } = await res.json(); ``` Send the grower to the returned URL. After they authorize, John Deere redirects them to your `clientRedirectUrl` with a `code` parameter in the URL. ## Step 3: Exchange the code for tokens Use the redirect URL (including the `code`) to get the user's tokens: ```bash cURL theme={null} curl -X POST "https://johndeere-oauth2-helper.withleaf.io/get_token" \ -H "Content-Type: application/json" \ -d '{ "clientKey": "your-john-deere-app-id", "clientSecret": "your-john-deere-secret", "responseUrl": "https://your-app.com/callback?code=abc123", "clientRedirectUrl": "https://your-app.com/callback" }' ``` ```python Python theme={null} tokens = requests.post( "https://johndeere-oauth2-helper.withleaf.io/get_token", json={ "clientKey": "your-john-deere-app-id", "clientSecret": "your-john-deere-secret", "responseUrl": "https://your-app.com/callback?code=abc123", "clientRedirectUrl": "https://your-app.com/callback" } ).json() refresh_token = tokens["refreshToken"] ``` ```javascript JavaScript theme={null} const tokenRes = await fetch( "https://johndeere-oauth2-helper.withleaf.io/get_token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientKey: "your-john-deere-app-id", clientSecret: "your-john-deere-secret", responseUrl: "https://your-app.com/callback?code=abc123", clientRedirectUrl: "https://your-app.com/callback", }), } ); const { refreshToken } = await tokenRes.json(); ``` Save the `refreshToken`. You'll attach it to the Leaf user in the next step. ## Step 4: Grant organization access The grower must explicitly share their organizations with your application. Redirect them to: ``` https://connections.deere.com/connections/{yourJohnDeereAppId}/select-organizations?redirect_uri={yourRedirectUrl} ``` On this page, the grower toggles on the organizations they want to share. Leaf can only sync data from allowed organizations. ## Step 5: Attach credentials to the Leaf user ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientKey": "your-john-deere-app-id", "clientSecret": "your-john-deere-secret", "refreshToken": "the-refresh-token-from-step-3", "clientEnvironment": "STAGE" }' ``` ```python Python theme={null} headers = {"Authorization": f"Bearer {leaf_token}"} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/john-deere-credentials", headers=headers, json={ "clientKey": "your-john-deere-app-id", "clientSecret": "your-john-deere-secret", "refreshToken": refresh_token, "clientEnvironment": "STAGE" } ) print(response.json()) ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/john-deere-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ clientKey: "your-john-deere-app-id", clientSecret: "your-john-deere-secret", refreshToken: refreshToken, clientEnvironment: "STAGE", }), } ); console.log(await res.json()); ``` Set `clientEnvironment` to `STAGE` for sandbox testing or `PRODUCTION` once John Deere has approved your app for production. Leaf manages token refresh automatically after this point. ## Step 6: Confirm the credentials are attached ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" ``` If this worked, Leaf returns the John Deere credential object for the Leaf user. John Deere sandbox rules: one test account, no more than five connected organizations, under 150,000 API calls/month, and no longer than 18 months in sandbox. Violating these can get your app revoked. ## What you built You completed the John Deere OAuth flow and attached credentials to a Leaf user. Leaf now syncs field boundaries, machine files, and field operations from John Deere Operations Center. Data will appear in [field operations](/guides/tutorials/field-operations-quickstart) queries once processing completes. For more details on the credentials schema and endpoints, see the [John Deere provider guide](/providers/john-deere) and the [provider credentials API reference](/api-reference/providers). # Connect to Stara Telemetry API Source: https://docs.withleaf.io/guides/tutorials/connect-stara Connect Stara Telemetry to Leaf. Get API credentials, obtain tokens, and attach them to a Leaf user for standardized field data. This tutorial walks through connecting Stara's telemetry platform to Leaf. You'll get developer credentials from Stara, obtain API tokens, and attach them to a Leaf user. Stara provides field names, field boundaries, planting operations, as-applied operations, and machine information. [Magic Link](/components/magic-link) and [Leaf Link](/components/leaf-link) can handle Stara authentication for you. This tutorial covers the manual API flow. ## Before you start * A Leaf account with a valid API token. * A Leaf user created. * A Stara developer account. Contact Stara at [servicos@stara.com.br](mailto:servicos@stara.com.br) or +55 054 99706-7292 to request API access. They'll provide a username and password. ## Step 1: Get your API key Authenticate with Stara to get an API key: ```bash cURL theme={null} curl -X POST "https://v2apitelemetria.telemetriastara.com.br/autenticacao" \ -H "Content-Type: application/json" \ -d '{ "login": "your-stara-username", "password": "your-stara-password" }' ``` ```python Python theme={null} import requests response = requests.post( "https://v2apitelemetria.telemetriastara.com.br/autenticacao", json={ "login": "your-stara-username", "password": "your-stara-password" } ) api_key = response.json()["apiKey"] ``` ```javascript JavaScript theme={null} const res = await fetch( "https://v2apitelemetria.telemetriastara.com.br/autenticacao", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ login: "your-stara-username", password: "your-stara-password", }), } ); const { apiKey } = await res.json(); ``` ## Step 2: Get access and refresh tokens Use the API key to generate tokens: ```bash cURL theme={null} curl -X POST "https://v2apitelemetria.telemetriastara.com.br/token" \ -H "Content-Type: application/json" \ -d '{ "apiKey": "your-stara-api-key" }' ``` ```python Python theme={null} token_response = requests.post( "https://v2apitelemetria.telemetriastara.com.br/token", json={"apiKey": api_key} ) tokens = token_response.json() access_token = tokens["accessToken"] access_token_client = tokens["accessTokenClient"] refresh_token = tokens["refreshToken"] ``` ```javascript JavaScript theme={null} const tokenRes = await fetch( "https://v2apitelemetria.telemetriastara.com.br/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apiKey }), } ); const { accessToken, accessTokenClient, refreshToken } = await tokenRes.json(); ``` ## Step 3: Attach credentials to the Leaf user ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "apiKey": "the-api-key-from-step-1", "accessToken": "the-access-token-from-step-2", "accessTokenClient": "the-access-token-client-from-step-2", "refreshToken": "the-refresh-token-from-step-2" }' ``` ```python Python theme={null} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/stara-credentials", headers={"Authorization": f"Bearer {leaf_token}"}, json={ "apiKey": api_key, "accessToken": access_token, "accessTokenClient": access_token_client, "refreshToken": refresh_token } ) print(response.json()) ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/stara-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ apiKey: apiKey, accessToken: accessToken, accessTokenClient: accessTokenClient, refreshToken: refreshToken, }), } ); console.log(await res.json()); ``` Leaf manages token refresh automatically and begins syncing data. ## Step 4: Confirm the credentials are attached Check the stored credentials for the Leaf user: ```bash cURL theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" ``` If this worked, Leaf returns the Stara credential object with `apiKey`, `accessToken`, `accessTokenClient`, and `refreshToken`. ## What you built You connected Stara Telemetry to a Leaf user. Leaf now syncs field names, boundaries, planting, and application data from Stara. Query the data through the [field operations quickstart](/guides/tutorials/field-operations-quickstart). If you don't have field boundaries set up yet, start with the [fields overview](/fields/overview) and [uploading boundaries](/fields/uploading-boundaries) docs. For the credentials schema and management endpoints, see the [Stara provider guide](/providers/stara) and the [provider credentials API reference](/api-reference/providers). # Connect to Trimble API Source: https://docs.withleaf.io/guides/tutorials/connect-trimble Connect Trimble Agriculture data to Leaf. Register a developer application, complete the OAuth flow, and attach credentials to a Leaf user. This tutorial walks through connecting Trimble Agriculture to Leaf so you can sync field boundaries, machine files, and field operations. You'll register a developer application, run the OAuth flow, and attach the resulting provider credentials to a Leaf user. [Magic Link](/components/magic-link) and [Leaf Link](/components/leaf-link) handle the OAuth UI for you. This tutorial covers the manual flow for developers building it into their own application. ## Before you start * A Leaf account with a valid API token. * A Leaf user created. * A Trimble developer account. Register at [agdeveloper.trimble.com](https://agdeveloper.trimble.com/log-in-or-register/). * API credentials requested through [Trimble's integration request page](https://agriculture.trimble.com/en/partners/developer-resources/request-software-integration-api). Trimble sends you a Client ID and Client Secret after approval. ## Step 1: Get the authorization URL Leaf constructs the Trimble OAuth URL for you: ```bash cURL theme={null} curl -X POST "https://trimble-oauth2-helper.withleaf.io/get_url" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-trimble-client-id", "client_secret": "your-trimble-client-secret", "client_redirect_url": "https://your-app.com/callback" }' ``` ```python Python theme={null} import requests response = requests.post( "https://trimble-oauth2-helper.withleaf.io/get_url", json={ "client_id": "your-trimble-client-id", "client_secret": "your-trimble-client-secret", "client_redirect_url": "https://your-app.com/callback" } ) auth_url = response.json()["url"] ``` ```javascript JavaScript theme={null} const res = await fetch("https://trimble-oauth2-helper.withleaf.io/get_url", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "your-trimble-client-id", client_secret: "your-trimble-client-secret", client_redirect_url: "https://your-app.com/callback", }), }); const { url } = await res.json(); ``` The redirect URL must match what Trimble authorized during your application registration in Step 1. Trailing slashes can cause failures. Redirect the grower to the returned URL. After authentication, Trimble asks the grower to grant access to their organizations. If the grower skips this step, Leaf won't receive data from those organizations. You can ask them to manage access later through [provider organizations](/providers/organizations). After authorization, Trimble redirects to your `client_redirect_url` with a `code` parameter. ## Step 2: Exchange the code for tokens ```bash cURL theme={null} curl -X POST "https://trimble-oauth2-helper.withleaf.io/get_token" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-trimble-client-id", "client_secret": "your-trimble-client-secret", "code": "the-code-from-redirect", "client_redirect_url": "https://your-app.com/callback" }' ``` ```python Python theme={null} tokens = requests.post( "https://trimble-oauth2-helper.withleaf.io/get_token", json={ "client_id": "your-trimble-client-id", "client_secret": "your-trimble-client-secret", "code": "the-code-from-redirect", "client_redirect_url": "https://your-app.com/callback" } ).json() access_token = tokens["accessToken"] refresh_token = tokens["refreshToken"] ``` ```javascript JavaScript theme={null} const tokenRes = await fetch( "https://trimble-oauth2-helper.withleaf.io/get_token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "your-trimble-client-id", client_secret: "your-trimble-client-secret", code: "the-code-from-redirect", client_redirect_url: "https://your-app.com/callback", }), } ); const { accessToken, refreshToken } = await tokenRes.json(); ``` ## Step 3: Attach credentials to the Leaf user ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": "your-trimble-client-id", "clientSecret": "your-trimble-client-secret", "accessToken": "the-access-token-from-step-2", "refreshToken": "the-refresh-token-from-step-2" }' ``` ```python Python theme={null} response = requests.post( f"https://api.withleaf.io/services/usermanagement/api/users/{leaf_user_id}/trimble-credentials", headers={"Authorization": f"Bearer {leaf_token}"}, json={ "clientId": "your-trimble-client-id", "clientSecret": "your-trimble-client-secret", "accessToken": access_token, "refreshToken": refresh_token } ) print(response.json()) ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/usermanagement/api/users/${leafUserId}/trimble-credentials`, { method: "POST", headers: { Authorization: `Bearer ${leafToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ clientId: "your-trimble-client-id", clientSecret: "your-trimble-client-secret", accessToken: accessToken, refreshToken: refreshToken, }), } ); console.log(await res.json()); ``` Leaf handles token refresh automatically after this point. ## Step 4: Confirm the credentials are attached ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials" \ -H "Authorization: Bearer YOUR_LEAF_TOKEN" ``` If this worked, Leaf returns the Trimble credential object for the Leaf user. ## What you built You connected Trimble Agriculture to a Leaf user. Leaf now syncs field boundaries, machine files, and field operations from Trimble. Query the data through the [field operations](/guides/tutorials/field-operations-quickstart) endpoints. For the credentials schema and management endpoints, see the [Trimble provider guide](/providers/trimble) and the [provider credentials API reference](/api-reference/providers). # Connect Leaf's MCP Server in Cursor Source: https://docs.withleaf.io/guides/tutorials/cursor-mcp Connect Leaf's MCP server to Cursor IDE to query field data, pull harvest operations, and explore the Leaf API using natural language and tool calls. Leaf provides a Model Context Protocol (MCP) server that lets you interact with the Leaf API directly from Cursor. You can list users, query fields, pull operations, check weather data, and browse API documentation without leaving your editor. ## Before you start * A Leaf account with API credentials. * Cursor IDE installed (version 0.40+ with MCP support). * A valid Leaf API token. Get one by authenticating: ```bash theme={null} curl -X POST "https://api.withleaf.io/api/authenticate" \ -H "Content-Type: application/json" \ -d '{"username": "your-email@example.com", "password": "your-password"}' ``` Save the `id_token` from the response. ## Step 1: Add the MCP server to Cursor Open Cursor Settings (Cmd+, on Mac, Ctrl+, on Windows/Linux) and navigate to **MCP Servers**. Click **Add new MCP Server** and configure it: * **Name:** `leaf-mcp` * **Type:** `HTTP` * **URL:** `https://mcp.withleaf.io/mcp` Under **Headers**, add: ``` LEAF_TOKEN: your-leaf-api-token ``` Your Leaf token expires periodically. If you get authentication errors, generate a fresh token and update the header. ## Step 2: Verify the connection Open a new Cursor chat (Agent mode) and ask: ``` List the available Leaf API documentation ``` Cursor calls the `get_docs_index` tool and returns a list of all documentation topics available through the MCP server. If you see the index, the connection is working. ## Step 3: List your Leaf users ``` Show me all Leaf users in my account ``` Cursor calls `list_users` and returns the paginated list of Leaf users under your API owner account. Each user has an `id` you'll use in subsequent queries. ## Step 4: Query fields for a user ``` List fields for Leaf user ``` Replace `` with an actual UUID from the previous step. Cursor calls `list_fields` and returns fields with their boundaries, providers, and metadata. ## Step 5: Pull harvest operations ``` Show me harvested operations for Leaf user ``` Cursor calls `list_operations` with `operationType=harvested` and returns the harvest operations. Each operation includes `startTime`, `endTime`, `fieldId`, and links to its summary and GeoJSON data. To dig into a specific operation: ``` Get the summary for operation ``` This calls `get_operation_summary` and returns aggregated stats like total area, average yield, and crop type. ## Step 6: Explore additional tools The MCP server exposes tools for the full Leaf API surface: | Category | Tools | | ------------- | -------------------------------------------------------------------------------------------------------------- | | Users | `list_users` | | Fields | `list_fields`, `get_field`, `get_field_boundary` | | Operations | `list_operations`, `get_operation`, `get_operation_summary`, `get_operation_units` | | Machine files | `list_files`, `get_file`, `get_file_summary`, `get_file_status`, `get_file_units` | | Batches | `list_batches`, `get_batch`, `get_batch_status` | | Weather | `get_weather_forecast_field_daily`, `get_weather_historical_field_daily`, and hourly/lat-lon variants | | Billing | `list_billing_contracts`, `get_contract_consumption` | | Configuration | `get_api_owner_configuration`, `get_leaf_user_configuration` | | Credentials | `get_john_deere_credentials_events`, `get_climate_fieldview_credentials_events`, `get_cnhi_credentials_events` | | Documentation | `get_docs_index`, `get_leaf_doc` | You can ask Cursor natural-language questions and it will pick the right tool. For example: * "What's the weather forecast for field X?" * "Show me the processing status of file Y" * "Pull the API documentation for field operations" ## What you built You connected Leaf's MCP server to Cursor and used it to query Leaf users, fields, and harvest operations through natural language. The MCP server gives you API access from your editor without writing HTTP requests, which is useful for exploration, debugging, and rapid prototyping. For the same workflow in Claude Code, see [Connect Leaf's MCP Server in Claude Code](/guides/tutorials/claude-code-mcp). # Field Operations Quickstart Source: https://docs.withleaf.io/guides/tutorials/field-operations-quickstart Create a Leaf user, connect a data provider, and retrieve standardized field operation data from planting, harvest, application, and tillage activities. This tutorial takes you from a fresh Leaf account to viewing standardized field operation data. You'll authenticate, create a Leaf user, connect a provider, and query the resulting field operations. ## Before you start * A Leaf account with API credentials (email and password). [Register here](https://withleaf.io/account/get-a-demo) if you don't have one. * At least one provider account (John Deere, Climate FieldView, CNHi, etc.) with farm data, or test data from Leaf's sample user. * cURL, Python 3, or Node.js installed. ## Step 1: Get your Leaf token Authenticate with your Leaf credentials to get a bearer token. This token is required for all subsequent API calls. ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/api/authenticate" \ -H "Content-Type: application/json" \ -d '{ "username": "your-email@example.com", "password": "your-password" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.withleaf.io/api/authenticate", json={ "username": "your-email@example.com", "password": "your-password" } ) token = response.json()["id_token"] print(token) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.withleaf.io/api/authenticate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "your-email@example.com", password: "your-password", }), }); const { id_token } = await response.json(); console.log(id_token); ``` Save the `id_token` from the response. You'll pass it as a `Bearer` token in the `Authorization` header on every request. ## Step 2: Create a Leaf user A Leaf user represents a single data owner (typically a grower or consultant). All provider credentials and data are organized under Leaf users. Your account includes a sample Leaf user with pre-loaded data. You can skip this step if you just want to explore the sample data. Query `GET /users` to find it. ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/usermanagement/api/users" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Jane Farmer", "email": "jane@example.com" }' ``` ```python Python theme={null} import requests headers = {"Authorization": f"Bearer {token}"} response = requests.post( "https://api.withleaf.io/services/usermanagement/api/users", headers=headers, json={"name": "Jane Farmer", "email": "jane@example.com"} ) leaf_user = response.json() leaf_user_id = leaf_user["id"] print(f"Leaf user created: {leaf_user_id}") ``` ```javascript JavaScript theme={null} const res = await fetch( "https://api.withleaf.io/services/usermanagement/api/users", { method: "POST", headers: { Authorization: `Bearer ${id_token}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Jane Farmer", email: "jane@example.com" }), } ); const leafUser = await res.json(); console.log("Leaf user created:", leafUser.id); ``` Save the `id` from the response. This is your `leafUserId`. ## Step 3: Connect a provider The fastest way to connect a provider is with Magic Link. It generates a shareable URL that walks the grower through OAuth without any front-end code on your side. First, configure your provider application credentials using the [provider setup guides](/providers/overview). Then create a Magic Link: ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/widgets/api/magic-link/users/YOUR_LEAF_USER_ID/provider" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "expiresIn": 900 }' ``` ```python Python theme={null} response = requests.post( f"https://api.withleaf.io/services/widgets/api/magic-link/users/{leaf_user_id}/provider", headers=headers, json={ "expiresIn": 900 } ) magic_link = response.json()["link"] print(f"Send this to the grower: {magic_link}") ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/widgets/api/magic-link/users/${leafUser.id}/provider`, { method: "POST", headers: { Authorization: `Bearer ${id_token}`, "Content-Type": "application/json", }, body: JSON.stringify({ expiresIn: 900, }), } ); const { link } = await res.json(); console.log("Send this to the grower:", link); ``` Send the returned `link` to the grower. They authenticate with their provider account, and Leaf stores the credentials automatically. To restrict which providers appear, add `"allowedProviders": ["JohnDeere", "ClimateFieldView"]` to the request body. ### Confirm the connection worked After the grower completes the Magic Link flow, verify that the provider now appears for the Leaf user: ```bash cURL theme={null} curl "https://api.withleaf.io/services/integrations/api/resources?leafUserId=YOUR_LEAF_USER_ID" \ -H "Authorization: Bearer YOUR_TOKEN" ``` If this worked, the response includes a summary for the connected provider. If the provider does not appear yet, wait a few minutes and try again. For direct API credential attachment (without Magic Link), see the provider-specific tutorials: [John Deere](/guides/tutorials/connect-john-deere), [Climate FieldView](/guides/tutorials/connect-climate-fieldview), [CNHi](/guides/tutorials/connect-cnhi), [Trimble](/guides/tutorials/connect-trimble), [AgLeader](/guides/tutorials/connect-agleader), [Stara](/guides/tutorials/connect-stara). ## Step 4: Wait for data processing Once a provider is connected, Leaf begins syncing machine files. The sync-to-operations pipeline works like this: 1. **Machine files** are fetched from the provider and converted to Leaf's standard canonical format (available as GeoJSON or GeoParquet). 2. **Field operations** are created by merging machine files that overlap in time and field boundary. You can set up [alerts](/alerts/overview) to get notified when processing completes instead of polling. ## Step 5: Query your field operations Once processing finishes, query the operations for your Leaf user. ```bash cURL theme={null} curl "https://api.withleaf.io/services/operations/api/operations?leafUserId=YOUR_LEAF_USER_ID" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} response = requests.get( "https://api.withleaf.io/services/operations/api/operations", headers=headers, params={"leafUserId": leaf_user_id} ) operations = response.json() for op in operations: print(f"{op['id']} - {op['operationType']} - {op['startTime']}") ``` ```javascript JavaScript theme={null} const res = await fetch( `https://api.withleaf.io/services/operations/api/operations?leafUserId=${leafUser.id}`, { headers: { Authorization: `Bearer ${id_token}` } } ); const operations = await res.json(); operations.forEach((op) => console.log(`${op.id} - ${op.operationType} - ${op.startTime}`) ); ``` Each operation has an `operationType` of `planted`, `applied`, `harvested`, or `tillage`. ## Step 6: Get operation details For any operation, you can fetch the summary (aggregated stats) and the standard GeoJSON (point-level data). **Summary:** ```bash cURL theme={null} curl "https://api.withleaf.io/services/operations/api/operations/OPERATION_ID/summary" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} op_id = operations[0]["id"] summary = requests.get( f"https://api.withleaf.io/services/operations/api/operations/{op_id}/summary", headers=headers ).json() print(summary) ``` ```javascript JavaScript theme={null} const opId = operations[0].id; const summary = await fetch( `https://api.withleaf.io/services/operations/api/operations/${opId}/summary`, { headers: { Authorization: `Bearer ${id_token}` } } ).then((r) => r.json()); console.log(summary); ``` **Standard GeoJSON:** ```bash cURL theme={null} curl "https://api.withleaf.io/services/operations/api/operations/OPERATION_ID/standardGeojson" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} geojson = requests.get( f"https://api.withleaf.io/services/operations/api/operations/{op_id}/standardGeojson", headers=headers ).json() print(f"Features: {len(geojson['features'])}") ``` ```javascript JavaScript theme={null} const geojson = await fetch( `https://api.withleaf.io/services/operations/api/operations/${opId}/standardGeojson`, { headers: { Authorization: `Bearer ${id_token}` } } ).then((r) => r.json()); console.log("Features:", geojson.features.length); ``` The standard GeoJSON contains point-level data with normalized property names like `yieldVolume`, `seedRate`, `appliedRate`, etc. You can filter by `operationType`, `startTime`, `endTime`, and other parameters. ## What you built You now have a working pipeline that: * Authenticates with the Leaf API * Creates Leaf users to represent growers * Connects data providers via Magic Link * Retrieves standardized field operations across providers From here, you'll likely want to: * Set up [alerts](/alerts/overview) to react to new data automatically * Explore [machine file conversion](/machine-data/file-conversion) for lower-level file access * Review [configuration](/configuration/overview) to tune merge behavior and data cleaning * Try [manual file upload](/guides/tutorials/manual-file-upload) for thumb drive data # Upload and Process Machine Files Source: https://docs.withleaf.io/guides/tutorials/manual-file-upload Upload machine files to Leaf's batch API for conversion to standardized field operations. Supports .dat, .cn1, ISOXML, .agt, .shp, and more. Leaf's manual file upload lets you submit machine data files directly instead of pulling them from a provider API. This is useful for growers who export data to USB thumb drives or for testing with local files. Leaf accepts `.zip` files containing proprietary formats like `.dat`, `.cn1`, ISOXML, `.agt`, `.shp`, `.2020`, `.ilf`, and more, and converts them into a standard canonical format, accessible as either GeoJSON or GeoParquet. ## Before you start * A Leaf account with a valid API token. * A Leaf user created. * One or more machine data files packaged as `.zip`. If you have nested zips, Leaf unzips recursively. * cURL, Python 3, or Node.js installed. ## Step 1: Get your Leaf token ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/api/authenticate" \ -H "Content-Type: application/json" \ -d '{ "username": "your-email@example.com", "password": "your-password" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.withleaf.io/api/authenticate", json={"username": "your-email@example.com", "password": "your-password"} ) token = response.json()["id_token"] headers = {"Authorization": f"Bearer {token}"} ``` ```javascript JavaScript theme={null} const res = await fetch("https://api.withleaf.io/api/authenticate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "your-email@example.com", password: "your-password", }), }); const { id_token } = await res.json(); ``` ## Step 2: Upload the file POST your `.zip` file to the batch upload endpoint. If you don't know the file format, set `provider` to `Other` and Leaf detects it automatically. ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/operations/api/batch" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "file=@/path/to/your-data.zip" \ -F "leafUserId=YOUR_LEAF_USER_ID" \ -F "provider=Other" ``` ```python Python theme={null} leaf_user_id = "your-leaf-user-id" with open("/path/to/your-data.zip", "rb") as f: response = requests.post( "https://api.withleaf.io/services/operations/api/batch", headers=headers, files={"file": f}, data={"leafUserId": leaf_user_id, "provider": "Other"} ) batch = response.json() batch_id = batch["id"] print(f"Batch created: {batch_id}") ``` ```javascript JavaScript theme={null} const formData = new FormData(); formData.append("file", fileBlob, "your-data.zip"); formData.append("leafUserId", leafUserId); formData.append("provider", "Other"); const res = await fetch( "https://api.withleaf.io/services/operations/api/batch", { method: "POST", headers: { Authorization: `Bearer ${id_token}` }, body: formData, } ); const batch = await res.json(); console.log("Batch created:", batch.id); ``` The response includes a `batch_id`. If the zip contains multiple files, Leaf discovers and processes each one individually. ## Step 3: Check batch status Poll the batch endpoint until the status is `PROCESSED`: ```bash cURL theme={null} curl "https://api.withleaf.io/services/operations/api/batch/BATCH_ID" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} import time while True: batch_status = requests.get( f"https://api.withleaf.io/services/operations/api/batch/{batch_id}", headers=headers ).json() status = batch_status["status"] print(f"Status: {status}") if status in ("PROCESSED", "FAILED"): break time.sleep(10) file_ids = batch_status.get("leafFiles", []) print(f"Files discovered: {len(file_ids)}") ``` ```javascript JavaScript theme={null} let batchStatus; do { await new Promise((r) => setTimeout(r, 10000)); batchStatus = await fetch( `https://api.withleaf.io/services/operations/api/batch/${batch.id}`, { headers: { Authorization: `Bearer ${id_token}` } } ).then((r) => r.json()); console.log("Status:", batchStatus.status); } while (!["PROCESSED", "FAILED"].includes(batchStatus.status)); console.log("Files:", batchStatus.leafFiles?.length); ``` The `leafFiles` array contains the IDs of all machine files extracted and processed from your upload. ## Step 4: Retrieve converted files Use the file IDs to get the standardized data: **Get file metadata:** ```bash cURL theme={null} curl "https://api.withleaf.io/services/operations/api/files/FILE_ID" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} file_id = file_ids[0] file_data = requests.get( f"https://api.withleaf.io/services/operations/api/files/{file_id}", headers=headers ).json() print(f"Operation type: {file_data['operationType']}") print(f"Provider: {file_data['provider']}") ``` ```javascript JavaScript theme={null} const fileId = batchStatus.leafFiles[0]; const fileData = await fetch( `https://api.withleaf.io/services/operations/api/files/${fileId}`, { headers: { Authorization: `Bearer ${id_token}` } } ).then((r) => r.json()); console.log("Operation type:", fileData.operationType); ``` **Get the file summary** (aggregated statistics): ```bash cURL theme={null} curl "https://api.withleaf.io/services/operations/api/files/FILE_ID/summary" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} summary = requests.get( f"https://api.withleaf.io/services/operations/api/files/{file_id}/summary", headers=headers ).json() print(summary) ``` ```javascript JavaScript theme={null} const summary = await fetch( `https://api.withleaf.io/services/operations/api/files/${fileId}/summary`, { headers: { Authorization: `Bearer ${id_token}` } } ).then((r) => r.json()); console.log(summary); ``` **Get processing status** (for troubleshooting): ```bash cURL theme={null} curl "https://api.withleaf.io/services/operations/api/files/FILE_ID/status" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} status = requests.get( f"https://api.withleaf.io/services/operations/api/files/{file_id}/status", headers=headers ).json() for step, info in status.items(): print(f"{step}: {info['status']}") ``` ```javascript JavaScript theme={null} const status = await fetch( `https://api.withleaf.io/services/operations/api/files/${fileId}/status`, { headers: { Authorization: `Bearer ${id_token}` } } ).then((r) => r.json()); Object.entries(status).forEach(([step, info]) => console.log(`${step}: ${info.status}`) ); ``` The status endpoint shows the processing state for each pipeline step: `standardGeojson`, `cleanupGeojson`, `summary`, `units`, etc. ## Step 5: Troubleshoot failures If some files fail, check the batch status for details: ```python theme={null} batch_detail = requests.get( f"https://api.withleaf.io/services/operations/api/batch/{batch_id}/status", headers=headers ).json() for file_info in batch_detail: if file_info.get("status") == "failed": print(f"File {file_info['id']}: {file_info.get('message')}") ``` Common failure reasons: unsupported file format, corrupted zip, or empty data files. ## What you built You uploaded machine data files to Leaf and retrieved standardized output (GeoJSON or GeoParquet — same data, your choice of format). These files go through the same conversion pipeline as provider-synced data, so you can use them interchangeably in your application. Once Leaf has both machine files and field boundaries for a Leaf user, it automatically merges overlapping files into [field operations](/guides/tutorials/field-operations-quickstart). For a no-code upload experience, use [Leaf Link](/components/leaf-link) or [Magic Link](/components/magic-link). For the full endpoint reference, see the [machine files API reference](/api-reference/files). # Migrate from CNHI AFS Connect to CNHI FieldOps Source: https://docs.withleaf.io/guides/tutorials/migrate-cnhi-to-fieldops Move your existing CNHI (AFS Connect) integration to CNHI FieldOps. Covers what changed, new credentials setup, and running both providers in parallel. This guide covers how to migrate an existing CNHI (AFS Connect) integration to CNHI FieldOps. You can migrate at your own pace — both providers run in parallel, and no growers are disrupted until you choose to switch them. ## What changed CNHI FieldOps is CNH Industrial's current API platform. The data you receive through Leaf — field operations, machine files, boundaries — is the same. What changes is how the connection is established. | Aspect | CNHI (AFS Connect) | CNHI FieldOps | | ------------------------------------ | ---------------------------------- | ------------------------------------------- | | Leaf credential path | `cnhi-credentials` | `cnhi-field-ops-credentials` | | Leaf OAuth helper | `/get_url`, `/get_token` | `/fieldops/get_url`, `/fieldops/get_token` | | Magic Link / Leaf Link provider name | `CNHI` | `CNHIFieldOps` | | Subscription keys | Legacy keys | New keys required (legacy keys do not work) | | Change detection | Polling (Leaf checks periodically) | Webhooks (CNH pushes changes to Leaf) | | Token format | Opaque | JWT | The FieldOps API also supports webhook-based change notifications. Instead of Leaf polling CNH on a schedule, CNH pushes events to Leaf when new data is available. This reduces the delay between when a grower uploads data from their equipment and when Leaf processes it. ## What you need before migrating * A CNH developer account at [develop.cnh.com](https://develop.cnh.com/) (company-domain email required — Gmail, Hotmail, and other generic domains are not supported). * A new FieldOps application registered in the developer portal. This produces a new `clientId`, `clientSecret`, and `subscriptionKey`. Existing CNHI credentials cannot be reused. * Each grower must re-authorize through the FieldOps OAuth flow. Existing refresh tokens do not carry over. * Growers must have the **Farm Manager** title in their FieldOps account and must have [logged into the FieldOps portal](https://develop.cnh.com/get-started/fieldops-portal) at least once. ## Migration approach: run both in parallel Leaf treats CNHI and CNHI FieldOps as separate providers. A single Leaf user can hold both `cnhi-credentials` and `cnhi-field-ops-credentials` at the same time. This means you can migrate growers one at a time without disrupting anyone. The recommended approach: 1. Register your CNHI FieldOps app keys with Leaf: ```bash theme={null} curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"clientId": "your-fieldops-client-id", "clientSecret": "your-fieldops-secret", "subscriptionKey": "your-fieldops-key"}' \ "https://api.withleaf.io/services/usermanagement/api/app-keys/CNHIFieldOps/your-app-name/PRODUCTION" ``` 2. For each grower you want to migrate: * Run the FieldOps OAuth flow (via the [Leaf helper](/guides/tutorials/connect-cnhi-fieldops) or [Magic Link](/components/magic-link)) * Attach `cnhi-field-ops-credentials` to the Leaf user * Verify data syncs correctly * Once confirmed, delete the old `cnhi-credentials` from the Leaf user 3. Growers you haven't migrated yet continue working on the legacy CNHI provider with no disruption. While both credentials are attached, Leaf syncs from both providers. This may produce duplicate fields or files until you remove the old credential. We recommend verifying the FieldOps connection, then promptly deleting the legacy credential. ### Identifying FieldOps data in the API Data from CNHI FieldOps appears in the same Leaf endpoints (fields, files, operations) as legacy CNHI data. The `provider` field distinguishes the source: * Legacy CNHI: `"provider": "CNHI"` * CNHI FieldOps: `"provider": "CNHIFieldOps"` You can filter queries by provider (e.g., `?provider=CNHIFieldOps`) to see only FieldOps data. During the parallel-run period, the same physical field may appear twice with different `provider` values. ## Abbreviated setup steps For the full walkthrough with code examples in cURL, Python, and JavaScript, see [Connect CNHI FieldOps](/guides/tutorials/connect-cnhi-fieldops). The key steps: 1. **Get the authorization URL** — `POST https://cnhi-oauth2-helper.withleaf.io/fieldops/get_url` with `client_id`, `client_redirect_url`, and `production` flag. 2. **Redirect the grower** — they log in and consent. 3. **Exchange the code for a refresh token** — `POST https://cnhi-oauth2-helper.withleaf.io/fieldops/get_token` with `client_id`, `client_secret`, `client_redirect_url`, and `response_url`. 4. **Attach credentials to the Leaf user** — `POST /users/{leafUserId}/cnhi-field-ops-credentials` with `clientId`, `clientSecret`, `subscriptionKey`, `refreshToken`, and `clientEnvironment`. 5. **Confirm** — `GET /users/{leafUserId}/cnhi-field-ops-credentials`. ## Updating Magic Link and Leaf Link If you use Magic Link or Leaf Link widgets to onboard growers: 1. Register your FieldOps app keys with Leaf: ```bash theme={null} curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"clientId": "your-fieldops-client-id", "clientSecret": "your-fieldops-secret", "subscriptionKey": "your-fieldops-key"}' \ "https://api.withleaf.io/services/usermanagement/api/app-keys/CNHIFieldOps/your-app-name/PRODUCTION" ``` 2. Add `https://widget.withleaf.io` as a callback URL in your CNHI FieldOps application on the [CNH developer portal](https://develop.cnh.com/). 3. Update your `allowedProviders` to include `"CNHIFieldOps"`. During the transition, you can include both `"CNHI"` and `"CNHIFieldOps"` so growers can connect through either provider. ## What to do next * [CNHI FieldOps provider guide](/providers/cnhi-fieldops) — Credentials schema, endpoints, and troubleshooting. * [Connect CNHI FieldOps tutorial](/guides/tutorials/connect-cnhi-fieldops) — Full step-by-step with code examples. * [Provider credentials API reference](/api-reference/providers) — Endpoint reference for all providers. # Get Satellite Imagery for Your Fields Source: https://docs.withleaf.io/guides/tutorials/satellite-imagery Use Leaf's crop monitoring API to get satellite imagery for your fields. Create fields, retrieve NDVI, NDRE, and RGB images from Sentinel-2 and Planet. Leaf's crop monitoring API delivers satellite imagery from Sentinel-2 and Planet, processed and clipped to your field boundaries. You get NDVI, NDRE, RGB compositions, and raw multiband GeoTIFFs. This tutorial walks through creating a satellite field and retrieving images. ## Before you start * A Leaf account with a valid API token. * A field boundary (as a GeoJSON MultiPolygon). You can use coordinates from any source. Leaf users and configurations are not required for satellite imagery. You only need authentication and a field geometry. ## Step 1: Get your Leaf token ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/api/authenticate" \ -H "Content-Type: application/json" \ -d '{ "username": "your-email@example.com", "password": "your-password" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.withleaf.io/api/authenticate", json={"username": "your-email@example.com", "password": "your-password"} ) token = response.json()["id_token"] ``` ```javascript JavaScript theme={null} const res = await fetch("https://api.withleaf.io/api/authenticate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "your-email@example.com", password: "your-password", }), }); const { id_token } = await res.json(); ``` ## Step 2: Create a satellite field POST a field boundary to the crop monitoring endpoint. The geometry must be a MultiPolygon. ```bash cURL theme={null} curl -X POST "https://api.withleaf.io/services/satellite/api/fields" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "externalId": "my-field-001", "providers": [ { "name": "sentinel", "startDate": "2025-01-01" } ], "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48821, 41.77088], [-93.48738, 41.77088], [-93.48738, 41.77137], [-93.48821, 41.77137] ]]] } }' ``` ```python Python theme={null} headers = {"Authorization": f"Bearer {token}"} field = requests.post( "https://api.withleaf.io/services/satellite/api/fields", headers=headers, json={ "externalId": "my-field-001", "providers": [{"name": "sentinel", "startDate": "2025-01-01"}], "geometry": { "type": "MultiPolygon", "coordinates": [[[ [-93.48821, 41.77137], [-93.48821, 41.77088], [-93.48738, 41.77088], [-93.48738, 41.77137], [-93.48821, 41.77137] ]]] } } ).json() satellite_field_id = field["id"] print(f"Satellite field created: {satellite_field_id}") ``` ```javascript JavaScript theme={null} const field = await fetch( "https://api.withleaf.io/services/satellite/api/fields", { method: "POST", headers: { Authorization: `Bearer ${id_token}`, "Content-Type": "application/json", }, body: JSON.stringify({ externalId: "my-field-001", providers: [{ name: "sentinel", startDate: "2025-01-01" }], geometry: { type: "MultiPolygon", coordinates: [[[ [-93.48821, 41.77137], [-93.48821, 41.77088], [-93.48738, 41.77088], [-93.48738, 41.77137], [-93.48821, 41.77137], ]]], }, }), } ).then((r) => r.json()); console.log("Satellite field created:", field.id); ``` Key details: * The `name` field in providers must be exactly `"sentinel"` or `"planet"`. Planet requires activation from Leaf support. * The `startDate` tells Leaf how far back to fetch historical images. * You can request multiple providers and asset types for the same field. After creation, Leaf begins fetching and processing images from the start date forward. ## Step 3: Retrieve images Query the images available for your satellite field: ```bash cURL theme={null} curl "https://api.withleaf.io/services/satellite/api/fields/SATELLITE_FIELD_ID/processes" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```python Python theme={null} images = requests.get( f"https://api.withleaf.io/services/satellite/api/fields/{satellite_field_id}/processes", headers=headers ).json() for img in images: print(f"Date: {img['date']}") print(f" NDVI PNG: {img.get('ndvi', {}).get('png')}") print(f" NDVI TIF: {img.get('ndvi', {}).get('tif')}") print(f" RGB PNG: {img.get('rgb', {}).get('png')}") ``` ```javascript JavaScript theme={null} const images = await fetch( `https://api.withleaf.io/services/satellite/api/fields/${field.id}/processes`, { headers: { Authorization: `Bearer ${id_token}` } } ).then((r) => r.json()); images.forEach((img) => { console.log(`Date: ${img.date}`); console.log(` NDVI: ${img.ndvi?.png}`); console.log(` RGB: ${img.rgb?.png}`); }); ``` Each image process includes: * **Multiband GeoTIFF** (the original satellite data) * **RGB** composition (GeoTIFF and PNG) * **NDVI** (GeoTIFF and PNG) * **NDRE** (GeoTIFF and PNG) The download URLs require authentication. Pass your Leaf token in the `Authorization` header when downloading. You can filter results by date range and cloud coverage: ```text theme={null} ?startDate=2025-06-01&endDate=2025-09-01&maxCloudCoverage=50 ``` If cloud coverage is 50% or less, the image is included but cloud shadows may still be visible. There's no way to filter out the shadow itself. ## Step 4: Set up alerts Instead of polling for new images, set up an alert to get notified when new satellite imagery is processed: ```bash theme={null} curl -X POST "https://api.withleaf.io/services/alerts/api/alerts" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "newSatelliteImage", "url": "https://your-app.com/webhook/satellite" }' ``` Leaf sends a POST to your webhook URL with the satellite field ID, process ID, and timestamp whenever a new image finishes processing. ## What you built You created a satellite field with Leaf's crop monitoring API and retrieved NDVI, NDRE, and RGB imagery. Leaf continuously monitors the field and processes new images as they become available from Sentinel-2 (roughly every 5 days) or Planet (daily with activation). For the full endpoint reference, see the [satellite API reference](/api-reference/satellite). To use satellite data in ArcGIS, see the [ArcGIS integration tutorial](/guides/tutorials/arcgis-integration). # Leaf Documentation Source: https://docs.withleaf.io/index Access clean, standardized, and aggregated farm data from all major agricultural sources.

The AI-native API for farm data

Leaf’s API delivers clean, analysis-ready farm data for people and agents, including boundaries, operations, imagery, and weather from every major agricultural system.

Leaf crop monitoring satellite imagery

Learn the platform

Get up to speed with Leaf's core concepts, authentication flow, and data model before you start building.

Get your Leaf token and start making API calls with Bearer auth. Walk through a step-by-step tutorial with cURL, Python, and JavaScript examples. Look up terms like Leaf User, Provider, Field Operations, and more.

Explore the API

Leaf's API covers the full agricultural data lifecycle — from connecting provider accounts to retrieving satellite imagery.

Create, sync, and manage field boundaries across providers. Access planting, harvest, application, and tillage data from all major brands. Get daily and hourly forecasts and historical weather by field or coordinates. Connect John Deere, CNHi, CNHI FieldOps, Climate FieldView, Trimble, and more. Get notified in real time when new data arrives. Analyze normalized farm data across space and time with SQL.
# Irrigation Overview Source: https://docs.withleaf.io/irrigation/overview Pull irrigation data from Lindsay and Valley through the Leaf API: as-applied activities, irrigated field summaries, and equipment like pivots and sensors. Leaf connects to irrigation providers (Lindsay FieldNET and Valley) to pull equipment data, as-applied irrigation activities, and field-level irrigation summaries into a single API. ## Data types ### Irrigation equipment Equipment records represent physical irrigation infrastructure: pivots, lateral moves, and sensors. Each equipment record belongs to a Leaf user and includes the equipment's location and type. ### As-applied irrigation As-applied irrigation data represents the actual irrigation events reported by the equipment. Each record is summarized by day and split by depth, showing the amount of water applied across a multipolygon geometry. ### Irrigated field Irrigated field records show how an irrigation event intersects with a specific field boundary. The irrigation geometries are clipped to the field boundary, so you see only the portion of irrigation that falls within the field. Different water depths across zones or angles are represented as separate multipolygon geometries within the record. ## Endpoints Base URL: `https://api.withleaf.io/services/irrigation/api` | Action | Method | Path | | ------------------------------- | ------ | ---------------------------------------------------------------- | | List irrigation equipment | GET | `/users/{leafUserId}/irrigation-equipment` | | Get irrigation equipment | GET | `/users/{leafUserId}/irrigation-equipment/{id}` | | List as-applied irrigation | GET | `/users/{leafUserId}/irrigation/applied-irrigation` | | Get an irrigation activity | GET | `/users/{leafUserId}/irrigation/applied-irrigation/{id}` | | List irrigated fields | GET | `/users/{leafUserId}/irrigation/fields` | | Get an irrigated field | GET | `/users/{leafUserId}/irrigation/fields/{fieldId}` | | Get an irrigated field activity | GET | `/users/{leafUserId}/irrigation/fields/{fieldId}/irrigated/{id}` | ## Configuration The `irrigationProcessingRange` configuration controls how far back Leaf fetches irrigation data from providers. Default is 12 months. See [Configuration](/configuration/overview#irrigation) for details. ## Alerts Two irrigation-specific events are available: * `newIrrigationActivity` fires when new as-applied data arrives from a provider. * `newFieldIrrigationActivity` fires when irrigation data is matched to a field boundary. See [Alert Events](/alerts/events#irrigation-events) for payload schemas. ## What to do next * [Configuration](/configuration/overview) to adjust `irrigationProcessingRange`. * [Irrigation API Reference](/api-reference/irrigation) for full endpoint details. # Leaf Lake Overview Source: https://docs.withleaf.io/leaf-lake/overview SQL-queryable data lake for normalized agronomic data. Query planting, harvest, application, and tillage operations alongside USDA soil data using standard SQL. Leaf Lake gives you direct SQL access to all of the agronomic data flowing through Leaf. Instead of downloading GeoJSON files and processing them locally, you send a SQL query to a single endpoint and get back JSON rows. The data in Leaf Lake comes from the same pipeline that produces Leaf's standardGeojson files. Every point from every machine file — planting, application, harvest, tillage — is available as a row in the `points` table. Leaf also provides built-in environmental datasets: USDA SSURGO soil survey polygons and US state/county boundaries, ready for spatial joins. ## How it works 1. **Authenticate** — Get a Bearer token from the Leaf API using your existing credentials. Leaf Lake uses the same authentication as all other Leaf endpoints. 2. **Write SQL** — Compose a query against the available tables. Leaf Lake uses BigQuery SQL with aggregations, CTEs, JOINs, and spatial functions. 3. **POST the query** — Send the SQL as the request body to the query endpoint. The response is a JSON array of result rows. All queries are automatically scoped to the authenticated API owner. You only see your own data. Leaf Lake is read-only — only SELECT queries (including WITH, ORDER BY, UNION) are supported. No writes, updates, or schema changes. ``` Authenticate → Write SQL → POST /query → JSON rows ``` ## Data freshness Leaf Lake's ingestion pipeline runs continuously. New machine data typically appears in the `points` table within minutes of being processed by the standard Leaf pipeline. If a file has been converted and a standardGeojson exists, the data is on its way into the lake. ## Available tables | Table | Contents | Geometry type | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------- | | `points` | All operation data (planted, applied, harvested, tillage) from Leaf's machine data pipeline | Point | | `fields` | Leaf field boundaries synced from the Fields API | Polygon | | `ssurgo` | USDA Soil Survey Geographic Database — soil map units with properties like drainage class, farmland classification, and soil name | Polygon | | `states_counties` | US state and county boundaries | Polygon | ### The `points` table Each row is a single data point from a machine file. The columns match the properties in Leaf's standardGeojson output, which vary by operation type. See [Sample Output](/machine-data/sample-output) for the full property reference. Each point includes a `fieldIds` column containing the Leaf field UUID(s) associated with that point. You can filter directly by field ID instead of providing a WKT boundary polygon. Points also carry `leafUserId` for user-level filtering. Common columns across all operation types: `operationType`, `geometry`, `timestamp`, `crop`, `fieldIds`, `leafUserId`, `area`, `distance`, `heading`, `speed`, `elevation`, `equipmentWidth`, `recordingStatus`, `sectionId`, `machinery`. Type-specific columns include `seedRate` and `variety` for planted, `wetMassPerArea` and `harvestMoisture` for harvested, `appliedRate` and `products` for applied, and `tillageDepthTarget` for tillage. The full breakdown is covered in [Querying Leaf Lake](/leaf-lake/querying). ### The `fields` table Leaf field boundaries, synced from the [Fields API](/api-reference/fields). Each row is a field with its merged boundary geometry. Automatically scoped to your API owner account. Use this table to spatially join operations with field boundaries without hardcoding WKT. ### The `ssurgo` table SSURGO data from the USDA provides soil map unit polygons with attributes like soil name, drainage class, and farmland classification. Use spatial joins to relate operation data to soil properties — for example, calculating average yield by soil type within a field. ### The `states_counties` table US state and county boundary polygons. Useful for grouping operations by geography or filtering to a region. ## Spatial SQL functions Leaf Lake uses BigQuery SQL with geography extensions. Geometry columns are stored as text and converted using `ST_GeogFromText`. | Function | Description | | ------------------------------- | --------------------------------------------------- | | `ST_GeogFromText('WKT')` | Creates a geography from Well-Known Text | | `ST_Intersects(geog1, geog2)` | Returns true if two geographies share any space | | `ST_X(point)` / `ST_Y(point)` | Extracts longitude / latitude from a point | | `ST_Area(geog)` | Returns the area of a polygon in square meters | | `ST_Intersection(geog1, geog2)` | Returns the geography where two geographies overlap | | `ST_AsText(geog)` | Converts a geography to Well-Known Text | | `ST_ConvexHull(geog)` | Returns the convex hull of a geography | | `ST_Union_Agg(geog)` | Aggregates multiple geographies into one | ## Common use cases * **Yield by soil type**: Join harvest points with SSURGO polygons to see how yield varies across soil map units within a field. * **Seed variety comparison**: Query planting data grouped by variety and year to compare seed rate distributions and coverage. * **Multi-year trend analysis**: Aggregate harvest data across seasons to track yield trends without downloading and processing files. * **Product performance**: Join application and harvest data to compare treated vs. control areas within a field. * **Instant reprocessing**: Change filtering criteria (outlier thresholds, moisture cutoffs) by modifying the SQL WHERE clause instead of reprocessing files through the pipeline. * **Regional analysis**: Join operations with state/county boundaries to aggregate data by geography. ## What to do next * [Querying Leaf Lake](/leaf-lake/querying) — Schema reference and example queries for each operation type, SSURGO, and state/county data. * [API Reference: Leaf Lake](/api-reference/leaf-lake) — Endpoint details, request/response format, and code examples. * [Authentication](/getting-started/authentication) — How to get a Bearer token for API requests. * [Sample Output](/machine-data/sample-output) — Full property reference for standardGeojson point data by operation type. # Querying Leaf Lake Source: https://docs.withleaf.io/leaf-lake/querying Schema reference and example queries for Leaf Lake: the points table for all operation types, SSURGO soil data, and state/county boundaries. Leaf Lake exposes your agronomic data through four tables. This page documents the schema of each table and provides example queries you can adapt. Leaf Lake uses BigQuery SQL. All queries are automatically scoped to your API owner account — you only see data belonging to your Leaf users. ## The `points` table Each row represents a single data point from a machine file. The columns match the standardGeojson point properties produced by Leaf's machine data pipeline. See [Sample Output](/machine-data/sample-output) for the full property definitions. ### Common columns (all operation types) | Column | Type | Description | | ----------------- | --------- | ----------------------------------------------------------- | | `operationType` | string | `planted`, `harvested`, `applied`, or `tillage` | | `geometry` | string | Geographic location as WKT (convert with `ST_GeogFromText`) | | `timestamp` | timestamp | When the data was recorded | | `crop` | string | Normalized crop name | | `fieldIds` | array | Leaf field UUID(s) associated with this point | | `leafUserId` | string | The Leaf user who owns this data | | `area` | float | Area covered at this point | | `distance` | float | Distance traveled | | `heading` | float | Machine heading in degrees | | `speed` | float | Machine speed | | `elevation` | float | Elevation | | `equipmentWidth` | float | Width of the implement | | `recordingStatus` | string | Recording status (e.g. `On`) | | `sectionId` | int | Section/row identifier | | `machinery` | list | Machine and implement names | | `variety` | string | Crop variety name | ### Planted columns | Column | Type | Description | | ---------------- | ----- | ---------------------------- | | `seedRate` | int | Seeds per area at this point | | `seedRateTarget` | int | Target seed rate | | `seedDepth` | float | Planting depth | | `downForce` | float | Down force reading | | `singulation` | float | Meter singulation percentage | | `skips` | float | Skip percentage | | `doubles` | float | Double percentage | ### Harvested columns | Column | Type | Description | | ------------------ | ----- | ------------------------- | | `harvestMoisture` | float | Grain moisture percentage | | `wetMass` | float | Wet mass | | `wetMassPerArea` | float | Wet mass per area | | `wetVolume` | float | Wet volume | | `wetVolumePerArea` | float | Wet volume per area | | `dryMass` | float | Dry mass | | `dryMassPerArea` | float | Dry mass per area | | `dryVolume` | float | Dry volume | | `dryVolumePerArea` | float | Dry volume per area | ### Applied columns | Column | Type | Description | | ------------------- | ------ | ----------------------------------------------- | | `appliedRate` | float | Application rate at this point | | `appliedRateTarget` | float | Target application rate | | `products` | list | Array of product objects with `name` and `rate` | | `tankMixName` | string | Name of the tank mix | ### Tillage columns | Column | Type | Description | | -------------------- | ----- | ---------------------- | | `tillageDepthTarget` | float | Target tillage depth | | `tillageDepthActual` | float | Actual tillage depth | | `tillType` | list | Tillage implement type | *** ## Filtering by field Filter points to a specific Leaf field using the `fieldIds` column. No WKT boundary polygon needed. ```sql theme={null} SELECT * FROM points WHERE operationType = 'harvested' AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) ``` You can also filter spatially using a WKT polygon with `ST_Intersects`, or join against the `fields` or `states_counties` tables. See the examples below. *** ## Operations query examples ### List operations on a field See what operation types, crops, and date ranges exist for a field. ```sql theme={null} SELECT EXTRACT(YEAR FROM timestamp) AS op_year, operationType, crop, COUNT(*) AS point_count, MIN(timestamp) AS start_time, MAX(timestamp) AS end_time FROM points WHERE 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) GROUP BY EXTRACT(YEAR FROM timestamp), operationType, crop ORDER BY op_year, start_time ``` ### Planted: seed rate by variety and year ```sql theme={null} SELECT crop, EXTRACT(YEAR FROM timestamp) AS op_year, variety, COUNT(*) AS point_count, ROUND(MIN(seedRate), 0) AS min_seed_rate, ROUND(AVG(seedRate), 0) AS avg_seed_rate, ROUND(MAX(seedRate), 0) AS max_seed_rate FROM points WHERE operationType = 'planted' AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) GROUP BY crop, variety, EXTRACT(YEAR FROM timestamp) ORDER BY op_year ``` ### Planted: spatial point data Retrieve individual planting points with coordinates for mapping. ```sql theme={null} SELECT crop, EXTRACT(YEAR FROM timestamp) AS planting_year, seedRate, ST_X(ST_GeogFromText(geometry)) AS lon, ST_Y(ST_GeogFromText(geometry)) AS lat FROM points WHERE operationType = 'planted' AND seedRate IS NOT NULL AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) ``` ### Applied: summary by product and year ```sql theme={null} SELECT EXTRACT(YEAR FROM timestamp) AS op_year, tankMixName, COUNT(*) AS point_count, MIN(timestamp) AS start_time, MAX(timestamp) AS end_time FROM points WHERE operationType = 'applied' AND timestamp >= '2025-01-01' AND timestamp <= '2025-12-31' AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) GROUP BY EXTRACT(YEAR FROM timestamp), tankMixName ORDER BY op_year, start_time ``` ### Applied: point data with product details ```sql theme={null} SELECT TO_JSON_STRING(products) AS products_json, appliedRate, timestamp, ST_X(ST_GeogFromText(geometry)) AS lon, ST_Y(ST_GeogFromText(geometry)) AS lat FROM points WHERE operationType = 'applied' AND timestamp >= '2025-01-01' AND timestamp <= '2025-12-31' AND appliedRate IS NOT NULL AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) ``` ### Harvested: yield statistics Leaf Lake stores yield in metric units. Convert to bu/ac using crop-specific factors (corn: 62.77 kg/ha per bu/ac, soybeans: 67.25 kg/ha per bu/ac). ```sql theme={null} SELECT crop, EXTRACT(YEAR FROM timestamp) AS op_year, COUNT(*) AS point_count, ROUND(AVG(wetMassPerArea) / 62.77, 1) AS avg_yield_bu_ac, ROUND(MIN(wetMassPerArea) / 62.77, 1) AS min_yield_bu_ac, ROUND(MAX(wetMassPerArea) / 62.77, 1) AS max_yield_bu_ac, ROUND(STDDEV(wetMassPerArea) / 62.77, 1) AS std_yield_bu_ac FROM points WHERE operationType = 'harvested' AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) GROUP BY crop, EXTRACT(YEAR FROM timestamp) ORDER BY op_year DESC ``` ### Harvested: outlier filtering with CTEs Filter yield data to remove statistical outliers. Adjust the standard deviation threshold to control how aggressively outliers are removed. This replaces pipeline-level reprocessing — change the threshold and re-run the query. ```sql theme={null} WITH stats AS ( SELECT crop, EXTRACT(YEAR FROM timestamp) AS harvest_year, AVG(wetMassPerArea) AS mean_yield, STDDEV(wetMassPerArea) AS std_yield FROM points WHERE operationType = 'harvested' AND wetMassPerArea IS NOT NULL AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) GROUP BY crop, EXTRACT(YEAR FROM timestamp) ) SELECT p.crop, EXTRACT(YEAR FROM p.timestamp) AS harvest_year, p.wetMassPerArea / 62.77 AS yield_bu_ac, p.harvestMoisture AS moisture, ST_X(ST_GeogFromText(p.geometry)) AS lon, ST_Y(ST_GeogFromText(p.geometry)) AS lat FROM points p JOIN stats s ON p.crop = s.crop AND EXTRACT(YEAR FROM p.timestamp) = s.harvest_year WHERE p.operationType = 'harvested' AND p.wetMassPerArea IS NOT NULL AND 'YOUR_FIELD_UUID' IN UNNEST(p.fieldIds) AND p.wetMassPerArea BETWEEN s.mean_yield - 3 * s.std_yield AND s.mean_yield + 3 * s.std_yield ``` ### Tillage: basic query ```sql theme={null} SELECT EXTRACT(YEAR FROM timestamp) AS op_year, tillType, COUNT(*) AS point_count, ROUND(AVG(tillageDepthTarget), 1) AS avg_depth_target, MIN(timestamp) AS start_time, MAX(timestamp) AS end_time FROM points WHERE operationType = 'tillage' AND 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) GROUP BY EXTRACT(YEAR FROM timestamp), tillType ORDER BY op_year ``` *** ## The `fields` table Leaf field boundaries synced from the [Fields API](/api-reference/fields). Automatically scoped to your API owner account. | Column | Type | Description | | ------------------ | ------------- | ---------------------------------------------- | | `id` | string (UUID) | The Leaf field ID | | `field_geometry` | geography | Union of all boundary geometries for the field | | `boundary_id_list` | array | List of boundary IDs | | `updated_date` | date | Last update date | ### Join operations with field boundaries Use the `fields` table to spatially join points with field boundaries instead of hardcoding WKT polygons. ```sql theme={null} SELECT f.id AS field_id, p.operationType, p.crop, COUNT(*) AS point_count FROM points p JOIN fields f ON ST_Intersects(ST_GeogFromText(p.geometry), f.field_geometry) WHERE f.id = 'YOUR_FIELD_UUID' GROUP BY f.id, p.operationType, p.crop ``` *** ## The `ssurgo` table The SSURGO (Soil Survey Geographic Database) table contains USDA soil map unit polygons with associated attributes. | Column | Type | Description | | -------------------- | ------ | -------------------------------------------------------------------- | | `mukey` | string | Map unit key — unique identifier for the soil map unit | | `soil_name` | string | Soil map unit name | | `county` | string | County name | | `drainage_class` | string | Soil drainage classification (e.g. "Well drained", "Poorly drained") | | `farmland_class` | string | Farmland classification (e.g. "Prime farmland") | | `hydric_rating` | string | Hydric soil rating | | `flooding_frequency` | string | Flooding frequency class | | `ponding_frequency` | string | Ponding frequency class | | `aws0_150` | float | Available water storage, 0-150 cm depth | | `aws0_999` | float | Available water storage, full soil profile | | `geometry` | string | Boundary of the soil map unit (convert with `ST_GeogFromText`) | ### Query soil units intersecting a field Derive the field extent from your points using a CTE, then intersect with SSURGO. ```sql theme={null} WITH field_envelope AS ( SELECT ST_ConvexHull(ST_Union_Agg(ST_GeogFromText(geometry))) AS field_geom FROM points WHERE 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) ) SELECT mukey, soil_name, county, drainage_class, farmland_class, ROUND(ST_Area(ST_Intersection( ST_GeogFromText(ssurgo.geometry), fe.field_geom )) / 4046.86, 2) AS acres, ST_AsText(ST_Intersection( ST_GeogFromText(ssurgo.geometry), fe.field_geom )) AS clipped_geometry FROM ssurgo CROSS JOIN field_envelope fe WHERE ST_Intersects(ST_GeogFromText(ssurgo.geometry), fe.field_geom) ORDER BY mukey ASC ``` ### Join harvest data with soil types Calculate average yield by soil map unit within a field. This is a spatial join between the `points` table and `ssurgo`. ```sql theme={null} WITH field_envelope AS ( SELECT ST_ConvexHull(ST_Union_Agg(ST_GeogFromText(geometry))) AS field_geom FROM points WHERE 'YOUR_FIELD_UUID' IN UNNEST(fieldIds) ), field_soils AS ( SELECT mukey, soil_name, drainage_class, farmland_class, ssurgo.geometry AS soil_geom FROM ssurgo CROSS JOIN field_envelope fe WHERE ST_Intersects(ST_GeogFromText(ssurgo.geometry), fe.field_geom) ), yield_by_soil AS ( SELECT s.mukey, AVG(p.wetMassPerArea) AS avg_yield, COUNT(*) AS harvest_points FROM points p JOIN field_soils s ON ST_Intersects(ST_GeogFromText(s.soil_geom), ST_GeogFromText(p.geometry)) WHERE p.operationType = 'harvested' AND p.timestamp >= '2025-01-01' AND p.timestamp <= '2025-12-31' AND 'YOUR_FIELD_UUID' IN UNNEST(p.fieldIds) GROUP BY s.mukey ) SELECT s.mukey, s.soil_name, s.drainage_class, s.farmland_class, ROUND(COALESCE(y.avg_yield, 0) / 62.77, 1) AS avg_yield_bu_ac, y.harvest_points FROM field_soils s LEFT JOIN yield_by_soil y ON s.mukey = y.mukey ORDER BY avg_yield_bu_ac DESC ``` *** ## The `states_counties` table US state and county boundary polygons for geographic grouping and filtering. | Column | Type | Description | | ------------ | --------- | ---------------------------- | | `STATE` | string | State code (e.g. `IL`, `IA`) | | `COUNTYNAME` | string | County name | | `geometry` | geography | State or county boundary | ### Filter operations by county ```sql theme={null} SELECT c.STATE, c.COUNTYNAME, EXTRACT(YEAR FROM p.timestamp) AS op_year, p.operationType, p.crop, COUNT(*) AS point_count FROM points p JOIN states_counties c ON ST_Intersects(ST_GeogFromText(p.geometry), c.geometry) WHERE c.STATE = 'IL' AND c.COUNTYNAME = 'La Salle' GROUP BY c.STATE, c.COUNTYNAME, EXTRACT(YEAR FROM p.timestamp), p.operationType, p.crop ORDER BY op_year ``` *** ## What to do next * [Leaf Lake Overview](/leaf-lake/overview) — Product description and key concepts. * [API Reference: Leaf Lake](/api-reference/leaf-lake) — Endpoint details, request/response format, and code examples. * [Sample Output](/machine-data/sample-output) — Full standardGeojson property reference by operation type. * [Units](/machine-data/units) — Unit reference for all numeric properties. # Leaf Users Source: https://docs.withleaf.io/leaf-users/overview Leaf users are the central organizing concept in the API. Each holds provider credentials, and all fields, machine files, and field operations belong to one. A Leaf user is the entity that holds provider credentials and owns all the data that flows from those connections. Every field, machine file, field operation, and satellite image in Leaf belongs to exactly one Leaf user. When you create a Leaf user and attach provider credentials, Leaf begins syncing data from that provider automatically. In practice, most implementations create one Leaf user per grower, and that's the simplest way to think about it. But a Leaf user is really defined by the credentials attached to it, not by a strict 1:1 relationship with a single grower. A John Deere Operations Center account, for example, may contain data from multiple organizations and multiple growers (called "clients" in John Deere's terminology). If you connect that account to a single Leaf user, all of that data -- across all organizations and clients -- lands under that one Leaf user. Leaf tracks the provider-side grower as a property on individual files and fields, but does not treat it as a separate entity in the hierarchy. ## How Leaf users fit into the hierarchy The account structure is: **API owner → Leaf users → Data.** Your API owner is the top-level Leaf account, identified by an email address. Beneath it, each Leaf user acts as an isolated container. Fields, machine files, field operations, and provider credentials all live under a specific Leaf user. Data does not cross Leaf user boundaries within an API owner, though Leaf users can be shared across API owners using [Leaf Connect](/components/leaf-connect). ```mermaid theme={null} graph TD APIOwner["API Owner"] --> LeafUserA["Leaf User A"] APIOwner --> LeafUserB["Leaf User B"] LeafUserA --> CredsA["Provider Credentials"] LeafUserA --> FieldsA["Fields & Boundaries"] LeafUserA --> FilesA["Machine Files"] LeafUserA --> OpsA["Field Operations"] LeafUserB --> CredsB["Provider Credentials"] LeafUserB --> FieldsB["Fields & Boundaries"] LeafUserB --> FilesB["Machine Files"] LeafUserB --> OpsB["Field Operations"] ``` For the full terminology and data pipeline, see [Core Concepts](/getting-started/core-concepts). ## Leaf users and growers The most common pattern is one Leaf user per grower, and that's the recommended starting point. But it's worth understanding what happens when the provider-side account doesn't line up neatly with a single grower. With John Deere, a single Operations Center account can span multiple organizations and multiple clients (John Deere's term for growers). If you connect that account to one Leaf user, all of that data -- across all organizations and clients -- lands under that one Leaf user. Leaf tracks the provider-side grower as a property on individual files and fields (the `name` attribute on growers comes directly from the client name at John Deere), but does not treat it as a separate entity in the hierarchy. You can control what actually gets synced. `organizationDataSync` controls which John Deere organizations Leaf processes -- set it to `SELECTED_ONLY` to pick specific ones instead of syncing everything the account has access to. `customDataSync` controls which fields are fully processed versus held in preview mode. Together, these let you narrow the scope of a broad provider connection without needing to split it across multiple Leaf users. See [Provider Organizations](/providers/organizations) and [Configuration](/configuration/overview) for details. Other providers work similarly in principle: the credentials you attach determine what data flows in, and all of it lands under the Leaf user that holds those credentials. If your use case requires a different mapping than one Leaf user per grower (for example, one per region or per farm), talk to Customer Success before committing to an architecture. The choice affects billing, data isolation, and configuration granularity. ## Mapping Leaf users to your own system The `externalId` field on a Leaf user lets you store your own internal user ID, making it straightforward to match Leaf users back to accounts in your system. You can filter the [GET /users](/api-reference/users#get-all-leaf-users) endpoint by `externalId` to look up a Leaf user from your own identifier. ## Connecting providers A Leaf user can be connected to multiple providers (John Deere, Climate FieldView, CNHi, Trimble, AgLeader, etc.), but only one credential set per provider. Once credentials are attached, Leaf begins syncing data from that provider for the Leaf user -- pulling field boundaries, machine files, and creating field operations based on your [configurations](/configuration/overview). There are two ways to connect a grower's provider account to a Leaf user: * **Via the API** -- your application manages the OAuth flow with the provider and passes the resulting credentials to Leaf. See [Connecting Providers](/providers/overview) for details on each provider. * **Via Leaf widgets** -- [Magic Link](/components/magic-link) is a hosted web page you send to growers; they click it and authorize their provider account without touching your API. [Leaf Link](/components/leaf-link) is an embeddable React or Angular component that does the same thing inside your own UI. ## Configuration inheritance Configurations control how Leaf ingests, processes, and outputs data. They cascade from the API owner down to Leaf users: any setting on the API owner applies to all Leaf users unless a specific Leaf user has its own override. Configuration changes are not retroactive -- they only affect data processed after the change. This means you can set sensible defaults at the API owner level (processing range, filtered GeoJSON, custom data sync) and override them selectively for specific growers. See [Configuration](/configuration/overview) for the full list of settings. ## Account structure patterns **Standard pattern**: one API owner per environment (test, staging, production), one Leaf user per grower. This is the most common setup. It keeps each grower's data isolated, maps cleanly to billing (usage is tracked per Leaf user), and lets you apply per-grower configuration overrides when needed. **Managed service provider pattern**: if you serve multiple client organizations, consider a separate API owner per client (per environment). Each client's API owner holds Leaf users for that client's growers. This gives you billing isolation between clients and lets you apply entirely different configurations per client org. Do not repeatedly connect the same large provider account during development. Each connection syncs data and consumes your testing acre allotment. Use `customDataSync` to limit processing to specific fields, and `organizationDataSync` set to `SELECTED_ONLY` to control which John Deere organizations Leaf processes. See [Configuration](/configuration/overview) for details on both settings. ## Things to watch for **John Deere organization scope.** John Deere accounts often have access to many organizations. By default, Leaf syncs data from all of them. If you only need a subset, set `organizationDataSync` to `SELECTED_ONLY` before connecting the account. See [Provider Organizations](/providers/organizations) for managing which organizations sync. **Use PATCH for simple profile changes.** The partial update endpoint (`PATCH /users/{id}`) lets you change profile fields like `name`, `email`, `phone`, `address`, or `externalId` without affecting provider credentials. Only the fields you send are updated. **PUT is a full replacement.** The full update endpoint (`PUT /users`) replaces the entire Leaf user object. If the existing user has provider credentials and you omit them from the request body, those credentials are removed. Always include credentials you want to keep. **Deletion is permanent.** Deleting a Leaf user removes all associated provider credentials and stops data syncing. This cannot be undone. ## What to do next * [Leaf Users API Reference](/api-reference/users) -- endpoint details for creating, reading, updating (full and partial), and deleting Leaf users. * [Connecting Providers](/providers/overview) -- how to attach provider credentials to a Leaf user. * [Configuration](/configuration/overview) -- control how Leaf processes data, at the API owner or Leaf user level. * [Quickstart](/getting-started/quickstart) -- walk through the full setup end to end. # Field Operations Source: https://docs.withleaf.io/machine-data/field-operations How Leaf merges converted machine files into field operations: the auto-merge process, operation structure, summaries, filtered GeoJSON, and operation images. Field operations are what you get when Leaf takes converted machine files and spatially allocates them to field boundaries. A field operation represents a real-world task — planting, harvesting, spraying, or tillage — performed on a specific field. This page explains how files become operations and what data is available on each operation. ## How files become operations Field operations are created only when `fieldOperationCreation` is enabled. A single field activity often spans many machine files. A corn harvest on one field might be represented by dozens or even hundreds of files from the provider. Leaf's auto-merge process handles this automatically: 1. Leaf groups machine files by operation type (`planted`, `harvested`, `applied`, `tillage`) and time proximity. 2. The grouped files are spatially intersected with field boundaries. 3. Files that overlap the same field and represent the same activity are merged into a single field operation. This works for both provider-connected files and manually uploaded files. Field boundaries are required. Without boundaries, Leaf converts machine files and produces file-level summaries but cannot create field operations. See [Uploading Boundaries](/fields/uploading-boundaries) or [Managing Fields](/fields/managing-fields) for creating and managing boundaries. ## Merge timing The merge process runs continuously. Operations are created or updated each time new files arrive or field boundary changes are made. Merging is computationally expensive and can take time for large datasets. ## Operation structure A field operation (`GET /operations/{id}`) contains: | Field | Description | | ----------------------- | ---------------------------------------------------------------------------------------------------------- | | `id` | Unique operation ID | | `leafUserId` | Owner Leaf user | | `apiOwnerUsername` | API owner | | `type` | `planted`, `harvested`, `applied`, or `tillage` | | `startTime` / `endTime` | Time range of the operation | | `updatedTime` | Last time the operation was modified | | `files` | List of machine file IDs that were merged into this operation | | `fields` | List of field objects with `id` and `coverage` (percentage of the field boundary covered by the operation) | | `providers` | List of source providers | ## Operation data outputs For each processed operation, Leaf produces: **Standard GeoJSON** (`/operations/{id}/standardGeojson`) — The merged point-level data in Leaf's standard schema. This is the full dataset. **Filtered GeoJSON** (`/operations/{id}/filteredGeojson`) — Filtered output is controlled by `operationsFilteredGeojson`, but the endpoint behavior is not just a simple enabled-or-404 switch. When a dedicated filtered output exists, this endpoint returns it. If not, Leaf can still return the cleaned standard GeoJSON instead. Low-speed points (\< 0.5 m/s) are removed, and for harvest operations statistical outliers can also be removed. **Images V2** (`/operations/{id}/imagesV2`) — Generated from the filtered GeoJSON when data filtering is enabled. Uses a fixed color ramp with 7 quantile-based classes. Returns extent and legend metadata alongside each image. If the filtered GeoJSON fails, images fall back to the standard GeoJSON. **GeoTIFF Images** (`/operations/{id}/geotiffImages`) — Georeferenced TIFF versions of the property maps. Requires both `operationsFilteredGeojson` and `operationsImageAsGeoTiff` to be enabled. **Summary** (`/operations/{id}/summary`) — Aggregate statistics and a geometry. **Units** (`/operations/{id}/units`) — Map of property names to their unit strings. ## Operation summary The summary is a GeoJSON Feature containing aggregate properties and a MultiPolygon geometry representing the operation's spatial coverage. ### Properties available by operation type **All operation types** include: `operationType`, `startTime`, `endTime`, `totalArea`, `totalDistance`, `elevation`, `machinery`, `originalOperationType`, `originalOperationData`. **Harvest** adds: `crop`, `harvestMoisture`, `wetMass`, `totalWetMass`, `wetMassPerArea`, `wetVolume`, `totalWetVolume`, `wetVolumePerArea`, `dryMass`, `totalDryMass`, `dryMassPerArea`, `dryVolume`, `totalDryVolume`, `dryVolumePerArea`, `varieties`. `wetVolume`, `wetVolumePerArea`, `dryVolume`, and `dryVolumePerArea` are not available when the crop is sugarcane. **Planting** adds: `crop`, `seedRate`, `totalPlanted`, `varieties`, `singulation`, `downForce`, `skips`, `doubles`. **Application** adds: `appliedRate`, `totalApplied`, `products`, `tankMix`, `crop`. **Tillage** adds: `tillType`, `tillageDepthActual`, `tillageDepthTarget`. ### Stat properties Most numeric properties in the summary include `avg`, `min`, `max`, and `unit`. Total properties include `value` and `unit`. ### Machinery The `machinery` array contains objects with `name`, `type` (`machine` or `implement`), `serialNumber` (when available), and `brand`. ### Varieties and products Harvest and planting summaries include a `varieties` array with per-variety breakdowns of area, totals, and rates. Application summaries include a `products` array with per-product name, type (`Component` or `Carrier`), rate, total applied, and area. ## Machine file summary vs. operation summary **Machine file summary** — Statistics from a single file's point data, processed and cleaned by Leaf. Useful for understanding individual file contents. **Operation summary** — Statistics from all files merged to a field boundary. Represents the complete picture of what happened on that field. This is what most applications should use. ## Configurations that affect operations | Configuration | Effect | | --------------------------- | ------------------------------------------------------------------------------- | | `operationsFilteredGeojson` | Enable filtered GeoJSON and V2 images | | `operationsRemoveOutliers` | Enable/disable outlier removal on harvest data | | `operationsOutliersLimit` | Standard deviation threshold for outlier removal (default: 3) | | `operationsImageAsGeoTiff` | Enable GeoTIFF image generation | | `fieldOperationCreation` | Enable/disable automatic creation of field operations from merged machine files | | `operationsMergeRange` | Time window for grouping files into the same operation | | `operationsMergeOverlap` | Minimum spatial overlap required to merge a file into a field | ## What to do next * [Sample Output](/machine-data/sample-output) — Full example responses for files and operations. * [Units](/machine-data/units) — Unit reference table for all numeric properties. * [File Conversion](/machine-data/file-conversion) — Details on the file-level processing pipeline. * [API Reference: Operations](/api-reference/operations) — Full endpoint reference for field operations. # File Conversion Source: https://docs.withleaf.io/machine-data/file-conversion How Leaf converts raw machine files into standard GeoJSON or GeoParquet, including pipeline stages, status tracking, summaries, and cleanup rules. This page covers what happens after Leaf receives a machine data file, whether from a provider connection or a manual upload. Every file passes through the same conversion pipeline, producing standardized point data and a summary. ## Pipeline stages Each file moves through these stages in order: 1. **originalFile** — The raw proprietary file as received. Stored for reference. 2. **rawGeojson** — The proprietary format is parsed into Leaf's raw GeoJSON representation. 3. **standardGeojson** — The point data is standardized to Leaf's public schema and units. This is the primary point-level output returned by the file resource. 4. **filteredGeojson** — If filtered output is available, Leaf exposes a filtered point dataset as a separate public artifact rather than as a cleanup step name. 5. **summary** — Aggregate statistics (avg, min, max, totals) are calculated from the point data. Includes a geometry representing the spatial coverage. 6. **units** — A map of property names to their units for the file. 7. **propertiesPNGs** — PNG images generated from numeric properties. 8. **zippedPNGs** — A zip bundle containing the generated PNG images. Each stage runs independently and has its own status. ## Tracking file status Use `GET /files/{id}/status` to check where a file is in the pipeline. The response is a map keyed by the public step names returned by the API: ```json theme={null} { "originalFile": { "status": "processed", "message": "ok" }, "rawGeojson": { "status": "processed", "message": "ok" }, "standardGeojson": { "status": "processed", "message": "ok" }, "filteredGeojson": { "status": "processed", "message": "ok" }, "propertiesPNGs": { "status": "processed", "message": "ok" }, "zippedPNGs": { "status": "processed", "message": "ok" }, "summary": { "status": "processed", "message": "ok" }, "units": { "status": "processed", "message": "ok" } } ``` Some keys may be absent when a file has not reached that step or when that output is not produced for the file. Possible status values: `processed`, `failed`, `skipped`. If a stage fails, the `message` field contains details about what went wrong. ## File metadata A converted file (`GET /files/{id}`) includes: | Field | Description | | ------------------------- | ------------------------------------------------------------------------ | | `id` | Unique file ID | | `leafUserId` | Owner Leaf user | | `provider` | Source provider (or `Leaf` for manual uploads) | | `fileFormat` | Original format (e.g., `AGDATA`, `CN1`, `ISO11783`, `SHAPEFILE`) | | `fileName` | Original file name | | `operationType` | `planted`, `harvested`, `applied`, or `tillage` | | `downloadOriginalFile` | Authenticated download URL for the original proprietary file | | `downloadStandardGeojson` | Authenticated download URL for the standardized data | | `summary` | Embedded summary object with aggregate stats and geometry | | `fields` | Field IDs this file has been matched to | | `sourceFiles` | If this file was created by merging, the IDs of the source machine files | | `batchId` | If uploaded via batch API, the batch ID | Always use the `download`-prefixed URLs (e.g., `downloadStandardGeojson`) for file downloads. These point to `api.withleaf.io` and require authentication. Direct S3 URLs are being deprecated. ## File summary The summary (`GET /files/{id}/summary`) is a GeoJSON Feature with aggregate properties and a geometry representing the spatial coverage of the operation. The properties vary by operation type. Common properties across all types: | Property | Description | | ----------------------- | ----------------------------------------------- | | `operationType` | `planted`, `harvested`, `applied`, or `tillage` | | `startTime` / `endTime` | Time range of the operation | | `totalArea` | Total area covered | | `totalDistance` | Total distance traveled | | `elevation` | Elevation statistics | | `speed` | Speed statistics | | `crop` | Crop type(s) | | `machinery` | Machine and implement info | | `originalOperationType` | The operation type as reported by the provider | | `totalFuelUsed` | Total fuel consumed (when available) | The summary geometry is built from a buffer of the operation points, creating a polygon that approximates the coverage area. ## Data cleanup When `cleanupStandardGeojson` is enabled, Leaf removes points that fail these validation rules: | Property | Valid when | | -------------------- | ------------------ | | `wetMass` | > 0.0 | | `wetMassPerArea` | > 0.0 | | `wetVolume` | > 0.0 | | `wetVolumePerArea` | > 0.0 | | `harvestMoisture` | > 0.0 and \< 100.0 | | `appliedRate` | > 0.0 | | `seedRate` | > 0.0 | | `tillageDepthActual` | >= 0.0 | | `recordingStatus` | = "On" | | `crop` | != "unknown" | | `products` | >= 0.0 | You can customize which rules apply and their thresholds using the `cleanupRules` configuration. ## Filtered GeoJSON and outlier removal If `operationsFilteredGeojson` is enabled, Leaf produces an additional filtered version of the data. The filter removes: * Points with `speed` less than 0.5 m/s (all operation types) For harvest data, outlier removal can also be applied. Points where the harvested volume is more than 3 standard deviations from the mean are excluded. This threshold is configurable via `operationsOutliersLimit`. Disable outlier removal entirely with `operationsRemoveOutliers`. At the operation level, the filtered GeoJSON is used as the basis for generating V2 images, which use a fixed color ramp with 7 quantile-based classes. See [Field Operations](/machine-data/field-operations) for details on operation images. ## Processing timing Files from provider connections process immediately on first sync, then at least every 24 hours. Event-driven providers trigger processing sooner. Manually uploaded files begin processing as soon as Leaf receives the upload. Processing time depends on data volume. Expect initial results within a few minutes. Leaf archives files to slower storage after 180 days of no access. Contact support if you need to retrieve archived files or require a different retention period. ## What to do next * [Field Operations](/machine-data/field-operations) — How converted files are merged into field operations. * [Sample Output](/machine-data/sample-output) — Example file and operation responses. * [Units](/machine-data/units) — Unit reference for all numeric properties. # Machine Data Overview Source: https://docs.withleaf.io/machine-data/overview How Leaf processes raw machine data into standardized files and merged field operations: ingestion, conversion, cleanup, and allocation to field boundaries. Leaf turns raw machine data into standardized, analysis-ready field operations. Data comes in from two sources: direct provider connections (John Deere, Climate FieldView, CNHi, CNHI FieldOps, Ag Leader, Trimble, Raven Slingshot) and manual file uploads. Regardless of the source, every file moves through the same processing pipeline. ## The pipeline Machine data flows through four stages: 1. **Ingestion** — Leaf receives proprietary-format files, either pulled from a connected provider or uploaded manually via the batch API. 2. **Conversion** — Each file is converted from its native format into Leaf's standard canonical format (`standardGeojson`). Optionally, a `filteredGeojson` is produced with low-speed points and outliers removed. A summary with aggregate statistics (averages, min/max, totals) is generated for each file. 3. **Merge** — Files that belong to the same task (planting, harvest, application, or tillage) and overlap the same field boundary are grouped and merged into a single field operation. One operation may contain hundreds of machine files. 4. **Output** — Each field operation includes a standardGeojson, optional filteredGeojson, images (PNG or GeoTIFF), and a summary with geometry. ``` Provider / Upload → Machine Files → Field Operations (per file) (per field + task) ``` ## Key concepts **Machine file** — A single converted file. It has its own summary, standardGeojson, and unit map. Machine files are the building blocks of field operations. You can list and query them via the `/files` endpoints. **Field operation** — The result of merging one or more machine files against a field boundary. Operations represent a real-world activity on a specific field: planting corn, harvesting soybeans, spraying herbicide, or running a tillage pass. You can list and query them via the `/operations` endpoints. **Field boundaries are required for operations.** Without boundaries, Leaf still converts machine files and produces file-level summaries, but it cannot create field operations. Make sure boundaries exist before expecting operations to appear. **Operation types** — Leaf recognizes four types: `planted`, `harvested`, `applied`, and `tillage`. The data properties available on summaries and point data vary by type. ## Processing timing Files from provider connections are pulled immediately on first sync and then at least every 24 hours. Providers with event-driven APIs (like John Deere) trigger processing sooner when new data arrives. Manually uploaded files begin processing as soon as Leaf receives them. The merge process that creates field operations runs continuously. Merging is computationally expensive and may take time for large datasets. ## What each stage produces | Stage | Output | Endpoint | | ---------- | -------------------------------------------------------------------------- | ---------------------------------------------- | | Conversion | standardGeojson, filteredGeojson (optional), summary | `/files/{id}`, `/files/{id}/summary` | | Merge | Field operation with merged standardGeojson, summary with geometry, images | `/operations/{id}`, `/operations/{id}/summary` | ## Configuration Leaf's behavior is controlled by configurations set at the API owner or Leaf user level. Configurations affect which files are processed, how data is cleaned, and how operations are created. If a Leaf user has no custom configuration, it inherits the API owner's settings. Key configurations for machine data: * `cleanupStandardGeojson` — Remove invalid points from the standardGeojson during conversion. * `operationsFilteredGeojson` — Generate a filtered version of the operation data with low-speed and outlier points removed. * `operationsRemoveOutliers` — Control whether statistical outlier removal is applied to harvest data. * `outOfStandardOperations` — Allow processing of operations that don't meet standard validation criteria. These operations are marked as non-standard. Useful when you need data even from incomplete or edge-case files. * `operationsProcessingRange` — Limit how far back (in months) Leaf processes data from provider connections. * `customDataSync` — Restrict processing to specific fields. ## Common use cases * **Normalize multi-brand data**: Pull machine files from John Deere, Climate FieldView, CNHi, Trimble, and AgLeader into one standard canonical format without writing per-provider parsers. * **Build yield maps**: Retrieve harvest field operations with per-point yield data and summary statistics, then render them in your application. * **Collect planting records**: Access planting field operations with seed rate, variety, and population data for compliance reporting or agronomic analysis. * **Accept USB uploads**: Let growers upload machine files from monitors via the batch API or Magic Link when they don't use a cloud provider. ## What to do next * [Uploading Files](/machine-data/uploading-files) — Manual upload via the batch API, supported formats, and file preparation by equipment type. * [File Conversion](/machine-data/file-conversion) — Pipeline stages, status tracking, and what each stage produces. * [Field Operations](/machine-data/field-operations) — How files become merged operations and what data is available. * [Sample Output](/machine-data/sample-output) — Example responses for files and operations across all operation types. * [Units](/machine-data/units) — Unit reference for all numeric properties. * [API Reference: Operations](/api-reference/operations) — Full endpoint reference for field operations. * [API Reference: Files](/api-reference/files) — Full endpoint reference for machine files and batch uploads. # Sample Output Source: https://docs.withleaf.io/machine-data/sample-output Example API responses for machine files, file summaries, field operations, operation summaries, images, and units for planted, harvested, applied, and tillage. This page collects example responses from the machine data endpoints. Each section shows the response shape and realistic values for one resource type. ## Machine file A machine file returned by `GET /files/{id}`. The structure is the same across operation types; the summary properties vary. ```json theme={null} { "id": "7b525b72-a8e7-4d34-80bb-9ea2dde87a09", "provider": "providerName", "fields": [ "696b5df6-e401-4d13-bafe-3d2689723254" ], "fileFormat": "CN1", "fileName": "testFile.zip", "downloadOriginalFile": "url", "downloadStandardGeojson": "url", "leafUserId": "286eeb50-8e85-4e33-9f2d-1b9dcf0e56d7", "apiOwnerUsername": "test", "summary": { ... } } ``` ## Machine file summaries The `summary` field on a machine file is a GeoJSON Feature. Properties differ by operation type. ### Harvested ```json theme={null} { "type": "Feature", "properties": { "operationType": "harvested", "startTime": "2015-09-23T00:00:00Z", "endTime": "2015-09-24T00:36:55.8Z", "crop": ["corn"], "totalArea": { "value": 24963.04, "unit": "m2" }, "totalDistance": { "value": 17978.42, "unit": "ft" }, "elevation": { "avg": 155.78, "min": 147.6, "max": 162.4, "unit": "ft" }, "speed": { "avg": 4.93, "min": 2.43, "max": 6.39, "unit": "mi/hr" }, "harvestMoisture": { "avg": 18.22, "min": 12.75, "max": 21.18, "unit": "percentage" }, "wetMass": { "avg": 5.86, "min": 0.04, "max": 11.06, "unit": "lb" }, "totalWetMass": { "value": 72494.18, "unit": "lb" }, "wetMassPerArea": { "avg": 11752.32, "min": 87.80, "max": 40340.88, "unit": "lb/ac" }, "dryMass": { "avg": 5.63, "min": 0.04, "max": 10.89, "unit": "lb" }, "totalDryMass": { "value": 69707.74, "unit": "lb" }, "dryMassPerArea": { "avg": 11300.59, "min": 82.16, "max": 38518.42, "unit": "lb/ac" }, "wetVolume": { "avg": 0.10, "min": 0.001, "max": 0.20, "unit": "bu" }, "totalWetVolume": { "value": 1294.54, "unit": "bu" }, "wetVolumePerArea": { "avg": 209.86, "min": 1.57, "max": 720.37, "unit": "bu/ac" }, "dryVolume": { "avg": 0.10, "min": 0.001, "max": 0.19, "unit": "bu" }, "totalDryVolume": { "value": 1244.78, "unit": "bu" }, "dryVolumePerArea": { "avg": 201.80, "min": 1.47, "max": 687.83, "unit": "bu/ac" }, "totalFuelUsed": { "value": 196.25, "unit": "gal" }, "varieties": [ { "name": "Corn", "harvestMoisture": { "avg": 18.22, "min": 12.75, "max": 21.18, "unit": "percentage" }, "wetMass": { "value": 72494.18, "unit": "lb" }, "dryMass": { "value": 69707.74, "unit": "lb" }, "wetVolume": { "value": 1294.54, "unit": "bu" }, "dryVolume": { "value": 1244.78, "unit": "bu" }, "area": { "value": 24963.04, "unit": "m2" } } ], "machinery": [ { "name": "Case IH X010 Series 7010", "type": "machine", "serialNumber": "8227260", "brand": "Case IH" }, { "name": "Case IH Corn Head 15ft 6row", "type": "implement", "brand": "Case IH" } ], "originalOperationType": "Harvesting" }, "geometry": { "type": "MultiPolygon", "coordinates": [[[[-89.804, 40.478], [-89.808, 40.476], [-89.807, 40.473], [-89.805, 40.471], [-89.801, 40.471], [-89.798, 40.473], [-89.798, 40.476], [-89.801, 40.478], [-89.804, 40.478]]]] } } ``` ### Planted ```json theme={null} { "type": "Feature", "properties": { "operationType": "planted", "startTime": "2015-05-02T20:02:28.64Z", "endTime": "2015-05-02T20:09:32.64Z", "crop": ["corn"], "totalArea": { "value": 5779.89, "unit": "m2" }, "totalDistance": { "value": 1555.35, "unit": "ft" }, "elevation": { "avg": 142.05, "min": 141.9, "max": 142.2, "unit": "ft" }, "speed": { "avg": 3.91, "min": 0.058, "max": 4.258, "unit": "mi/hr" }, "seedRate": { "avg": 32889.93, "min": 31960, "max": 46135.31, "unit": "seeds/ac" }, "totalPlanted": { "value": 46974, "unit": "seeds" }, "singulation": { "avg": 99.62, "min": 99.48, "max": 99.76, "unit": "prcnt" }, "downForce": { "avg": 67.79, "min": 2, "max": 114, "unit": "lbf" }, "totalFuelUsed": { "value": 196.25, "unit": "gal" }, "varieties": [ { "name": "variety", "rate": { "avg": 32889.93, "min": 31960, "max": 46135.31, "unit": "seeds/ac", "minTarget": 32000, "maxTarget": 33000, "avgTarget": 32890.79 }, "area": { "value": 5779.89, "unit": "m2" }, "totalPlanted": { "value": 46974, "unit": "seeds" } } ], "machinery": [ { "name": "Challenger 745 Tractor", "type": "machine", "serialNumber": "8220203", "brand": "Challenger" }, { "name": "Kinze 40.0' 16 Row Planter", "type": "implement", "brand": "Kinze" } ], "originalOperationType": "SowingAndPlanting" }, "geometry": { "type": "MultiPolygon", "coordinates": [[[[-89.804, 40.478], [-89.808, 40.476], [-89.807, 40.473], [-89.805, 40.471], [-89.801, 40.471], [-89.798, 40.473], [-89.798, 40.476], [-89.801, 40.478], [-89.804, 40.478]]]] } } ``` ### Applied ```json theme={null} { "type": "Feature", "properties": { "operationType": "applied", "startTime": "2017-04-22T18:31:23.105Z", "endTime": "2017-04-22T18:46:08.308Z", "crop": [], "totalArea": { "value": 63817.04, "unit": "m2" }, "totalDistance": { "value": 7532.93, "unit": "ft" }, "elevation": { "avg": 254.06, "min": 244.1, "max": 263.5, "unit": "ft" }, "speed": { "avg": 10.12, "min": 1.82, "max": 12.68, "unit": "mi/hr" }, "appliedRate": { "avg": 14.23, "min": 0.27, "max": 66.09, "unit": "gal/ac" }, "totalApplied": { "value": 224.46, "unit": "gal" }, "totalFuelUsed": { "value": 196.25, "unit": "gal" }, "tankMix": true, "products": [ { "name": "Fastac EC", "type": "Component", "rate": { "unit": "floz/ac", "value": 3.4 }, "totalApplied": { "value": 75.41, "unit": "floz" }, "area": { "value": 89753.33, "unit": "m2" } }, { "name": "Water", "type": "Carrier", "rate": { "unit": "gal/ac", "value": 15 }, "totalApplied": { "value": 332.68, "unit": "gal" }, "area": { "value": 89753.33, "unit": "m2" } } ], "operationDescription": "Pre-Emerge", "machinery": [ { "name": "John Deere R Series R4030", "type": "machine", "serialNumber": "10071690", "brand": "John Deere" }, { "name": "Sprayer", "type": "implement", "brand": "unknown" } ], "originalOperationType": "CropProtection" }, "geometry": { "type": "MultiPolygon", "coordinates": [[[[-89.804, 40.478], [-89.808, 40.476], [-89.807, 40.473], [-89.805, 40.471], [-89.801, 40.471], [-89.798, 40.473], [-89.798, 40.476], [-89.801, 40.478], [-89.804, 40.478]]]] } } ``` ### Tillage ```json theme={null} { "type": "Feature", "properties": { "operationType": "tillage", "startTime": "2016-09-13T20:33:14.786Z", "endTime": "2016-09-13T22:03:10.801Z", "totalArea": { "value": 202441.91, "unit": "m2" }, "totalDistance": { "value": 54476.66, "unit": "ft" }, "elevation": { "avg": 994.65, "min": 994.65, "max": 994.65, "unit": "ft" }, "tillType": ["Closing Disk"], "tillageDepthActual": { "avg": 0, "min": 0, "max": 0, "unit": "in" }, "tillageDepthTarget": { "avg": 6, "min": 6, "max": 6, "unit": "in" } }, "geometry": { "type": "MultiPolygon", "coordinates": [[[[-89.804, 40.478], [-89.808, 40.476], [-89.807, 40.473], [-89.805, 40.471], [-89.801, 40.471], [-89.798, 40.473], [-89.798, 40.476], [-89.801, 40.478], [-89.804, 40.478]]]] } } ``` ## Field operation A field operation returned by `GET /operations/{id}`: ```json theme={null} { "id": "uuid", "apiOwnerUsername": "leaf@withleaf.io", "leafUserId": "uuid", "startTime": "2016-09-19T18:30:51.640+00:00", "endTime": "2016-09-21T21:48:25.000+00:00", "updatedTime": "2023-04-15T12:00:00.000+00:00", "type": "harvested", "files": ["uuid-1", "uuid-2", "uuid-3"], "fields": [ { "id": "uuid", "coverage": 0.95 } ], "providers": ["JohnDeere"] } ``` ## Field operation summary The operation summary (`GET /operations/{id}/summary`) has the same structure as a file summary but represents the merged result across all files in the operation. See the file summary examples above — the properties and format are identical, with differences only in the values (which reflect the merged dataset). ## Standard GeoJSON point data Each point in the `standardGeojson` is a GeoJSON Feature. Properties vary by operation type. ### Harvested point ```json theme={null} { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.838, 28.687] }, "properties": { "timestamp": "2011-10-05T14:48:00.000Z", "operationType": "harvested", "crop": "corn", "variety": "Pioneer P1185", "area": 3.42, "distance": 12.5, "heading": 180.3, "speed": 4.2, "elevation": 155.8, "equipmentWidth": 15.0, "harvestMoisture": 18.2, "wetMass": 5.86, "wetVolume": 0.10, "wetMassPerArea": 11752.32, "wetVolumePerArea": 209.86, "dryMass": 5.63, "dryVolume": 0.10, "dryMassPerArea": 11300.59, "dryVolumePerArea": 201.80, "recordingStatus": "On", "sectionId": 0, "machinery": ["Case IH X010 Series 7010"] } } ``` ### Planted point ```json theme={null} { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.838, 28.687] }, "properties": { "timestamp": "2011-10-05T14:48:00.000Z", "operationType": "planted", "crop": "corn", "variety": "P1309WAM", "area": 3.42, "distance": 12.5, "heading": 90.1, "speed": 4.6, "elevation": 197.8, "equipmentWidth": 40.0, "seedRate": 36590, "seedRateTarget": 37000, "seedDepth": 2.0, "downForce": 137.0, "singulation": 99.3, "skips": 0.29, "doubles": 0.24, "recordingStatus": "On", "sectionId": 0, "machinery": ["Challenger 745 Tractor"] } } ``` ### Applied point ```json theme={null} { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.838, 28.687] }, "properties": { "timestamp": "2011-10-05T14:48:00.000Z", "operationType": "applied", "crop": "corn", "area": 3.42, "distance": 12.5, "heading": 270.5, "speed": 10.1, "elevation": 254.1, "equipmentWidth": 90.0, "appliedRate": 14.23, "appliedRateTarget": 15.0, "recordingStatus": "On", "sectionId": 0, "machinery": ["John Deere R Series R4030"], "products": [ { "name": "Fastac EC", "rate": { "value": 3.4 } }, { "name": "Water", "rate": { "value": 15 } } ] } } ``` ### Tillage point ```json theme={null} { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-93.150, 41.671] }, "properties": { "timestamp": "2021-12-08T17:46:13.482Z", "operationType": "tillage", "area": 3.42, "distance": 12.5, "heading": 45.2, "speed": 5.5, "elevation": 690.4, "equipmentWidth": 30.0, "tillageDepthTarget": 4.0, "tillType": ["Closing Disk"], "recordingStatus": "On", "sectionId": 0, "machinery": ["MachineName"] } } ``` ## Properties by operation type Properties are marked as: **yes** (always in response), **configurable** (required by default, can be made optional via configuration), or blank (present when data is available). ### Planted | Property | Always present | Type | Description | | ----------------------- | :------------: | ------ | -------------------------------------------------------------------------------------- | | `crop` | configurable | string | Crop type (normalized). Required by default; set `cropOptional` to make optional. | | `seedRate` | configurable | int | Seeds per area at point. Required by default; set `seedRateOptional` to make optional. | | `operationType` | yes | string | `"planted"` | | `totalArea` | yes | float | Area covered | | `elevation` | yes | float | Elevation | | `originalOperationType` | | string | Original operation type from the source data | | `varieties` | | dict | Per-variety breakdowns | | `seedRateTarget` | | int | Target seed rate | | `seedDepth` | | float | Planting depth | | `machinery` | | list | Machine/implement info | | `speed` | | float | Speed at point | | `totalPlanted` | | int | Total seeds | | `operationDescription` | | string | Description from source data | | `downForce` | | float | Down force reading | | `singulation` | | float | Meter performance % | | `totalFuelUsed` | | dict | Total fuel consumed | ### Harvested When the crop is known and a moisture value is present, Leaf will calculate and fill in any missing dry or wet yield values. For example, if the source data only provides wet mass, Leaf derives dry mass using the crop's standard moisture percentage. | Property | Always present | Type | Description | | --------------------------------- | :------------: | ------ | -------------------------------------------------------------------- | | `crop` | configurable | string | Crop type. Required by default; set `cropOptional` to make optional. | | `elevation` | yes | float | Elevation | | `harvestMoisture` | yes | float | Moisture % | | `operationType` | yes | string | `"harvested"` | | `totalArea` | yes | float | Area covered | | `wetMass` / `wetMassPerArea` | yes | float | Wet mass and per-area | | `wetVolume` / `wetVolumePerArea` | yes\* | float | Wet volume and per-area | | `totalWetMass` / `totalWetVolume` | yes | float | Totals | | `dryMass` / `dryMassPerArea` | yes | float | Dry mass and per-area | | `dryVolume` / `dryVolumePerArea` | yes\* | float | Dry volume and per-area | | `totalDryMass` / `totalDryVolume` | yes | float | Totals | | `originalOperationType` | | string | Original operation type from the source data | | `varieties` | | dict | Per-variety breakdowns | | `speed` | | float | Speed at point | | `machinery` | | list | Machine/implement info | | `operationDescription` | | string | Description from source data | | `totalFuelUsed` | | dict | Total fuel consumed | \*Not available when the crop is sugarcane. ### Applied | Property | Always present | Type | Description | | ----------------------- | :------------: | ------- | ----------------------------------------------------------------- | | `appliedRate` | configurable | float | Applied rate. Required by default; can be configured as optional. | | `operationType` | yes | string | `"applied"` | | `totalArea` | yes | float | Area covered | | `elevation` | yes | float | Elevation | | `products` | yes | list | Product details | | `originalOperationType` | | string | Original operation type from the source data | | `appliedRateTarget` | | float | Target rate | | `machinery` | | list | Machine/implement info | | `speed` | | float | Speed at point | | `totalApplied` | | float | Total product applied | | `operationDescription` | | string | Description from source data | | `tankMix` | | boolean | Whether a tank mix was used | | `totalFuelUsed` | | dict | Total fuel consumed | ### Tillage | Property | Always present | Type | Description | | ----------------------- | :------------: | ------ | -------------------------------------------- | | `elevation` | yes | float | Elevation | | `operationType` | yes | string | `"tillage"` | | `totalArea` | yes | float | Area covered | | `originalOperationType` | | string | Original operation type from the source data | | `tillageDepthTarget` | | float | Target depth | | `tillageDepthActual` | | float | Actual depth | | `speed` | | float | Speed at point | | `machinery` | | list | Machine/implement info | | `operationDescription` | | string | Description from source data | | `totalFuelUsed` | | dict | Total fuel consumed | ## Images ### V2 images ```json theme={null} [ { "property": "wetMassPerArea", "legend": { "ranges": [ { "colorCode": "#C80000", "min": 0, "max": 20 }, { "colorCode": "#FF2800", "min": 20, "max": 50 }, { "colorCode": "#FF9600", "min": 50, "max": 100 }, { "colorCode": "#FFF000", "min": 100, "max": 250 }, { "colorCode": "#00E600", "min": 250, "max": 340 }, { "colorCode": "#00BE00", "min": 340, "max": 480 }, { "colorCode": "#008200", "min": 480, "max": 570 } ] }, "extent": { "xMin": 0.0, "xMax": 0.0, "yMin": 0.0, "yMax": 0.0 }, "downloadUrl": "https://api.withleaf.io/...", "status": "PROCESSED", "url": "https://..." } ] ``` ### GeoTIFF images ```json theme={null} [ { "property": "wetMassPerArea", "url": "https://s3-url-to-geotiff/geotiff-uuid.tif", "downloadUrl": "https://api.withleaf.io/..." }, { "property": "dryVolumePerArea", "url": "https://s3-url-to-geotiff/geotiff-uuid.tif", "downloadUrl": "https://api.withleaf.io/..." }, { "property": "harvestMoisture", "url": "https://s3-url-to-geotiff/geotiff-uuid.tif", "downloadUrl": "https://api.withleaf.io/..." } ] ``` # Units Source: https://docs.withleaf.io/machine-data/units Unit reference for numeric properties in Leaf machine files and field operations, organized by measurement system and operation type. Every machine file and field operation includes a units map (`GET /files/{id}/units` or `GET /operations/{id}/units`) that maps each numeric property to its unit string. The units you receive are controlled by the `unitMeasurement` configuration, which accepts three values: `DEFAULT`, `IMPERIAL`, or `METRIC`. Set it at the API owner or Leaf user level in [Configuration](/configuration/overview#unitmeasurement). Always check the units endpoint for the specific file or operation rather than assuming units from the tables below. Rate and product-specific units can vary by operation content. The same unit strings appear in both summary stat objects (`{ "avg": 4.93, "min": 2.43, "max": 6.39, "unit": "mi/hr" }`) and in the standalone units map (`"speed": "mi/hr"`). *** ## Default These are the units when `unitMeasurement` is set to `DEFAULT`. ### Common properties (all operation types) | Property | Unit | Description | | ---------------- | ----- | ------------------------------------------------------- | | `startTime` | - | ISO 8601 format (`yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'`) | | `endTime` | - | ISO 8601 format | | `operationType` | - | `"planted"`, `"harvested"`, `"applied"`, or `"tillage"` | | `crop` | - | Crop type (e.g., corn, soybeans) | | `varieties` | - | Variety breakdowns with seed rate, count, and area | | `machinery` | - | Machine name, type, brand, serial number | | `geometry` | - | GeoJSON geometry of the operation area | | `speed` | mi/hr | Travel speed | | `totalDistance` | ft | Total distance traveled | | `elevation` | ft | Landscape elevation | | `equipmentWidth` | ft | Equipment width | | `totalArea` | m² | Total area covered | ### Default — Planted | Property | Unit | Description | | -------------- | ---------- | -------------------------------------- | | `seedRate` | seeds/ac | Seeds planted per acre | | `singulation` | percentage | Meter performance (one seed at a time) | | `downForce` | - | No fixed default unit mapping | | `totalPlanted` | seeds | Total seeds planted | | `skips` | percentage | Missing seeds within a row | | `doubles` | percentage | Multiple seeds in same location | ### Default — Applied | Property | Unit | Description | | -------------- | ------------------------------------------------ | --------------------------------------------------------------------------- | | `products` | - | Product details and per-product rates | | `rate` | gal/ac, lb/ac, qt/ac, floz/ac, kg/ha, t/ha, L/ha | Per-product application rate. Units vary by product type (liquid vs solid). | | `appliedRate` | gal/ac | Combined applied rate across all products | | `totalApplied` | gal, lb, qt, t, L | Total product applied. Units vary by product type. | ### Default — Harvested | Property | Unit | Description | | ------------------ | ----- | ------------------------------ | | `totalWetMass` | lb | Mass before drying | | `wetMassPerArea` | lb/ac | Mass per area before drying | | `wetVolume` | bu | Volume before drying | | `totalWetVolume` | bu | Total volume before drying | | `wetVolumePerArea` | bu/ac | Volume per area before drying | | `harvestMoisture` | % | Moisture percentage at harvest | | `dryMass` | lb | Mass after drying | | `totalDryMass` | lb | Total dry mass | | `dryMassPerArea` | lb/ac | Dry mass per area | | `dryVolume` | bu | Volume after drying | | `totalDryVolume` | bu | Total dry volume | | `dryVolumePerArea` | bu/ac | Dry volume per area | ### Default — Tillage | Property | Unit | Description | | -------------------- | ---- | ----------------------------- | | `tillageDepthTarget` | - | No fixed default unit mapping | *** ## Imperial These are the units when `unitMeasurement` is set to `IMPERIAL`. ### Common properties (all operation types) | Property | Unit | Description | | ---------------- | ---- | --------------------- | | `startTime` | - | ISO 8601 format | | `endTime` | - | ISO 8601 format | | `operationType` | - | Operation type string | | `crop` | - | Crop type | | `varieties` | - | Variety breakdowns | | `machinery` | - | Machine details | | `geometry` | - | GeoJSON geometry | | `speed` | ft/s | Feet per second | | `totalDistance` | ft | Feet | | `elevation` | ft | Feet | | `equipmentWidth` | ft | Feet | | `totalArea` | ac | Acres | ### Imperial — Planted | Property | Unit | Description | | -------------- | ---------- | ------------------------------- | | `seedRate` | seeds/ac | Seeds per acre | | `singulation` | percentage | Meter performance | | `downForce` | lbf | Pound-force | | `totalPlanted` | seeds | Total seeds planted | | `skips` | percentage | Missing seeds | | `doubles` | percentage | Multiple seeds in same location | ### Imperial — Applied | Property | Unit | Description | | -------------- | ------------- | --------------------------------------------- | | `products` | - | Product details | | `rate` | gal/ac, lb/ac | Per-product rate. Units vary by product type. | | `appliedRate` | gal/ac, lb/ac | Combined applied rate | | `totalApplied` | gal, lb | Total product applied | ### Imperial — Harvested | Property | Unit | Description | | ------------------ | ----- | ------------------- | | `totalWetMass` | lb | Pounds | | `wetMassPerArea` | lb/ac | Pounds per acre | | `wetVolume` | bu | Bushels | | `totalWetVolume` | bu | Bushels | | `wetVolumePerArea` | bu/ac | Bushels per acre | | `harvestMoisture` | % | Moisture percentage | | `dryMass` | lb | Pounds | | `totalDryMass` | lb | Pounds | | `dryMassPerArea` | lb/ac | Pounds per acre | | `dryVolume` | bu | Bushels | | `totalDryVolume` | bu | Bushels | | `dryVolumePerArea` | bu/ac | Bushels per acre | ### Imperial — Tillage | Property | Unit | Description | | -------------------- | ---- | ----------- | | `tillageDepthTarget` | in | Inches | *** ## Metric These are the units when `unitMeasurement` is set to `METRIC`. ### Common properties (all operation types) | Property | Unit | Description | | ---------------- | ---- | --------------------- | | `startTime` | - | ISO 8601 format | | `endTime` | - | ISO 8601 format | | `operationType` | - | Operation type string | | `crop` | - | Crop type | | `varieties` | - | Variety breakdowns | | `machinery` | - | Machine details | | `geometry` | - | GeoJSON geometry | | `speed` | m/s | Meters per second | | `totalDistance` | m | Meters | | `elevation` | m | Meters | | `equipmentWidth` | m | Meters | | `totalArea` | ha | Hectares | ### Metric — Planted | Property | Unit | Description | | -------------- | ---------- | ------------------------------- | | `seedRate` | seeds/ha | Seeds per hectare | | `singulation` | percentage | Meter performance | | `downForce` | kgf | Kilogram-force | | `totalPlanted` | seeds | Total seeds planted | | `skips` | percentage | Missing seeds | | `doubles` | percentage | Multiple seeds in same location | ### Metric — Applied | Property | Unit | Description | | -------------- | ------------------ | --------------------------------------------- | | `products` | - | Product details | | `rate` | m³/ha, L/ha, kg/ha | Per-product rate. Units vary by product type. | | `appliedRate` | m³/ha, L/ha, kg/ha | Combined applied rate | | `totalApplied` | m³, L | Total product applied | ### Metric — Harvested | Property | Unit | Description | | ------------------ | ----- | ------------------------ | | `totalWetMass` | t | Tonnes | | `wetMassPerArea` | t/ha | Tonnes per hectare | | `wetVolume` | m³ | Cubic metres | | `totalWetVolume` | m³ | Cubic metres | | `wetVolumePerArea` | m³/ha | Cubic metres per hectare | | `harvestMoisture` | % | Moisture percentage | | `dryMass` | t | Tonnes | | `totalDryMass` | t | Tonnes | | `dryMassPerArea` | t/ha | Tonnes per hectare | | `dryVolume` | m³ | Cubic metres | | `totalDryVolume` | m³ | Cubic metres | | `dryVolumePerArea` | m³/ha | Cubic metres per hectare | ### Metric — Tillage | Property | Unit | Description | | -------------------- | ---- | ----------- | | `tillageDepthTarget` | cm | Centimeters | # Uploading Files Source: https://docs.withleaf.io/machine-data/uploading-files Upload machine files to Leaf using the batch API. Covers supported formats, ZIP requirements, and folder structure rules by equipment manufacturer. Machine data enters Leaf two ways: through a connected provider or via manual file upload. This page covers the manual upload path using the batch API. Manual upload is useful when growers aren't connected to a cloud provider. They can pull files from a USB drive or monitor, ZIP them, and upload to Leaf. You can also use Leaf Link's pre-built upload widget or a Magic Link to give end users a no-code upload experience. ## Upload basics All uploads go through the batch API (`/batch` endpoint). Every upload must be a **ZIP file**, maximum 3 GB. Leaf extracts and processes the contents automatically. You can optionally specify which provider the files came from. If you don't know or the ZIP contains files from multiple providers, Leaf auto-detects, splits, and processes each format independently. After uploading, Leaf assigns a `batchId` you can use to track processing status for all files in that upload. ## Folder structure rules **ZIP the folder directly from the monitor or USB drive.** Each manufacturer uses specific folder names and directory structures that Leaf expects to find intact. * Keep folder names exactly as they appear (e.g., `TASKDATA`, `RCD`, `AgData`) * Preserve the complete folder hierarchy * Don't create new folders, rename existing ones, or move files around * Don't manually collect individual files into a new folder Leaf searches up to two levels deep inside nested ZIPs. You can upload a ZIP containing other ZIPs. Upload files directly from the monitor when possible. Original monitor files contain richer data and process more reliably than exported shapefiles. If you're using Ag Leader SMS, export the original `.agdata`, `.ilf`, or `.yld` files rather than creating shapefile exports. ## Preparing files by equipment type ### John Deere **GreenStar 2 (2600)** ``` RCD ├── *.fdd └── *.fdl ``` Locate the `RCD` folder on the USB drive, ZIP it, and upload. **GreenStar 3 (2630)** ``` GS3_2630 └── RCD └── EIC └── global.ver └── documentation └── ... ├── *.fdd └── *.fdl ``` Locate the `GS3_2630` or `RCD` folder, ZIP it with the complete hierarchy, and upload. **GreenStar 4 (Gen 4 — 4600/4630)** ``` JD-Data └── log └── *.jdl ``` Locate the `JD-Data` folder, ZIP it, and upload. **MyJohnDeere shapefile exports** — Supported, but native monitor files are preferred. ### Climate FieldView / Precision Planting These are the same 20|20 monitors listed under both brands. **20|20 SeedSense Generation 1 and 2** ``` ├── harvest_*.dat ├── field_map_*.dat └── liquid_map_*.dat ``` **20|20 SeedSense Generation 3** ``` └── *.2020 ``` Locate the folder containing these files, ZIP it, and upload. ### CNHi (Case IH / New Holland) **Pro 700 / IntelliView IV (Voyager 2)** ``` .cn1/ ├── index.vy1 └── (other data files) ``` The `.cn1` folder contains all operation data. ZIP the entire folder and upload. **Pro 1200 / IntelliView 12 (ISOXML)** ``` TASKDATA ├── TASKDATA.XML └── *.bin ``` Locate the `TASKDATA` folder, ZIP it keeping the folder name, and upload. ### Ag Leader **INTEGRA (v3.5+), VERSA, or COMPASS** ``` ├── *.agdata └── *.agsetup ``` Both files must be present together. ZIP the folder containing both and upload. **Edge, Insight, or INTEGRA (v3.4)** ``` └── *.ilf ``` **PF Advantage, PF 3000, PF 3000 Pro, YM2000** ``` └── *.yld ``` ZIP the folder containing these files and upload. ### Trimble **FMX or CFX monitors (AgData format)** ``` AgData ├── Fields/ └── *.agf ├── implements/ └── *.agi ├── prescriptions/ └── *.agm ├── Tasks/ └── *.agt ├── Users/ └── *.agu └── vehicles/ └── *.agv ``` Locate the `AgData` folder, ZIP it with the complete structure, and upload. **GFX-750, TMX-2050 monitors (AgGPS format — shapefiles)** ``` AgGPS └── Data └── "Grower" └── Farm └── field └── "Task" ├── *.cpg ├── *.dbf ├── *.shp └── *.shx ``` ### Raven Slingshot **Raven FMIS** ``` ├── *.xml └── *.tab ``` Both `.xml` and `.tab` files should be present. ZIP the folder and upload. **Raven JDP** ``` └── *.jdp ``` ### ISOXML equipment Supported brands: CLAAS, Kuhn, Kverneland Group, Müller-Elektronik, Teknomika, Topcon. ``` TASKDATA ├── *.XML └── *.bin ``` The folder **must** be named `TASKDATA`. Do not rename it. ### Farmobile GeoJSON files exported from Farmobile. ZIP the exported files and upload. Since GeoJSON files don't contain unit information, Leaf assumes Farmobile's default units. ## Shapefile uploads Generic shapefiles can be uploaded from SMS (Ag Leader), Raven Slingshot, Topcon, and other systems. Original monitor files are strongly preferred when available. ### Requirements ``` shapefile.zip ├── *.shp (required — geometry) ├── *.dbf (required — attributes) ├── *.shx (required — shape index) ├── *.prj (required — projection) └── *.cpg (optional — encoding) ``` All four required files must be present and share the same base name (e.g., `field_harvest.shp`, `field_harvest.dbf`, `field_harvest.shx`, `field_harvest.prj`). Place all files at the root level of the ZIP. ### Recognized column names Shapefile column names are often truncated to 10 characters. Leaf recognizes the following names in `.dbf` files: **Harvest:** * Crop: `Crop`, `Crop_Type`, `Product_Pr` * Moisture: `Moisture`, `Moisture__`, `MOISTURE` * Yield (volume/area): `Yld_Vol_We`, `Yield__Wet`, `WET_YIELD` * Yield (mass/area): `Yield_Mass`, `Yld_Mass_W` (wet), `Yld_Mass_D`, `dryyldlba` (dry) * Yield (total wet mass): `WetMass`, `wetMass`, `Harvest_We` * Yield (total wet volume): `wetVolume` * Yield (dry volume/area): `Yld_Vol_Dr`, `DryYldbuac` Leaf calculates missing dry/wet yield properties automatically when a crop column is present. **Planting:** * Crop: `Crop`, `Crop_Type`, `Product_Pr` * Seed rate: `seedRate`, `SeedCount`, `Rt_Apd_Ct_` **Application:** * Applied rate: `AppliedRat`, `Rt_Apd_Liq`, `actualRate` * Product: `Product`, `product`, `Products` If your column names don't match any recognized name, contact Leaf support. ### Exporting from SMS (Ag Leader) If original monitor files aren't available: 1. In SMS Project Workspace, right-click the operation to export 2. Select **Export** → paper icon → "Export to a Generic File Format" 3. Choose **Generic** and **Shape** as the file type 4. Click **Export Selections and Settings** to configure columns. You can rename columns here. For example, to include Crop in harvest files: select Property → Product Management Item Type → add Crop Type → rename to "Crop" 5. Save, then select all four output files (`.shp`, `.dbf`, `.prj`, `.shx`), compress into a ZIP, and upload ## Troubleshooting uploads **"Upload failed" errors:** * Confirm the file is a ZIP (all uploads must be ZIP files) * Check that the ZIP contains all required components for the file type * Verify file size is under 3 GB * For shapefiles, confirm the `.dbf` includes recognized columns for the operation type **Missing or incomplete data:** * Verify the original folder structure from the monitor is intact * Don't rename folders (`TASKDATA`, `RCD`, `AgData` must keep their original names) * For ISOXML, the `TASKDATA` folder must contain both `*.XML` and `*.bin` files * For Ag Leader INTEGRA, both `*.agdata` and `*.agsetup` must be present * Check nesting depth (Leaf looks up to 2 levels deep in nested ZIPs) **Processing errors:** * Double-check that the folder structure matches what's expected for the equipment * Make sure files haven't been manually reorganized or flattened ## What to do next * [File Conversion](/machine-data/file-conversion) — What happens to files after they're uploaded. * [Field Operations](/machine-data/field-operations) — How converted files become merged field operations. * [API Reference: Files](/api-reference/files) — Full endpoint reference for file uploads and batch management. # MCP Server Source: https://docs.withleaf.io/mcp/overview Connect AI coding assistants like Cursor and Claude Code to the Leaf API using the MCP server. Query fields, operations, weather, and billing data. Leaf runs a public [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that gives AI coding assistants direct access to the Leaf API. Connect it to Cursor, Claude Code, or any MCP-compatible client and your assistant can query Leaf users, fields, field operations, machine files, weather, and billing data on your behalf. ## How it works The MCP server exposes Leaf API functionality as MCP tools. When your AI assistant needs to look up a field boundary or check the status of a machine file, it calls the appropriate tool. The server authenticates with your Leaf token, makes the API request, and returns the result to the assistant. No SDK installation required. The server runs remotely at `mcp.withleaf.io/mcp` using Streamable HTTP transport. ## Authentication Every request to the MCP server requires a valid Leaf API token passed as a `LEAF_TOKEN` header. The server extracts the token from the MCP request headers and uses it for all downstream API calls. Generate a token the same way you would for direct API use. See [Authentication](/getting-started/authentication) for details. ## Connecting from Cursor Add this to your `.cursor/mcp.json` file (project-level) or `~/.cursor/mcp.json` (global): ```json theme={null} { "mcpServers": { "leaf": { "url": "https://mcp.withleaf.io/mcp", "headers": { "LEAF_TOKEN": "" } } } } ``` Restart Cursor after saving. The Leaf tools appear in the MCP tools panel. ## Connecting from Claude Code Run this command in your terminal: ```bash theme={null} claude mcp add leaf \ --transport http \ --url https://mcp.withleaf.io/mcp \ --header "LEAF_TOKEN: " ``` Claude Code picks up the server immediately. ## Connecting from other MCP clients Any client that supports Streamable HTTP transport can connect. Point it at `https://mcp.withleaf.io/mcp` and include the `LEAF_TOKEN` header. The server does not use SSE or stdio transport. ## Available tools The MCP server exposes tools across these categories: | Category | Tools | What they do | | -------------------- | ----- | ------------------------------------------------------------------------ | | Documentation | 2 | Browse Leaf API docs without leaving your editor | | Configuration | 2 | Read API owner and Leaf user configuration settings | | Provider credentials | 3 | Check credential status for John Deere, Climate FieldView, and CNHi | | User management | 1 | List and filter Leaf users | | Fields | 3 | List fields, get field details and boundaries | | Field operations | 4 | List and inspect field operations with summaries and units | | Machine files | 5 | List, inspect, and check processing status of machine files | | Batch uploads | 3 | List and inspect batch uploads | | Weather | 8 | Forecast and historical weather by field or coordinates, daily or hourly | | Billing | 5 | List contracts, check consumption by API owner or Leaf user | See [Tools Reference](/mcp/tools-reference) for the full list with parameters. ## Common use cases * **Troubleshoot customer issues**: Ask your AI assistant to look up a Leaf user's credential events, file processing status, or field boundaries without leaving your editor. * **Explore data interactively**: Query field operations, machine files, and summaries by provider, date range, or operation type in natural language. * **Monitor billing**: Check contract consumption at the API owner or Leaf user level to catch unexpected usage spikes. ## What to do next * [Tools Reference](/mcp/tools-reference) for the complete tool list with parameters and types. * [Authentication](/getting-started/authentication) to generate your Leaf API token. # MCP Tools Reference Source: https://docs.withleaf.io/mcp/tools-reference Reference for all Leaf MCP server tools in Cursor, Claude Code, and other AI assistants. Covers fields, operations, machine files, weather, billing, and users. Every tool the Leaf MCP server exposes to AI coding assistants. Tools are organized by category. All tools authenticate using the `LEAF_TOKEN` header configured in your MCP client. See [MCP Server Overview](/mcp/overview) for setup instructions. ## Documentation ### `get_docs_index` Returns an index of all available Leaf API documentation pages. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ------------- | | — | — | — | No parameters | ### `get_leaf_doc` Returns the contents of a specific Leaf API documentation page. | Parameter | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------------------------------------------- | | `doc_path` | string | Yes | Path to the doc page (e.g., `API_Reference/Alerts/alerts_endpoints`) | *** ## Configuration ### `get_api_owner_configuration` Returns configuration settings for the API owner, including defaults for field operation image creation, fields auto-sync, and merge intersections. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ------------- | | — | — | — | No parameters | ### `get_leaf_user_configuration` Returns configuration settings for a specific Leaf user. If the user has no custom configuration, they inherit from the API owner. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `leaf_user_id` | string | Yes | UUID of the Leaf user | *** ## Provider Credentials ### `get_john_deere_credentials_events` Returns events and status information for a Leaf user's John Deere credentials. Useful for troubleshooting connection issues with John Deere Operations Center. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `leaf_user_id` | string | Yes | UUID of the Leaf user | ### `get_climate_fieldview_credentials_events` Returns events and status information for a Leaf user's Climate FieldView credentials. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `leaf_user_id` | string | Yes | UUID of the Leaf user | ### `get_cnhi_credentials_events` Returns events and status information for a Leaf user's CNHi credentials. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `leaf_user_id` | string | Yes | UUID of the Leaf user | *** ## User Management ### `list_users` Returns a paginated list of Leaf users belonging to the authenticated organization. | Parameter | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------ | | `email` | string | No | Filter by email address | | `name` | string | No | Filter by full name | | `external_id` | string | No | Filter by your external identifier | | `page` | integer | No | Zero-based page number (default `0`) | | `size` | integer | No | Page size, max 100 (default `10`) | *** ## Fields ### `list_fields` Returns a paginated list of fields for a Leaf user. | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------ | | `leaf_user_id` | string | Yes | UUID of the Leaf user | | `type` | string | No | Filter by field type | | `farm_id` | integer | No | Filter by farm ID | | `provider` | string | No | Filter by provider | | `page` | integer | No | Zero-based page number (default `0`) | | `size` | integer | No | Page size, max 100 (default `10`) | ### `get_field` Returns details for a single field. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `leaf_user_id` | string | Yes | UUID of the Leaf user | | `field_id` | string | Yes | UUID of the field | ### `get_field_boundary` Returns the active GeoJSON boundary of a field. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `leaf_user_id` | string | Yes | UUID of the Leaf user | | `field_id` | string | Yes | UUID of the field | *** ## Field Operations ### `list_operations` Returns a paginated list of field operations for a Leaf user. Supports filtering by provider, time range, operation type, and field. | Parameter | Type | Required | Description | | ---------------- | ------- | -------- | ---------------------------------------------------------------------------------- | | `leaf_user_id` | string | Yes | UUID of the Leaf user | | `provider` | string | No | `CNHI`, `JohnDeere`, `Trimble`, `ClimateFieldView`, `AgLeader`, `Stara`, or `Leaf` | | `start_time` | string | No | ISO-8601 timestamp; operations starting on or after this time | | `updated_time` | string | No | ISO-8601 timestamp; operations updated on or after this time | | `end_time` | string | No | ISO-8601 timestamp; operations ending on or before this time | | `operation_type` | string | No | `applied`, `planted`, `harvested`, or `tillage` | | `field_id` | string | No | UUID of the field | | `page` | integer | No | Zero-based page number (default `0`) | | `size` | integer | No | Page size, max 100 (default `10`) | | `sort` | string | No | Comma-separated sort fields with optional `,asc` or `,desc` suffix | ### `get_operation` Returns details for a single field operation. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------------- | | `operation_id` | string | Yes | UUID of the field operation | ### `get_operation_summary` Returns the GeoJSON summary for a field operation. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------------- | | `operation_id` | string | Yes | UUID of the field operation | ### `get_operation_units` Returns the unit map for a field operation (what units each property uses). | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------------- | | `operation_id` | string | Yes | UUID of the field operation | *** ## Machine Files ### `list_files` Returns a paginated list of machine files with optional filters. Machine files are the raw data files from providers that Leaf processes into standardized field operations. | Parameter | Type | Required | Description | | ----------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------- | | `leaf_user_id` | string | No | UUID of the Leaf user | | `provider` | string | No | `CNHI`, `JohnDeere`, `Trimble`, `ClimateFieldView`, `AgLeader`, `RavenSlingshot`, `Stara`, or `Leaf` | | `status` | string | No | `processed`, `failed`, or `processing` | | `origin` | string | No | `provider`, `automerged`, `merged`, or `uploaded` | | `organization_id` | string | No | Provider organization ID (John Deere only) | | `batch_id` | string | No | UUID of the batch upload | | `created_time` | string | No | ISO-8601 timestamp; files created on or after this time | | `start_time` | string | No | ISO-8601 timestamp; operation started on or after this time | | `updated_time` | string | No | ISO-8601 timestamp; files updated on or after this time | | `end_time` | string | No | ISO-8601 timestamp; operation ended on or before this time | | `operation_type` | string | No | `applied`, `planted`, `harvested`, or `tillage` | | `min_area` | float | No | Minimum operation area in square meters | | `page` | integer | No | Zero-based page number (default `0`) | | `size` | integer | No | Page size, max 100 (default `10`) | | `sort` | string | No | Comma-separated sort fields with optional `,asc` or `,desc` suffix | ### `get_file` Returns details for a single machine file. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `file_id` | string | Yes | UUID of the machine file | ### `get_file_summary` Returns the summary for a machine file. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `file_id` | string | Yes | UUID of the machine file | ### `get_file_units` Returns the unit map for a machine file. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `file_id` | string | Yes | UUID of the machine file | ### `get_file_status` Returns the processing status for every step of Leaf's pipeline for a machine file. Each step reports `processed`, `processing`, or `failed`. Pipeline steps: `standardGeojson`, `cleanupGeojson`, `areaAndYield`, `summary`, `units`, `originalFile`. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `file_id` | string | Yes | UUID of the machine file | *** ## Batch Uploads ### `list_batches` Returns a paginated list of manual-upload batches. | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------ | | `leaf_user_id` | string | No | UUID of the Leaf user | | `provider` | string | No | Provider name (e.g., `JohnDeere`, `AgLeader`, `Trimble`) | | `status` | string | No | `RECEIVED`, `PROCESSING`, `PROCESSED`, or `FAILED` | | `page` | integer | No | Zero-based page number (default `0`) | | `size` | integer | No | Page size, max 100 (default `10`) | | `sort` | string | No | Comma-separated sort fields with optional `,asc` or `,desc` suffix | ### `get_batch` Returns details for a single batch upload. | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------- | | `batch_id` | string | Yes | UUID of the batch | ### `get_batch_status` Returns the processing status of all files inside a batch. | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------- | | `batch_id` | string | Yes | UUID of the batch | *** ## Weather Weather tools come in two flavors: **field-based** (pass a Leaf user ID and field ID) and **coordinate-based** (pass latitude and longitude). Each flavor supports daily and hourly granularity for both forecasts and historical data. That's eight tools total. All weather tools share these optional parameters: | Parameter | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------- | | `start_time` | string | No | Start of the time range (`YYYY-MM-DD` for daily, ISO-8601 for hourly) | | `end_time` | string | No | End of the time range | | `model` | string | No | Weather model to use | | `units` | string | No | Unit system for results | ### Field-based weather These tools require `leaf_user_id` (string) and `field_id` (string), plus the shared optional parameters above. | Tool | Granularity | Data | | ------------------------------------- | ----------- | ---------- | | `get_weather_forecast_field_daily` | Daily | Forecast | | `get_weather_forecast_field_hourly` | Hourly | Forecast | | `get_weather_historical_field_daily` | Daily | Historical | | `get_weather_historical_field_hourly` | Hourly | Historical | ### Coordinate-based weather These tools require `lat` (float) and `lon` (float), plus the shared optional parameters above. | Tool | Granularity | Data | | --------------------------------------- | ----------- | ---------- | | `get_weather_forecast_lat_lon_daily` | Daily | Forecast | | `get_weather_forecast_lat_lon_hourly` | Hourly | Forecast | | `get_weather_historical_lat_lon_daily` | Daily | Historical | | `get_weather_historical_lat_lon_hourly` | Hourly | Historical | *** ## Billing ### `list_billing_contracts` Returns all billing contracts for the authenticated API owner. Each contract includes product type, start/end dates, and quota. Contract product types: * `AUDIT_FIELDS_BOUNDARY` — active field boundary area (not bounded by contract dates) * `FIELDS_BOUNDARY` — field boundary area consumption within the contract period * `FIELDS_BOUNDARY_SENTERA` — Sentera-exclusive field boundary area * `OPERATIONS_FILE` — machine file area consumption * `OPERATIONS_OPERATION` — field operation area consumption * `SATELLITE_PROCESS_PLANET` — Planet satellite imagery area * `SATELLITE_PROCESS_SENTINEL` — Sentinel satellite imagery area | Parameter | Type | Required | Description | | --------- | ---- | -------- | ------------- | | — | — | — | No parameters | ### `get_billing_contract` Returns details for a specific billing contract. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | -------------------- | | `contract_id` | string | Yes | UUID of the contract | ### `get_contract_consumption` Returns consumption metrics for a contract. Defaults to the current day if no timestamp is specified. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------ | | `contract_id` | string | Yes | UUID of the contract | | `timestamp` | string | No | ISO-8601 timestamp (`YYYY-MM-DDTHH:MM:SS.sssZ`) for a specific day | ### `get_contract_consumption_range_api_owner` Returns daily consumption breakdown for the API owner over a time range. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------- | | `contract_id` | string | Yes | UUID of the contract | | `start_time` | string | Yes | Start time in ISO-8601 format | | `end_time` | string | Yes | End time in ISO-8601 format | ### `get_contract_consumption_range_leaf_user` Returns daily consumption breakdown for a specific Leaf user over a time range. | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | ----------------------------- | | `contract_id` | string | Yes | UUID of the contract | | `target_leaf_user_id` | string | Yes | UUID of the Leaf user | | `start_time` | string | Yes | Start time in ISO-8601 format | | `end_time` | string | Yes | End time in ISO-8601 format | # AgLeader Source: https://docs.withleaf.io/providers/agleader Connect to AgLeader AgFiniti to pull machine files and field operations through Leaf's provider credentials API. Leaf connects to AgLeader using OAuth 2.0 with public/private key pairs. Once connected, Leaf syncs machine files and field operations for the Leaf user. ## Prerequisites 1. An AgLeader developer account. [Create an account](https://www.agleader.com/developers/). 2. Your application's `publicKey` and `privateKey` from AgLeader. 3. A grower's `refreshToken` obtained through the AgLeader OAuth consent flow. ## Setup steps 1. Complete the AgLeader OAuth flow to obtain a `refreshToken` for the grower's account. 2. POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "refreshToken": "grower-refresh-token", "publicKey": "your-public-key", "privateKey": "your-private-key" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "refreshToken": "grower-refresh-token", "publicKey": "your-public-key", "privateKey": "your-private-key" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "refreshToken": "grower-refresh-token", "publicKey": "your-public-key", "privateKey": "your-private-key" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` 3. Leaf validates the token and begins syncing. Check credential status with `GET /users/{leafUserId}/ag-leader-credentials`. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | -------------- | ------ | -------- | -------------------------------------------- | | `refreshToken` | string | Yes | The grower's refresh token | | `publicKey` | string | Yes | Your application's public key from AgLeader | | `privateKey` | string | Yes | Your application's private key from AgLeader | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "accessToken": "str", "refreshToken": "str", "publicKey": "str", "privateKey": "str" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | -------------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/ag-leader-credentials` | | Create credentials | POST | `/users/{leafUserId}/ag-leader-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/ag-leader-credentials` | | Get credential events | GET | `/users/{leafUserId}/ag-leader-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/ag-leader-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Status changes to invalid**: The grower may have revoked access or tokens expired. Have the grower re-authorize through the AgLeader OAuth flow. * **Key mismatch**: Verify that the `publicKey` and `privateKey` are from the same AgLeader application registration. ## What to do next * [Connect AgLeader Tutorial](/guides/tutorials/connect-agleader) — Step-by-step walkthrough. * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # Agvance Source: https://docs.withleaf.io/providers/agvance Connect to SSI Agvance to pull field boundaries, grower data, and farm structure through Leaf's provider credentials API. Leaf connects to Agvance using an API key combined with username/password credentials. Once connected, Leaf syncs growers, farms, and fields from the Agvance system. ## Prerequisites 1. Access to an Agvance instance with API access enabled. 2. Your `apiKey` from Agvance. 3. The `username`, `password`, and `databaseId` for the target Agvance database. 4. Optional: `clientEnvironment` if you need to override the default environment. ## Setup steps POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "apiKey": "your-api-key", "clientEnvironment": "PRODUCTION", "databaseId": "target-database-id", "password": "your-password", "username": "your-username" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/agvance-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/agvance-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "apiKey": "your-api-key", "clientEnvironment": "PRODUCTION", "databaseId": "target-database-id", "password": "your-password", "username": "your-username" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/agvance-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "apiKey": "your-api-key", "clientEnvironment": "PRODUCTION", "databaseId": "target-database-id", "password": "your-password", "username": "your-username" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------------------- | | `apiKey` | string | Yes | Your Agvance API key | | `clientEnvironment` | string | No | `STAGE` or `PRODUCTION`. Defaults to `STAGE` if omitted. | | `databaseId` | string | Yes | The target Agvance database ID | | `username` | string | Yes | Agvance username | | `password` | string | Yes | Agvance password | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "clientEnvironment": "PRODUCTION", "username": "str", "password": "str", "databaseId": "str", "sessionId": "str", "apiKey": "str" } ``` Leaf generates a `sessionId` after successful authentication. If you omit `clientEnvironment`, Leaf defaults it to `STAGE`. ## Confirm the credentials are attached Check the stored credentials for the Leaf user: ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/agvance-credentials" \ -H "Authorization: Bearer YOUR_TOKEN" ``` If this worked, Leaf returns the Agvance credential object with the resolved `sessionId`. ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ------------------------------------------------ | | Get credentials | GET | `/users/{leafUserId}/agvance-credentials` | | Create credentials | POST | `/users/{leafUserId}/agvance-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/agvance-credentials` | | Get credential events | GET | `/users/{leafUserId}/agvance-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/agvance-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/agvance-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/agvance-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Invalid credentials**: If the password changes in Agvance, delete and recreate the credentials with the new password. * **Wrong database ID**: Verify the `databaseId` matches the target Agvance database. * **STAGE vs. PRODUCTION mismatch**: If you set `clientEnvironment` explicitly, make sure it matches the Agvance instance you're connecting to. ## What to do next * [Growers](/fields/growers) for how Agvance grower and farm data appears in Leaf. * [Fields Overview](/fields/overview) for the synced resource hierarchy. * [Provider Credentials API Reference](/api-reference/providers) for the credential path matrix. # CLAAS Source: https://docs.withleaf.io/providers/claas Connect to CLAAS to pull machine files and field operations through Leaf's provider credentials API. Leaf connects to the CLAAS partner API using OAuth 2.0. Once connected, Leaf syncs equipment outbox files (ISO 11783 / ISOXML format) and processes them through the standard operations pipeline. ## Prerequisites 1. OAuth client credentials (`clientKey` and `clientSecret`) for the CLAAS partner/OFT API. 2. A grower's `refreshToken` obtained through the CLAAS OAuth 2.0 consent flow. ## Setup steps 1. Complete the CLAAS OAuth 2.0 flow to obtain a `refreshToken` for the grower's account. 2. POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientKey": "your-client-key", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/claas-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/claas-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "clientKey": "your-client-key", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/claas-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "clientKey": "your-client-key", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` 3. Leaf validates the credentials and begins syncing. Check credential status with `GET /users/{leafUserId}/claas-credentials`. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------------------- | | `clientKey` | string | Yes | Your application's client key from CLAAS | | `clientSecret` | string | Yes | Your application's client secret | | `refreshToken` | string | Yes | The grower's OAuth refresh token | | `clientEnvironment` | string | No | `STAGE` or `PRODUCTION`. Defaults to `STAGE` if omitted | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "clientKey": "str", "clientSecret": "str", "refreshToken": "str", "clientEnvironment": "PRODUCTION" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ---------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/claas-credentials` | | Create credentials | POST | `/users/{leafUserId}/claas-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/claas-credentials` | | Get credential events | GET | `/users/{leafUserId}/claas-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/claas-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/claas-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/claas-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Status changes to invalid**: The grower may have revoked access or the refresh token expired. Have the grower re-authorize through the CLAAS OAuth flow. * **STAGE vs. PRODUCTION mismatch**: Make sure `clientEnvironment` matches the environment your CLAAS app is registered in. If omitted, Leaf defaults to `STAGE`. ## What to do next * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # Climate FieldView Source: https://docs.withleaf.io/providers/climate-fieldview Connect to Climate FieldView to pull field boundaries, machine files, and field operations through Leaf's provider credentials API. Leaf connects to Climate FieldView using OAuth 2.0. Once connected, Leaf syncs farms, fields, machine files, and field operations for the Leaf user. Climate FieldView does not have a grower-level hierarchy, so data syncs at the farm and field level. ## Prerequisites 1. A Climate FieldView developer account. [Become a partner](https://dev.fieldview.com/join-us/). 2. Your application's `clientId`, `clientSecret`, and `apiKey` from Climate FieldView. 3. A grower's `refreshToken` obtained through the Climate FieldView OAuth 2.0 consent flow. ## Setup steps 1. Complete the Climate FieldView OAuth 2.0 flow to obtain a `refreshToken` for the grower's account. 2. POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "your-client-id", "clientSecret": "your-client-secret", "apiKey": "your-api-key", "refreshToken": "grower-refresh-token" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "apiKey": "your-api-key", "refreshToken": "grower-refresh-token" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "apiKey": "your-api-key", "refreshToken": "grower-refresh-token" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` 3. Leaf validates the token and begins syncing. Check credential status with `GET /users/{leafUserId}/climate-field-view-credentials`. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------------------------- | | `clientId` | string | Yes | Your application's client ID from Climate FieldView | | `clientSecret` | string | Yes | Your application's client secret | | `apiKey` | string | Yes | Your application's API key | | `refreshToken` | string | Yes | The grower's OAuth refresh token | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "tokenMetadata": {"scopes": ["str"]}, "clientId": "str", "clientSecret": "str", "apiKey": "str", "refreshToken": "str", "accessToken": "str" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ----------------------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/climate-field-view-credentials` | | Create credentials | POST | `/users/{leafUserId}/climate-field-view-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/climate-field-view-credentials` | | Get credential events | GET | `/users/{leafUserId}/climate-field-view-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/climate-field-view-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Status changes to invalid**: The grower may have revoked access in Climate FieldView, or the refresh token expired. Have the grower re-authorize. * **No grower hierarchy**: Climate FieldView does not expose a grower level in its FMIS structure. Data is organized by farms and fields only. ## What to do next * [Connect Climate FieldView Tutorial](/guides/tutorials/connect-climate-fieldview) — Step-by-step walkthrough. * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # CNHi (AFS Connect) Source: https://docs.withleaf.io/providers/cnhi Connect to CNHi (Case IH, New Holland) to pull field boundaries, machine files, and field operations through Leaf's provider credentials API. This page covers the **legacy CNHI (AFS Connect)** provider. For CNH Industrial's newer FieldOps API, see [CNHI FieldOps](/providers/cnhi-fieldops). CNH Industrial (Case IH, New Holland, Steyr) exposes two API platforms. Leaf supports both as separate providers: **CNHI (AFS Connect)** (this page) and **[CNHI FieldOps](/providers/cnhi-fieldops)**. If you're starting a new integration, use CNHI FieldOps. Leaf connects to CNHI AFS Connect using OAuth 2.0. Once connected, Leaf syncs growers, farms, fields, machine files, and field operations. ## Prerequisites 1. A CNHi developer account. [Register here](https://www.developer.cnhindustrial.com/). 2. Your application's `clientId`, `clientSecret`, and `subscriptionKey` from CNHi. 3. A grower's `refreshToken` obtained through the CNHi OAuth 2.0 consent flow. ## Setup steps 1. Complete the CNHi OAuth 2.0 flow to obtain a `refreshToken` for the grower's account. 2. POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "your-client-id", "clientSecret": "your-client-secret", "subscriptionKey": "your-subscription-key", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "subscriptionKey": "your-subscription-key", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "subscriptionKey": "your-subscription-key", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` 3. Leaf validates the token and begins syncing. Check credential status with `GET /users/{leafUserId}/cnhi-credentials`. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------- | | `clientId` | string | Yes | Your application's client ID from CNHi | | `clientSecret` | string | Yes | Your application's client secret | | `subscriptionKey` | string | Yes | Your CNHi subscription key | | `refreshToken` | string | Yes | The grower's OAuth refresh token | | `clientEnvironment` | string | Yes | `STAGE` or `PRODUCTION` | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "clientId": "str", "clientSecret": "str", "refreshToken": "str", "clientEnvironment": "PRODUCTION", "subscriptionKey": "str" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | --------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/cnhi-credentials` | | Create credentials | POST | `/users/{leafUserId}/cnhi-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/cnhi-credentials` | | Get credential events | GET | `/users/{leafUserId}/cnhi-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Status changes to invalid**: The grower may have revoked access or the refresh token expired. Have the grower re-authorize. * **STAGE vs. PRODUCTION mismatch**: Make sure `clientEnvironment` matches the environment your CNHi app is registered in. * **Missing subscription key**: CNHi requires a `subscriptionKey` in addition to OAuth credentials. Verify you're passing it in the request body. ## What to do next * [Connect CNHi Tutorial](/guides/tutorials/connect-cnhi) — Step-by-step walkthrough. * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # CNHI FieldOps Source: https://docs.withleaf.io/providers/cnhi-fieldops Connect to CNHI FieldOps (Case IH, New Holland) to pull field boundaries, machine files, and field operations through Leaf's provider credentials API. This page covers CNHI FieldOps, CNH Industrial's current API platform. For the legacy CNHI (AFS Connect) integration, see [CNHI (AFS Connect)](/providers/cnhi). CNH Industrial (Case IH, New Holland, Steyr) exposes two API platforms. Leaf supports both as separate providers: **[CNHI (AFS Connect)](/providers/cnhi)** and **CNHI FieldOps** (this page). CNHI FieldOps is the current platform — use it for new integrations. Leaf connects to CNHI FieldOps using OAuth 2.0. Once connected, Leaf syncs growers, farms, fields, machine files, and field operations. ## Prerequisites 1. A CNH developer account registered with a **company-domain email** (generic email domains like Gmail and Hotmail are not supported). [Register here](https://develop.cnh.com/). 2. A FieldOps application registered in the CNH Developer Portal, with your `clientId`, `clientSecret`, and `subscriptionKey`. 3. A grower's `refreshToken` obtained through the CNHI FieldOps OAuth 2.0 consent flow. Existing CNHI (AFS Connect) subscription keys do not work with FieldOps. You must obtain new credentials from the CNH Developer Portal for the FieldOps API. ## Setup steps 1. Complete the CNHI FieldOps OAuth 2.0 flow to obtain a `refreshToken` for the grower's account. 2. POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "your-client-id", "clientSecret": "your-client-secret", "subscriptionKey": "your-subscription-key", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "subscriptionKey": "your-subscription-key", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "subscriptionKey": "your-subscription-key", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` 3. Leaf validates the token and begins syncing. Check credential status with `GET /users/{leafUserId}/cnhi-field-ops-credentials`. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------------------------- | | `clientId` | string | Yes | Your application's client ID from the CNH Developer Portal | | `clientSecret` | string | Yes | Your application's client secret | | `subscriptionKey` | string | Yes | Your FieldOps subscription key (not reusable from legacy CNHI) | | `refreshToken` | string | Yes | The grower's OAuth refresh token | | `clientEnvironment` | string | No | `STAGE` or `PRODUCTION`. Defaults to `STAGE` | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "clientId": "str", "clientSecret": "str", "refreshToken": "str", "clientEnvironment": "PRODUCTION", "subscriptionKey": "str" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ------------------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/cnhi-field-ops-credentials` | | Create credentials | POST | `/users/{leafUserId}/cnhi-field-ops-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/cnhi-field-ops-credentials` | | Get credential events | GET | `/users/{leafUserId}/cnhi-field-ops-credentials/events` | ## Data sync behavior CNHI FieldOps supports webhook-based change notifications. When new files, operations, or field changes occur in a grower's FieldOps account, CNH pushes events to Leaf. This reduces sync latency compared to the legacy CNHI provider, which relies on periodic polling. Leaf manages webhook subscriptions automatically for each connected account. No additional configuration is needed. ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/cnhi-field-ops-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Status changes to invalid**: The grower may have revoked access or the refresh token expired. Have the grower re-authorize through the FieldOps OAuth flow. * **STAGE vs. PRODUCTION mismatch**: Make sure `clientEnvironment` matches the environment your CNHI FieldOps app is registered in. * **Missing or invalid subscription key**: FieldOps requires a new `subscriptionKey` separate from any legacy CNHI keys. Verify you're passing the correct key. * **Account mismatch**: The grower's FieldOps account may not exist in the environment you specified. Staging and production identities are separate. * **Grower cannot authorize**: The grower must have the **Farm Manager** title in their FieldOps account and must have logged into the [FieldOps portal](https://develop.cnh.com/get-started/fieldops-portal) at least once before they can complete the OAuth consent flow. ## How FieldOps data appears in Leaf's APIs CNHI FieldOps data flows through the same Leaf endpoints as every other provider — fields, files, and operations. The `provider` field on each object identifies the source: * Fields from FieldOps: `"provider": "CNHIFieldOps"` * Machine files from FieldOps: `"provider": "CNHIFieldOps"` * Field operations from FieldOps: `"provider": "CNHIFieldOps"` Legacy CNHI (AFS Connect) data continues to show `"provider": "CNHI"`. You can filter by provider when querying: ```bash theme={null} curl "https://api.withleaf.io/services/fields/api/fields?provider=CNHIFieldOps" \ -H "Authorization: Bearer YOUR_TOKEN" ``` The data structure, output format (GeoJSON/GeoParquet), and field operation merging behavior are identical regardless of provider. The only difference is the `provider` value on the returned objects. If a Leaf user has both `cnhi-credentials` and `cnhi-field-ops-credentials` attached, the same physical field may appear twice — once with `"provider": "CNHI"` and once with `"provider": "CNHIFieldOps"`. Remove the legacy credential after confirming the FieldOps connection to avoid duplicates. ## What to do next * [Connect CNHI FieldOps Tutorial](/guides/tutorials/connect-cnhi-fieldops) — Step-by-step walkthrough. * [Migrate from CNHI to CNHI FieldOps](/guides/tutorials/migrate-cnhi-to-fieldops) — For customers moving from the legacy provider. * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # John Deere Source: https://docs.withleaf.io/providers/john-deere Connect to John Deere Operations Center to pull field boundaries, machine files, and field operations through Leaf's provider credentials API. Leaf connects to John Deere Operations Center using OAuth 2.0. Once connected, Leaf syncs fields, machine files, and field operations for the Leaf user. ## Prerequisites 1. A John Deere developer account. [Register here](https://account.deere.com/actmgmt/onboarding/registration). 2. An application registered in the [My Applications](https://developer.deere.com/#/applications) portal with the appropriate scopes and redirect URIs configured. Make sure **Webhook Read** and **Webhook Write** permissions are enabled under **Operations Center - Webhook** — this allows Leaf to receive real-time data notifications instead of relying solely on polling. 3. Your application's `clientKey` and `clientSecret` from John Deere. 4. A grower's `refreshToken` obtained through the John Deere OAuth 2.0 consent flow. John Deere accounts can span multiple organizations. By default, Leaf syncs data from all organizations the account has access to. Use the `organizationDataSync` configuration to limit this to specific organizations. ## Setup steps 1. Complete the John Deere OAuth 2.0 flow to obtain a `refreshToken` for the grower's account. 2. POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientKey": "your-client-key", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "clientKey": "your-client-key", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "clientKey": "your-client-key", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` 3. Leaf validates the token and begins syncing. Check credential status with `GET /users/{leafUserId}/john-deere-credentials`. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------- | ------ | -------- | --------------------------------------------- | | `clientKey` | string | Yes | Your application's client key from John Deere | | `clientSecret` | string | Yes | Your application's client secret | | `refreshToken` | string | Yes | The grower's OAuth refresh token | | `clientEnvironment` | string | Yes | `STAGE` or `PRODUCTION` | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "tokenMetadata": {"scopes": ["str"]}, "clientKey": "str", "clientSecret": "str", "accessToken": "str", "refreshToken": "str", "clientEnvironment": "PRODUCTION" } ``` The `status` field reflects the current health of the credential. If Leaf detects during background processing that the credential is no longer valid, the status changes accordingly. ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | --------------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/john-deere-credentials` | | Create credentials | POST | `/users/{leafUserId}/john-deere-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/john-deere-credentials` | | Get credential events | GET | `/users/{leafUserId}/john-deere-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/john-deere-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Each event includes a `body`, `headers`, `statusCode`, and `createdDate`. Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Status changes to invalid**: The grower may have revoked access in John Deere Operations Center, or the refresh token expired. Have the grower re-authorize through the OAuth flow. * **Missing organizations**: If expected data isn't appearing, check whether `organizationDataSync` is set to `SELECTED_ONLY` and verify the correct organizations are selected. * **STAGE vs. PRODUCTION mismatch**: Make sure `clientEnvironment` matches the environment your John Deere app is registered in. ## What to do next * [Connect John Deere Tutorial](/guides/tutorials/connect-john-deere) — Step-by-step walkthrough. * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # Lindsay Source: https://docs.withleaf.io/providers/lindsay Connect to Lindsay FieldNET to pull irrigation activity data through Leaf's provider credentials API. Leaf connects to Lindsay FieldNET using OAuth 2.0 credentials. Once connected, Leaf syncs irrigation activity data for the Leaf user. ## Prerequisites 1. A Lindsay developer account with API access. 2. Your application's `clientId` and `clientSecret` from Lindsay. 3. A grower's `refreshToken` obtained through the Lindsay OAuth 2.0 consent flow. 4. The `clientEnvironment` for your integration: `STAGE` or `PRODUCTION`. ## Credentials schema | Field | Type | Required | Description | | ------------------- | ------ | -------- | --------------------------------------- | | `clientId` | string | Yes | Your Lindsay application client ID. | | `clientSecret` | string | Yes | Your Lindsay application client secret. | | `refreshToken` | string | Yes | The grower's OAuth refresh token. | | `clientEnvironment` | string | Yes | `STAGE` or `PRODUCTION`. | ## Endpoints | Method | Path | Description | | ------ | ------------------------------------------------ | ------------------------------------------ | | GET | `/users/{leafUserId}/lindsay-credentials` | Get stored credentials. | | POST | `/users/{leafUserId}/lindsay-credentials` | Create credentials. | | DELETE | `/users/{leafUserId}/lindsay-credentials` | Delete credentials. | | GET | `/users/{leafUserId}/lindsay-credentials/events` | Get credential events for troubleshooting. | Base URL: `https://api.withleaf.io/services/usermanagement/api` ### Create credentials ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "your-client-id", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/lindsay-credentials' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/lindsay-credentials" headers = {"Authorization": f"Bearer {TOKEN}"} payload = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", "clientEnvironment": "PRODUCTION", } response = requests.post(endpoint, headers=headers, json=payload) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/lindsay-credentials"; const headers = { Authorization: `Bearer ${TOKEN}` }; const payload = { clientId: "your-client-id", clientSecret: "your-client-secret", refreshToken: "grower-refresh-token", clientEnvironment: "PRODUCTION", }; axios.post(endpoint, payload, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ### Response ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "2026-01-15T12:00:00.000000Z", "tokenMetadata": { "scopes": ["str"] }, "clientId": "your-client-id", "clientSecret": "your-client-secret", "clientEnvironment": "PRODUCTION", "accessToken": "str", "refreshToken": "grower-refresh-token" } ``` ## Troubleshooting Use the events endpoint to check credential health. Events are retained for 30 days and are deleted when the credential is removed. ``` GET /users/{leafUserId}/lindsay-credentials/events ``` ## What to do next * [Irrigation Overview](/irrigation/overview) for details on Lindsay and Valley irrigation data. * [API Reference: Providers](/api-reference/providers) for the full credential path matrix. # Provider Organizations Source: https://docs.withleaf.io/providers/organizations Review connected provider organizations and control which John Deere Operations Center organizations sync data for each Leaf user. Some providers expose an organization layer above growers, farms, and fields. Leaf surfaces those organizations so you can see which ones are connected. Leaf's organization sync-management endpoints are John Deere-only. ## Organization endpoint groups Leaf exposes two related sets of organization endpoints: ### 1. Provider organization list Use this endpoint to see which organizations are connected or not connected for a provider account. `GET /users/{leafUserId}/organizations/{provider}` * Supported providers: `JohnDeere`, `Trimble` * John Deere returns both connected and not-connected organizations. * Trimble does not distinguish between connected and not-connected organizations. Example response: ```json theme={null} { "connectedOrganizations": [ { "id": "organization_id_1", "name": "Organization Name 1", "managementUri": "https://connections.deere.com/connections/clientKey/connections-dialog?orgId=organization_id_1" } ], "notConnectedOrganizations": [ { "id": "organization_id_2", "name": "Organization Name 2", "managementUri": "https://connections.deere.com/connections/clientKey/connections-dialog?orgId=organization_id_2" } ] } ``` ### 2. Provider organization sync management These endpoints manage the organizations Leaf is allowed to process for a connected provider account. These sync-management endpoints are `JohnDeere`-only. The `{provider}` value for this endpoint group must be `JohnDeere`. | Action | Method | Path | | ----------------------------- | ------ | ----------------------------------------------------------------------- | | List provider organizations | GET | `/users/{leafUserId}/{provider}/organizations` | | Get one provider organization | GET | `/users/{leafUserId}/{provider}/organizations/{providerOrgId}` | | Update organization status | PATCH | `/users/{leafUserId}/{provider}/organizations/{providerOrgId}/{status}` | | Sync provider organizations | POST | `/users/{leafUserId}/{provider}/organizations/sync` | ## Provider organization resource ```json theme={null} { "providerOrgId": "520674381", "providerOrgName": "Leaf Farms", "status": "SELECTED", "managementUri": "https://connections.deere.com/connections/clientKey/connections-dialog?orgId=Leaf Farms" } ``` * `providerOrgId` is the provider's organization ID. * `providerOrgName` is the provider's organization name. * `managementUri` is the provider-side URL for reviewing or fixing the app-to-organization connection. * `status` controls whether Leaf processes data from that organization. ## Organization statuses | Status | Meaning | | ---------- | ---------------------------------------------------------------------------------------- | | `SELECTED` | Leaf processes resources from this organization. | | `PREVIEW` | Leaf keeps the organization visible but does not process its downstream resources. | | `BLOCKED` | The app does not currently have the required provider-side access for this organization. | If a John Deere `managementUri` contains `connections-dialog`, the app-to-organization connection is established. If it contains `select-organizations`, the setup is incomplete and the organization remains unavailable for processing until you update the provider-side connection and run a sync again. ## Controlling sync scope The `organizationDataSync` configuration controls whether Leaf syncs every available organization or only the organizations you explicitly select: * `ALL` syncs every organization the account can access. * `SELECTED_ONLY` syncs only organizations you mark as `SELECTED`. You can combine this with `customDataSync` to limit both organization scope and field-level processing. John Deere accounts often have access to many organizations. If you leave `organizationDataSync` at `ALL`, Leaf may process much more data than you expect. Use `SELECTED_ONLY` when you need tighter control over sync scope and billing. ## Verifying what synced Use the Integrations Resources endpoint to confirm the amount of data currently available for a Leaf user: `GET https://api.withleaf.io/services/integrations/api/resources` Filter by `provider` and `leafUserId` to see grower, farm, and field counts per provider. This is a good way to confirm that your organization and field sync settings are producing the expected resource counts. ## What to do next * Review [Configuration](/configuration/overview) for `organizationDataSync` and `customDataSync`. * See [Organizations API Reference](/api-reference/organizations) for endpoint paths and methods. * See [Integrations API Reference](/api-reference/integrations) for provider resource summaries. * If you are connecting John Deere accounts with many organizations, set `organizationDataSync` to `SELECTED_ONLY` before scaling the integration. # Provider Authentication Source: https://docs.withleaf.io/providers/overview Connect to providers like John Deere Operations Center, Climate FieldView, CNHi, CNHI FieldOps, and Trimble through Leaf's unified credentials API. Leaf connects to agricultural data providers on behalf of your users. You store provider credentials on a Leaf user, and Leaf handles token refresh, data synchronization, and retry logic from that point forward. ## How it works Each provider requires its own set of credentials, typically obtained through an OAuth 2.0 flow that your application manages or that Leaf's Magic Link widget handles for you. The general sequence: 1. Register as a developer/partner with the provider and receive your app-level credentials (client ID, client secret, API keys, etc.). 2. Guide the grower through the provider's OAuth consent flow to obtain a refresh token. 3. POST those credentials to the appropriate Leaf endpoint for the Leaf user: `/users/{leafUserId}/{provider}-credentials`. 4. Leaf validates the credentials, begins syncing fields and machine files, and keeps the token refreshed automatically. All provider credential endpoints live under the User Management service: ``` https://api.withleaf.io/services/usermanagement/api ``` Each provider follows the same three-endpoint pattern: | Action | Method | Path | | ------------------ | ------ | -------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/{provider}-credentials` | | Create credentials | POST | `/users/{leafUserId}/{provider}-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/{provider}-credentials` | ## Authentication patterns Most providers use OAuth 2.0 with a refresh token. You complete the OAuth flow on your side (or use Magic Link) and pass the resulting tokens to Leaf. Leaf exchanges and refreshes tokens as needed. Some providers use API key authentication instead of OAuth. Sentera uses username/password. Raven Slingshot uses an API key with access key and shared secret. The credential schema for each provider is documented on its respective page. ## Environments Several providers offer sandbox or staging environments alongside production. John Deere, CNHi, CNHI FieldOps, Agvance, and Precision Planting Panorama support a `clientEnvironment` field that accepts `STAGE` or `PRODUCTION`. Set this when creating credentials. Leaf does not provide a separate test environment. Use distinct API owners (e.g., `leaf-test@company.com` vs. `leaf-production@company.com`) to separate test and production activity. Testing against large accounts without using `customDataSync` can consume acre allotments quickly. ## What happens after connection Once credentials are attached to a Leaf user: 1. Leaf syncs the provider's field structure (growers, farms, fields) based on your `fieldsAutoSync` configuration. 2. Machine files are fetched and converted to Leaf's standard canonical format (available as GeoJSON or GeoParquet). 3. Field operations are created by spatially allocating machine file data against field boundaries. 4. Subsequent syncs run at least every 24 hours. Providers with event-driven APIs (like John Deere) trigger syncs sooner. You can monitor credential health using the events endpoint available for each provider: `GET /users/{leafUserId}/{provider}-credentials/events`. Event logs are retained for 30 days. ## Connecting via Magic Link If you don't want to build the OAuth flow yourself, Leaf provides Magic Link and Leaf Link widgets. These handle the provider consent flow and credential storage automatically. You register your provider app keys with Leaf's `/app-keys/` endpoints, then generate a link or embed the widget. See the [Magic Link documentation](/components/magic-link) for setup details. ## Supported providers | Provider | Auth type | Environments | Credential endpoint suffix | | ------------------------------------------------- | --------------------- | ----------------- | -------------------------------- | | [John Deere](/providers/john-deere) | OAuth 2.0 | STAGE, PRODUCTION | `john-deere-credentials` | | [CLAAS](/providers/claas) | OAuth 2.0 | STAGE, PRODUCTION | `claas-credentials` | | [Climate FieldView](/providers/climate-fieldview) | OAuth 2.0 | Production only | `climate-field-view-credentials` | | [CNHI (AFS Connect - Legacy)](/providers/cnhi) | OAuth 2.0 | STAGE, PRODUCTION | `cnhi-credentials` | | [CNHI FieldOps](/providers/cnhi-fieldops) | OAuth 2.0 | STAGE, PRODUCTION | `cnhi-field-ops-credentials` | | [Trimble](/providers/trimble) | OAuth 2.0 | Production only | `trimble-credentials` | | [AgLeader](/providers/agleader) | OAuth 2.0 | Production only | `ag-leader-credentials` | | [Raven](/providers/raven) | OAuth 2.0 | Production only | `raven-credentials` | | [Raven Slingshot](/providers/raven-slingshot) | API key | Production only | `raven-slingshot-credentials` | | [Stara](/providers/stara) | API key + OAuth | Production only | `stara-credentials` | | [Sentera](/providers/sentera) | Username/password | Production only | `sentera-credentials` | | [Agvance](/providers/agvance) | API key + credentials | STAGE, PRODUCTION | `agvance-credentials` | | [Panorama](/providers/panorama) | OAuth 2.0 (Cognito) | STAGE, PRODUCTION | `panorama-credentials` | ## Common use cases * **Multi-provider ingestion**: Connect a grower's John Deere Operations Center, Climate FieldView, CNHi, CNHI FieldOps, and other accounts to a single Leaf user and receive all their data in one standard format. * **Grower onboarding**: Use Magic Link to let growers connect their own provider accounts without your app handling OAuth flows directly. * **Credential monitoring**: Subscribe to credential alert events to detect token expirations or revoked access before it affects data flow. * **Organization scoping**: For John Deere accounts with many organizations, use `organizationDataSync` to limit which organizations Leaf processes. ## What to do next * Set up credentials for your first provider. We recommend starting with [John Deere](/providers/john-deere) or [Climate FieldView](/providers/climate-fieldview) as they are the most common. * Configure [alerts](/alerts/overview) to receive notifications when credentials change status. * Review [organizations](/providers/organizations) if you need to control which provider organizations sync data. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # Precision Planting Panorama Source: https://docs.withleaf.io/providers/panorama Connect to Precision Planting Panorama to pull field boundaries, machine files, and field operations through Leaf's provider credentials API. Leaf connects to Precision Planting Panorama using OAuth 2.0 via AWS Cognito. Once connected, Leaf syncs growers, farms, fields, machine files, and field operations. The recommended way to create Panorama credentials is through the one-click integration endpoint: `POST /users/{leafUserId}/one-click-integration/Panorama`. This handles the Cognito token exchange and sharing handshake automatically. ## Prerequisites 1. A Precision Planting Panorama developer/partner account. 2. Your application's `clientId` from the Panorama developer portal. 3. The partner's `username` and `password` from your Panorama account. 4. A `refreshToken` obtained through the Cognito authentication flow, or use the one-click integration endpoint which handles this for you. 5. The grower's organizationCode, pre-authorized to share data with your Panorama partner application (manual credential creation only). ## Setup steps **Option A: One-click integration (recommended)** Use the one-click integration endpoint, which handles the Cognito token exchange automatically: ``` POST /users/{leafUserId}/one-click-integration/Panorama ``` One-click start request: ```json theme={null} { "clientId": "your-client-id", "username": "partner-username", "password": "partner-password", "clientEnvironment": "PRODUCTION", "sharingUrlId": "your-custom-sharing-url-id" } ``` * `organizationCode` is not requried for the one-click flow. * Set the `sharingUrlId` in both the one-click request body and your Panorama app key configuration. * Set the Sharing Confirmation URL to `https://widget.withleaf.io` in the Panorama Partner Portal so Leaf can receive the callback and complete credential attachment. * The `sharingUrlId` is the UUID contained in the Share Initiation URL and can be obtained through the Panorama Partner Portal under the Account Details tab. Panorama application information **Option B: Manual credential creation** If you manage the Cognito flow yourself, POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "your-client-id", "username": "partner-username", "password": "partner-password", "organizationCode": "grower-org-code", "refreshToken": "cognito-refresh-token", "clientEnvironment": "PRODUCTION" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/panorama-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/panorama-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "clientId": "your-client-id", "username": "partner-username", "password": "partner-password", "organizationCode": "grower-org-code", "refreshToken": "cognito-refresh-token", "clientEnvironment": "PRODUCTION" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/panorama-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "clientId": "your-client-id", "username": "partner-username", "password": "partner-password", "organizationCode": "grower-org-code", "refreshToken": "cognito-refresh-token", "clientEnvironment": "PRODUCTION" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ## Confirm the credentials are attached Check the stored credentials for the Leaf user: ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/panorama-credentials" \ -H "Authorization: Bearer YOUR_TOKEN" ``` If this worked, Leaf returns the Panorama credential object for the Leaf user. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------ | | `clientId` | string | Yes | Your application's client ID from Panorama | | `username` | string | Yes | The partner's Panorama username | | `password` | string | Yes | The partner's Panorama password | | `organizationCode` | string | Yes | The grower's organization code in Panorama | | `refreshToken` | string | Yes | Cognito refresh token | | `clientEnvironment` | string | Yes | `STAGE` or `PRODUCTION` | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "clientId": "str", "username": "str", "organizationCode": "str", "clientEnvironment": "PRODUCTION", "accessToken": "str", "refreshToken": "str" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ------------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/panorama-credentials` | | Create credentials | POST | `/users/{leafUserId}/panorama-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/panorama-credentials` | | Get credential events | GET | `/users/{leafUserId}/panorama-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/panorama-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/panorama-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/panorama-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Cognito token expiry**: Panorama uses AWS Cognito for auth. If the credential becomes invalid, the grower may need to re-authenticate. Using the one-click integration endpoint avoids much of this complexity. * **Wrong organization code**: For manual credential creation, verify the `organizationCode` is already authorized to share data with your Panorama partner application. * **STAGE vs. PRODUCTION mismatch**: Make sure `clientEnvironment` matches your Panorama setup. ## What to do next * [Field Operations Quickstart](/guides/tutorials/field-operations-quickstart) for checking synced field and operation data. * [Provider Organizations](/providers/organizations) if you need to review sync scope after connection. * [Provider Credentials API Reference](/api-reference/providers) for the credential path matrix. # Raven Source: https://docs.withleaf.io/providers/raven Connect to Raven using OAuth 2.0 to pull grower, farm, and field data through Leaf's provider credentials API. Leaf connects to Raven using OAuth 2.0 credentials. Once connected, Leaf syncs grower, farm, and field data for the Leaf user. This is separate from [Raven Slingshot](/providers/raven-slingshot), which uses API key credentials for machine file ingestion. ## Prerequisites 1. A Raven developer account with OAuth application credentials. 2. Your application's `clientId` and `clientSecret` from Raven. 3. A grower's `refreshToken` obtained through the Raven OAuth 2.0 consent flow. ## Credentials schema | Field | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------- | | `clientId` | string | Yes | Your Raven application client ID. | | `clientSecret` | string | Yes | Your Raven application client secret. | | `refreshToken` | string | Yes | The grower's OAuth refresh token. | ## Endpoints | Method | Path | Description | | ------ | ---------------------------------------------- | ------------------------------------------ | | GET | `/users/{leafUserId}/raven-credentials` | Get stored credentials. | | POST | `/users/{leafUserId}/raven-credentials` | Create credentials. | | DELETE | `/users/{leafUserId}/raven-credentials` | Delete credentials. | | GET | `/users/{leafUserId}/raven-credentials/events` | Get credential events for troubleshooting. | Base URL: `https://api.withleaf.io/services/usermanagement/api` ### Create credentials ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "your-client-id", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/raven-credentials' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/raven-credentials" headers = {"Authorization": f"Bearer {TOKEN}"} payload = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token", } response = requests.post(endpoint, headers=headers, json=payload) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/raven-credentials"; const headers = { Authorization: `Bearer ${TOKEN}` }; const payload = { clientId: "your-client-id", clientSecret: "your-client-secret", refreshToken: "grower-refresh-token", }; axios.post(endpoint, payload, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ### Response ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "2026-01-15T12:00:00.000000Z", "clientId": "your-client-id", "clientSecret": "your-client-secret", "accessToken": "str", "refreshToken": "grower-refresh-token" } ``` ## Troubleshooting Use the events endpoint to check credential health. Events are retained for 30 days and are deleted when the credential is removed. ``` GET /users/{leafUserId}/raven-credentials/events ``` ## What to do next * [Raven Slingshot](/providers/raven-slingshot) for API key-based machine file ingestion. * [API Reference: Providers](/api-reference/providers) for the full credential path matrix. # Raven Slingshot Source: https://docs.withleaf.io/providers/raven-slingshot Connect to Raven Slingshot to pull machine file data through Leaf's provider credentials API using API key authentication. Leaf connects to Raven Slingshot using API key credentials. Once connected, Leaf ingests machine files from Slingshot for the Leaf user. This is separate from [Raven](/providers/raven), which uses OAuth 2.0 credentials for grower, farm, and field data. ## Prerequisites 1. A Raven Slingshot portal account. 2. Your `apiKey`, `accessKey`, and `sharedSecret` from the Slingshot portal. ## Credentials schema | Field | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------- | | `apiKey` | string | Yes | Your Slingshot API key. | | `accessKey` | string | Yes | Your Slingshot access key. | | `sharedSecret` | string | Yes | Your Slingshot shared secret. | ## Endpoints | Method | Path | Description | | ------ | -------------------------------------------------------- | ------------------------------------------ | | GET | `/users/{leafUserId}/raven-slingshot-credentials` | Get stored credentials. | | POST | `/users/{leafUserId}/raven-slingshot-credentials` | Create credentials. | | DELETE | `/users/{leafUserId}/raven-slingshot-credentials` | Delete credentials. | | GET | `/users/{leafUserId}/raven-slingshot-credentials/events` | Get credential events for troubleshooting. | Base URL: `https://api.withleaf.io/services/usermanagement/api` ### Create credentials ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "apiKey": "your-api-key", "accessKey": "your-access-key", "sharedSecret": "your-shared-secret" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/raven-slingshot-credentials' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/raven-slingshot-credentials" headers = {"Authorization": f"Bearer {TOKEN}"} payload = { "apiKey": "your-api-key", "accessKey": "your-access-key", "sharedSecret": "your-shared-secret", } response = requests.post(endpoint, headers=headers, json=payload) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/raven-slingshot-credentials"; const headers = { Authorization: `Bearer ${TOKEN}` }; const payload = { apiKey: "your-api-key", accessKey: "your-access-key", sharedSecret: "your-shared-secret", }; axios.post(endpoint, payload, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ### Response ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "2026-01-15T12:00:00.000000Z", "apiKey": "your-api-key", "accessKey": "your-access-key" } ``` ## Troubleshooting Use the events endpoint to check credential health. Events are retained for 30 days and are deleted when the credential is removed. ``` GET /users/{leafUserId}/raven-slingshot-credentials/events ``` ## What to do next * [Raven](/providers/raven) for OAuth-based grower, farm, and field data. * [API Reference: Providers](/api-reference/providers) for the full credential path matrix. # Sentera Source: https://docs.withleaf.io/providers/sentera Connect to Sentera to pull field boundary data through Leaf's provider credentials API using username and password authentication. Leaf connects to Sentera using username/password authentication rather than OAuth. Once connected, Leaf syncs field boundary data for the Leaf user. Sentera is primarily used for field boundary management. ## Prerequisites 1. A Sentera account with the target organization. 2. The grower's `username` and `password` for their Sentera account. 3. The `organizationName` within Sentera that the grower belongs to. ## Setup steps POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "username": "grower-username", "password": "grower-password", "organizationName": "grower-org-name" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/sentera-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/sentera-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "username": "grower-username", "password": "grower-password", "organizationName": "grower-org-name" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/sentera-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "username": "grower-username", "password": "grower-password", "organizationName": "grower-org-name" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------ | ------ | -------- | ----------------------------- | | `username` | string | Yes | The grower's Sentera username | | `password` | string | Yes | The grower's Sentera password | | `organizationName` | string | Yes | The Sentera organization name | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "username": "str", "authToken": "str", "organizationId": "str", "organizationName": "str" } ``` Leaf generates an `authToken` and resolves the `organizationId` from the provided `organizationName` after successful authentication. ## Confirm the credentials are attached Check the stored credentials for the Leaf user: ```bash theme={null} curl "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/sentera-credentials" \ -H "Authorization: Bearer YOUR_TOKEN" ``` If this worked, Leaf returns the Sentera credential object with the resolved `organizationId`. ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ------------------------------------------------ | | Get credentials | GET | `/users/{leafUserId}/sentera-credentials` | | Create credentials | POST | `/users/{leafUserId}/sentera-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/sentera-credentials` | | Get credential events | GET | `/users/{leafUserId}/sentera-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/sentera-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/sentera-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/sentera-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Invalid credentials**: If the grower changed their Sentera password, the credentials stored in Leaf become invalid. Delete and recreate with the new password. * **Organization not found**: Verify the `organizationName` matches exactly what's configured in Sentera, including case sensitivity. ## What to do next * [Fields Overview](/fields/overview) for how Sentera field boundaries appear in Leaf. * [Managing Fields](/fields/managing-fields) for working with the synced boundaries. * [Provider Credentials API Reference](/api-reference/providers) for the credential path matrix. # Stara Source: https://docs.withleaf.io/providers/stara Connect to Stara Telemetry to pull machine files and field operations through Leaf's provider credentials API. Leaf connects to Stara using an API key combined with OAuth tokens. Once connected, Leaf syncs fields, machine files, and field operations. Stara's FMIS structure exposes fields only (no separate grower or farm hierarchy). ## Prerequisites 1. A Stara developer/partner account with API access. 2. Your `apiKey` from Stara. 3. The grower's `accessToken`, `accessTokenClient`, and `refreshToken` from the Stara authentication flow. ## Setup steps POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "apiKey": "your-api-key", "accessToken": "grower-access-token", "accessTokenClient": "grower-access-token-client", "refreshToken": "grower-refresh-token" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "apiKey": "your-api-key", "accessToken": "grower-access-token", "accessTokenClient": "grower-access-token-client", "refreshToken": "grower-refresh-token" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "apiKey": "your-api-key", "accessToken": "grower-access-token", "accessTokenClient": "grower-access-token-client", "refreshToken": "grower-refresh-token" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` ## Credentials schema **Create request body:** | Field | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------- | | `apiKey` | string | Yes | Your Stara API key | | `accessToken` | string | Yes | The grower's access token | | `accessTokenClient` | string | Yes | The grower's client access token | | `refreshToken` | string | Yes | The grower's refresh token | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "accessTokenClient": "str", "refreshToken": "str", "accessToken": "str", "apiKey": "str" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ---------------------------------------------- | | Get credentials | GET | `/users/{leafUserId}/stara-credentials` | | Create credentials | POST | `/users/{leafUserId}/stara-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/stara-credentials` | | Get credential events | GET | `/users/{leafUserId}/stara-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/stara-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **No grower/farm hierarchy**: Stara only exposes fields. If you expect grower or farm-level data, it won't be available from this provider. * **Status changes to invalid**: Tokens may have expired. Re-authenticate the grower through Stara. ## What to do next * [Connect Stara Tutorial](/guides/tutorials/connect-stara) — Step-by-step walkthrough. * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # Trimble Source: https://docs.withleaf.io/providers/trimble Connect to Trimble Agriculture to pull field boundaries, machine files, and field operations through Leaf's provider credentials API. Leaf connects to Trimble Agriculture using OAuth 2.0. Once connected, Leaf syncs growers, farms, fields, machine files, and field operations for the Leaf user. ## Prerequisites 1. A Trimble Agriculture developer account. [Register here](https://agdeveloper.trimble.com/log-in-or-register/). 2. Your application's `clientId` and `clientSecret` from Trimble. 3. A grower's `refreshToken` obtained through the Trimble OAuth 2.0 consent flow. ## Setup steps 1. Complete the Trimble OAuth 2.0 flow to obtain a `refreshToken` for the grower's account. 2. POST the credentials to Leaf: ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "your-client-id", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials' headers = {'Authorization': f'Bearer {TOKEN}'} data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token" } response = requests.post(endpoint, headers=headers, json=data) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials' const headers = { 'Authorization': `Bearer ${TOKEN}` } const data = { "clientId": "your-client-id", "clientSecret": "your-client-secret", "refreshToken": "grower-refresh-token" } axios.post(endpoint, data, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` 3. Leaf validates the token and begins syncing. Check credential status with `GET /users/{leafUserId}/trimble-credentials`. ## Credentials schema **Create request body:** | Field | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------- | | `clientId` | string | Yes | Your application's client ID from Trimble | | `clientSecret` | string | Yes | Your application's client secret | | `refreshToken` | string | Yes | The grower's OAuth refresh token | **Response:** ```json theme={null} { "id": "uuid", "status": "str", "createdTime": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", "tokenMetadata": {"scopes": ["str"]}, "clientId": "str", "clientSecret": "str", "accessToken": "str", "refreshToken": "str" } ``` ## Endpoints Base URL: `https://api.withleaf.io/services/usermanagement/api` | Action | Method | Path | | --------------------- | ------ | ------------------------------------------------ | | Get credentials | GET | `/users/{leafUserId}/trimble-credentials` | | Create credentials | POST | `/users/{leafUserId}/trimble-credentials` | | Delete credentials | DELETE | `/users/{leafUserId}/trimble-credentials` | | Get credential events | GET | `/users/{leafUserId}/trimble-credentials/events` | ## Troubleshooting Use the events endpoint to inspect credential health: ```bash cURL theme={null} curl -X GET \ -H 'Authorization: Bearer YOUR_TOKEN' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials/events' ``` ```python Python theme={null} import requests TOKEN = 'YOUR_TOKEN' endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials/events' headers = {'Authorization': f'Bearer {TOKEN}'} response = requests.get(endpoint, headers=headers) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios') const TOKEN = 'YOUR_TOKEN' const endpoint = 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/trimble-credentials/events' const headers = { 'Authorization': `Bearer ${TOKEN}` } axios.get(endpoint, { headers }) .then(res => console.log(res.data)) .catch(console.error) ``` Event logs are retained for 30 days. Once the credential is deleted or disassociated from the Leaf user, the logs are no longer available. Common issues: * **Status changes to invalid**: The grower may have revoked access or the refresh token expired. Have the grower re-authorize through Trimble's OAuth flow. ## What to do next * [Connect Trimble Tutorial](/guides/tutorials/connect-trimble) — Step-by-step walkthrough. * [Provider Authentication Overview](/providers/overview) — How provider credentials work across all providers. * [API Reference: Providers](/api-reference/providers) — Full endpoint reference for provider credentials. # Valley Source: https://docs.withleaf.io/providers/valley Connect to Valley irrigation systems to pull irrigation activity data through Leaf's provider credentials API. Leaf connects to Valley irrigation systems using API credentials. Once connected, Leaf syncs irrigation activity data for the Leaf user. ## Prerequisites 1. A Valley account with API access. 2. Your Valley API credentials: `apid`, `initializationVector`, `key`, `username`, and `password`. ## Credentials schema | Field | Type | Required | Description | | ---------------------- | ------ | -------- | --------------------------------------------- | | `apid` | string | Yes | Your Valley application ID. | | `initializationVector` | string | Yes | Initialization vector for API authentication. | | `key` | string | Yes | API key. | | `username` | string | Yes | Valley account username. | | `password` | string | Yes | Valley account password. | ## Endpoints | Method | Path | Description | | ------ | ----------------------------------------------- | ------------------------------------------ | | GET | `/users/{leafUserId}/valley-credentials` | Get stored credentials. | | POST | `/users/{leafUserId}/valley-credentials` | Create credentials. | | DELETE | `/users/{leafUserId}/valley-credentials` | Delete credentials. | | GET | `/users/{leafUserId}/valley-credentials/events` | Get credential events for troubleshooting. | Base URL: `https://api.withleaf.io/services/usermanagement/api` ### Create credentials ```bash cURL theme={null} curl -X POST \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "apid": "your-apid", "initializationVector": "your-iv", "key": "your-key", "username": "your-username", "password": "your-password" }' \ 'https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/valley-credentials' ``` ```python Python theme={null} import requests TOKEN = "YOUR_TOKEN" endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/valley-credentials" headers = {"Authorization": f"Bearer {TOKEN}"} payload = { "apid": "your-apid", "initializationVector": "your-iv", "key": "your-key", "username": "your-username", "password": "your-password", } response = requests.post(endpoint, headers=headers, json=payload) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require("axios"); const TOKEN = "YOUR_TOKEN"; const endpoint = "https://api.withleaf.io/services/usermanagement/api/users/{leafUserId}/valley-credentials"; const headers = { Authorization: `Bearer ${TOKEN}` }; const payload = { apid: "your-apid", initializationVector: "your-iv", key: "your-key", username: "your-username", password: "your-password", }; axios.post(endpoint, payload, { headers }) .then((res) => console.log(res.data)) .catch(console.error); ``` ### Response ```json theme={null} { "id": "uuid", "status": "OK", "createdTime": "2026-01-15T12:00:00.000000Z", "apid": "your-apid", "key": "your-key", "initializationVector": "your-iv", "username": "your-username" } ``` ## Troubleshooting Use the events endpoint to check credential health. Events are retained for 30 days and are deleted when the credential is removed. ``` GET /users/{leafUserId}/valley-credentials/events ``` ## What to do next * [Irrigation Overview](/irrigation/overview) for details on Lindsay and Valley irrigation data. * [API Reference: Providers](/api-reference/providers) for the full credential path matrix. # Crop Reference Table Source: https://docs.withleaf.io/resources/crops-table Reference of common crop names and standard moisture values used in Leaf harvest output. Leaf standardizes crop names across providers. The crop names below are examples you may see in Leaf harvest output. Standard moisture values are used in harvest calculations when applicable. ## Standard moisture reference Not every crop has a standard moisture value. When no standard moisture value is available, harvest output may not include separate dry-value calculations for that crop. ## Common harvest crops These are the crops most users look for first. | Crop or family | Name variants you may see | Standard Moisture (%) | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------: | | corn | `corn`, `corn, waxy`, `corn, white`, `milho`, `maíz`, `mais`, `körnermais` | 15.0 | | soybeans | `soybeans`, `soybean`, `soja`, `sojas` | 13.0 | | wheat family | `wheat`, `winter wheat`, `spring wheat`, `durum wheat`, `white wheat`, `wheat, winter`, `wheat, spring`, `wheat, durum`, `wheat, fall`, `wheat, hard red spring`, `wheat, hard red winter`, `wheat, hard white spring`, `wheat, hard white winter`, `wheat, soft red spring`, `wheat, soft red winter`, `wheat, soft white spring`, `wheat, soft white winter`, `hard red spring wheat`, `hard red winter wheat`, `soft red winter wheat`, `soft white winter wheat`, `wheat (hrd rd spr)`, `wheat (hrd rd wtr)`, `wheat, sft rd wtr`, `trigo` | 13.5 | | canola | `canola` | 10.0 | | rapeseed | `rapeseed`, `rape` | 12.5 | | barley | `barley, spring`, `spring barley` | 15.0 | | field peas | `field peas`, `peas (field)` | 12.0 | | edible beans | `edible beans`, `beans`, `bean` | 15.0 | | sorghum | `sorghum`, `sorgo` | 14.0 | | sunflowers | `sunflowers` | 10.0 | ## Additional crops with standard moisture values | Crop | Standard Moisture (%) | | -------------- | --------------------: | | alfalfa | 13.0 | | beans, navy | 15.0 | | bromegrass | 12.0 | | buckwheat | 14.0 | | chick peas | 14.0 | | flax | 10.0 | | grass, fescue | 12.0 | | grass, orchard | 12.0 | | lentils | 13.0 | | millet | 13.0 | | mustard | 9.5 | | oats, spring | 12.0 | | peas, blck-Eye | 12.0 | | popcorn | 14.0 | | rice | 13.0 | | rye, annual | 15.0 | | rye, perrenial | 15.0 | | safflower | 8.0 | | timothy Grass | 12.0 | | triticale | 13.0 | | coffee | 1.0 | | spelt | 13.0 | | einkorn | 13.0 | | emmer | 13.0 | ## All supported crop strings The following crop name strings can appear in Leaf output. This is a broader list than the moisture table above, because some crop names do not have a standard moisture value. If the crop from your machine file is not in this list, contact [support@withleaf.io](mailto:support@withleaf.io). asparagus, balm, beets, belgian endive, broad beans, broccoli, brussels sprouts, cabbage lettuce, canary seed, caraway seed, carrots, castor beans, cauliflower, celery, chervil, chicory, chinese cabbage, corn cob mix, corn salad, corn silage, crambe, cucumber, dahlia, digitalis lanate, dill, elephant grass, endives, english rye grass, evening primrose, field meadow grass, fodder beet, french beans, gherkin, ginseng, gladiolus, grain maize, grass forage, grassland, green beans, green cabbage, green peas, hard fescue grass, haricot beans, headed cabbage, hemp, honeydew, hops, hypericum, hyracinth, iris, italian rye grass, jalapeno, jerusalem artichoke, kale, leek, lentil crimson, lentil eston, lentil chilean, lentil laird, lettuce, lily, lima beans, lupine, marowfat peas, melon, melons tree, mushrooms, narcissus, none, olives, onions, oranges, oregano, parsley, pea trapper, peaches, pears, picklers, poppy seed, potatoes for chips, potatoes for retail, potatoes for starch, radicchio, raddice, red beet, red cabbage, red fescue grass, red kidney beans, rettich, rhubarb, rice long, rice medium, salad, savoy cabbage, scorzonera, seed potatoes, set aside, shallots, silver onions, spear grass, spinach, squash, strawberries, stubbel tuber, sugarbeet, sugar corn, sunflower oil, sunflower stripe, cantaloupe, tankard turnip, tick beans, tobacco, tomatoes, tuber fennel, tulip, turnip cabbage, turnip rooted celery, unspecified crop, vetch, vines, white cabbage, wood carrot # Glossary Source: https://docs.withleaf.io/resources/glossary Key terms used in the Leaf API: API owner, Leaf user, machine file, field operation, provider credentials, and other Leaf-specific terminology. Alphabetical reference of Leaf-specific terms. For a narrative overview of how these concepts relate, see [Core Concepts](/getting-started/core-concepts). | Term | Definition | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Alerts** | Webhook notifications triggered by events in Leaf — new field operations, credential expiration, boundary changes, file processing completion, etc. You register an alert URL once and Leaf pushes events to it instead of requiring you to poll. | | **API owner** | The top-level Leaf account, identified by the email address you registered with. Owns all Leaf users beneath it. Use separate API owners for test and production environments. | | **Batch upload** | A single upload request containing one or more machine files. Each batch is tracked by a batch ID and progresses through `RECEIVED → PROCESSING → PROCESSED` (or `FAILED`). Query the batch status endpoint to monitor progress. | | **Configurations** | Settings that control how Leaf ingests, processes, and outputs data. Applied at the API owner level (inherited by all Leaf users) or overridden per Leaf user. Changes are not retroactive — they only affect data processed after the change. | | **Custom data sync** | A configuration (`customDataSync`) that limits Leaf to fetching field metadata only from a provider. You then select which fields to fully process. Useful for controlling costs and scope. | | **Field** | A named area of land within a farm. In Leaf, the field object holds metadata (name, ID, area) but does not require a boundary. Fields can be synced from a provider or created via the API. | | **Field boundary** | The geographic polygon defining a field's extent. One active boundary per field. Required for creating field operations — without a boundary, Leaf still converts machine files and produces file summaries, but cannot clip data to the field. | | **Field operation** | The merged, boundary-clipped output from one or more machine files. Represents a single activity — planting, harvest, application, or tillage — on a specific field. Each field operation has a summary and optional property map images. | | **Filtered GeoJSON** | An optional processing stage where invalid or outlier data points are removed from the standard GeoJSON based on configurable rules. Enabled via configuration. | | **Leaf user** | Represents a grower (or region, sub-customer, etc.) under your API owner. Holds provider credentials, fields, machine files, and field operations. Data is isolated per Leaf user. Typically one Leaf user per grower. | | **Machine file** | A raw data file from a provider or direct upload containing GPS-tagged point data from field equipment. Leaf converts these through raw → standard → (optionally) filtered GeoJSON. Each machine file gets a summary with averages, min/max, and standard deviations. | | **Machine file summary** | Aggregated statistics derived from the point data in a single machine file — processed and cleaned by Leaf. Output properties vary by operation type (planted, applied, harvested, tillage). | | **Magic Link** | A hosted web widget you can send to growers. They click it and connect their provider account or upload files without interacting with the API directly. | | **Operation summary** | Aggregated statistics for a field operation — the result of merging machine files to a field boundary. Similar to a machine file summary but scoped to a single field and activity. | | **Provider** | A third-party platform that supplies agricultural data — John Deere, Climate FieldView, CNHi, CNHI FieldOps, Trimble, AgLeader, Stara, and others. Each provider requires its own license agreement and credentials. | | **Provider credentials** | OAuth2 tokens or API keys attached to a Leaf user that authorize Leaf to pull data from a specific provider. One credential set per provider per Leaf user. | | **Standard GeoJSON** | The cleaned, standardized GeoJSON output. Property names, units, and structure are consistent regardless of source provider or file format. This is the primary output format for machine files and field operations. | | **Webhook** | See **Alerts**. | ## Agricultural terms | Term | Definition | | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Application** | The task of applying products to a field — fertilizer, pesticides, herbicides, etc. One of the four field operation types. | | **Crop** | The type of plant being grown (e.g., soybeans, corn, wheat). | | **Farm** | A group of fields managed by a grower. | | **Grower** | A farmer or farm operator. In Leaf's account model, typically maps to one Leaf user. | | **Harvest** | The task of collecting a mature crop from a field. One of the four field operation types. | | **Implements** | Specialized pieces of farm equipment — drills, sprayers, combines, planters. | | **Organization** | The top-level entity for a farm business, such as the head office of a corporate farm. | | **Planting** | The task of placing seeds in the soil. One of the four field operation types. | | **Tillage** | The task of breaking up soil in preparation for planting. One of the four field operation types. | | **Variety** | A subtype of a crop (e.g., P7326 is a variety of corn). | # Troubleshooting Source: https://docs.withleaf.io/resources/troubleshooting Common issues and fixes for Leaf API integrations: authentication errors, provider connection problems, file processing failures, missing data, and webhooks. Practical fixes for the most common problems customers hit during integration and production use. ## Authentication issues ### Token expired or invalid Leaf tokens expire after 30 days. If you get a `401 Unauthorized` response, generate a new token. Your application should handle token refresh automatically rather than relying on a hardcoded token. ### Wrong API owner If you can't find Leaf users, fields, or files you expect to see, confirm you're authenticating with the correct API owner email. Test and production environments should use separate API owners (e.g., `leaf-test@yourcompany.com` and `leaf-prod@yourcompany.com`). There is no shared sandbox — you manage environment separation yourself. ## Provider connection issues ### Provider credentials not working Every provider requires its own license agreement and potentially detailed commercial agreements before you can pull data through Leaf. Leaf cannot grant you access to a provider's API — you must finalize those steps with each provider directly. Common causes of credential failures: * The OAuth token expired or was revoked by the grower. Check the credentials events endpoint for the specific provider to see the error. * The provider application hasn't been approved yet (John Deere requires app verification through their marketplace). * The grower hasn't granted the required scopes/permissions in the provider's consent flow. ### John Deere connections John Deere is the most common source of connection issues: * You need a separate agreement with John Deere before connecting. Contact your Leaf CSM if you're unsure about requirements. * Use the John Deere credentials events endpoint to check for specific error messages. * If you need to limit data by organization, use `organizationDataSync` to scope which John Deere organizations Leaf pulls from. ### Climate FieldView connections * Climate FieldView credentials use OAuth2. If the grower's token is revoked, you need to re-run the consent flow. * Check the Climate FieldView credentials events endpoint for connection status. ### CNHi connections * Similar to other providers, CNHi requires its own access agreement. * Use the CNHi credentials events endpoint to inspect connection errors. For any provider, the credentials events endpoint is the first place to look when a connection fails. It gives you the specific error and timestamp. ## File processing issues ### Machine files stuck in "processing" Files normally process within minutes. If a file stays in `processing` for an extended period: 1. Check the file status endpoint — it shows the processing state for each pipeline step (standardGeojson, cleanupGeojson, summary, units, etc.). 2. Look for a specific step that shows `failed` with an error message. 3. If the file format is unsupported or corrupt, processing fails. Check the `originalFile` step status. ### File processing failed The file status endpoint returns per-step status. Common failure causes: * Unsupported file format or corrupt data. * Crop type not recognized — if the crop from your machine file doesn't appear in the [crops table](/resources/crops-table), contact [support@withleaf.io](mailto:support@withleaf.io). * The file contained no valid GPS data points. ### When to upload manually vs. use provider sync Use provider sync for ongoing data ingestion where a Leaf user has valid credentials. Use manual upload for historical data (e.g., from a USB drive), unsupported providers, or older offline file formats. Both methods produce the same standard GeoJSON output. ## Data not appearing ### No field operations generated Field operations require field boundaries. Without a boundary, Leaf still converts machine files and produces file summaries, but cannot create field operations. Check: 1. Does the Leaf user have fields with active boundaries? 2. Do the machine files' GPS coordinates fall within those boundaries? 3. Has enough time passed for processing to complete? Use the file status endpoint to verify. ### Duplicate billing from duplicate connections If the same data is processed under different Leaf users, each user's acreage counts toward your total usage. This happens when: * The same grower's credentials are attached to multiple Leaf users. * The same boundary is connected under multiple Leaf users. Prevent this by assigning one Leaf user per actual customer and using test accounts during development. ### Too much data syncing Use `customDataSync` to preview and selectively process certain fields instead of syncing everything from a provider. This controls costs and avoids pulling unneeded data. For John Deere specifically, `organizationDataSync` limits which organizations Leaf pulls from. ## Webhook (alerts) issues ### Not receiving webhooks 1. Confirm your alert URL is registered and active — list your alerts to verify. 2. Your endpoint must return a `2xx` response. If it returns an error, Leaf retries with backoff but eventually stops. 3. Check that your server is publicly accessible. Leaf cannot reach `localhost` or private networks. 4. Verify there's no firewall or WAF blocking Leaf's requests. ### Webhook payloads not matching expectations Alert payloads contain event metadata, not the full resource. Use the IDs in the payload to fetch the complete data from the relevant endpoint (e.g., get file, get operation). ## Billing questions ### How billing works Leaf charges based on spatially unique acres processed per Leaf user. The charge is triggered when data is pulled and processed by Leaf, not when you query or download the processed data. Re-downloading the same data does not incur additional charges. Re-processing identical data under new Leaf users counts multiple times. Be careful when re-creating Leaf users or moving credentials between users. ### Checking usage Use the [billing contracts](/api-reference/billing) endpoints to view your current contracts and consumption. The consumption range endpoint shows daily usage breakdowns. ## Getting help If your issue isn't covered here: * Email support: [help@withleaf.io](mailto:help@withleaf.io) * Contact your Leaf CSM for billing or commercial questions # Satellite Imagery Overview Source: https://docs.withleaf.io/satellite/overview Register a field for satellite monitoring and receive processed NDVI, NDRE, and RGB images from Sentinel-2 and PlanetScope on every pass. Leaf's crop monitoring service turns satellite passes into field-level imagery. You register a field boundary, choose a satellite provider, and Leaf delivers processed, clipped images every time a new scene is available. The service produces RGB, NDVI, and NDRE composites, along with individual band images. You also get cloud coverage percentage and data coverage percentage for each image, so you can filter out unusable scenes. ## Providers Leaf supports two satellite sources: | | Sentinel-2 | PlanetScope | | ------------------- | :-------------: | :-------------: | | Spatial resolution | 10–60 m | 3 m | | Temporal resolution | 3–5 days | \~1 day | | Spectral bands | 12 | 4–8\* | | Cost | Free (included) | Billed per area | \*PlanetScope band count depends on the asset type requested. Both providers produce RGB, NDVI, and NDRE composites. Sentinel-2 data is freely available through the Copernicus program. PlanetScope requires a billing agreement, and usage is metered by area. ## How it works 1. **Create a satellite field** — POST a `MultiPolygon` geometry to `/services/satellite/api/fields`. Set the providers you want (Sentinel-2, PlanetScope, or both). 2. **Leaf fetches imagery** — By default, Leaf retrieves images from the last 30 days. You can set a `startDate` or `daysBefore` parameter to go further back. 3. **Continuous monitoring** — The field is monitored indefinitely. Each time a satellite passes over the field, Leaf processes and clips the imagery to your boundary. 4. **Retrieve images** — GET `/services/satellite/api/fields/{id}/processes` returns all processed images for the field, filterable by date range, cloud cover, and data coverage. After creating a satellite field, it may take a few minutes for images to become available. ## Image outputs For each satellite pass, Leaf produces: * **RGB** — True-color image as GeoTIFF (EPSG:4326) and PNG (EPSG:3857). * **NDVI** — Raw values as GeoTIFF, plus colorized GeoTIFF and PNGs with relative and absolute scales. * **NDRE** — Same formats as NDVI. * **Individual bands** — GeoTIFF for each spectral band at native resolution. * **Multiband** — A single GeoTIFF with all bands stacked. PNG files are scaled up by 800% for display purposes and have no fixed resolution. ## Field size limits * Maximum area: 50,000 hectares (123,000 acres) * Maximum perimeter: 300 km (180 miles) * Maximum vertices: 1,500 * Minimum inner ring area: 1 m² ## Common use cases * **In-season crop monitoring**: Register field boundaries and receive NDVI imagery on every Sentinel-2 pass to track crop health through the season. * **High-resolution scouting**: Use PlanetScope at 3-meter resolution to detect within-field variability and target scouting trips. * **Multi-year comparison**: Query historical satellite processes to compare vegetation index trends across growing seasons. ## What to do next * [Sentinel-2](/satellite/sentinel) — Band details, resolution, and cloud masking for Sentinel data. * [PlanetScope](/satellite/planet) — Asset types, band details, and subscription management. * [API Reference: Satellite](/api-reference/satellite) — Full endpoint reference for the crop monitoring service. # PlanetScope Source: https://docs.withleaf.io/satellite/planet PlanetScope imagery through Leaf's crop monitoring service: available asset types, subscription handling, and Planet-specific geometry requirements. Leaf integrates with Planet to deliver PlanetScope imagery at 3-meter resolution with near-daily revisit times. Leaf handles the subscription process internally, fetching both back-fill and forward-fill images based on the dates you specify. PlanetScope imagery is billed by area. When you create a satellite field with `providers: ["planet"]`, Leaf first checks whether Planet is enabled for your API owner. If Planet is not enabled, field creation fails before the subscription is created. ## Supported item and asset types Leaf supports the `PSScene` item type with these asset types: ### ortho\_analytic\_8b\_sr Atmospherically corrected surface reflectance. This is the default and the most commonly used asset type. Leaf produces 13 images per pass. | Band | Name | | :--- | :------------ | | 1 | Coastal Blue | | 2 | Blue | | 3 | Green I | | 4 | Green | | 5 | Yellow | | 6 | Red | | 7 | Red Edge | | 8 | Near-infrared | ### ortho\_analytic\_8b Radiometrically calibrated analytic image stored as 16-bit scaled radiance. Same 8 bands as `ortho_analytic_8b_sr`. Leaf produces 13 images per pass. ### ortho\_visual Color-corrected visual image with 3 bands (Red, Blue, Green). Leaf produces 2 images per pass. ### ortho\_udm2 Usable data mask (Cloud 2.0). Contains 8 bands: | Band | Description | | :--- | :-------------- | | 1 | Clear map | | 2 | Snow map | | 3 | Shadow map | | 4 | Light haze map | | 5 | Heavy haze map | | 6 | Cloud map | | 7 | Confidence map | | 8 | Unusable pixels | ### ortho\_analytic\_8b\_xml Radiometrically calibrated analytic image metadata. ## Requesting multiple asset types You can request more than one asset type per satellite field. Specify them in the `assetTypes` array when creating the field: ```json theme={null} { "externalId": "my-field-001", "providers": ["planet"], "assetTypes": ["ortho_analytic_8b_sr", "ortho_udm2"], "geometry": { "type": "MultiPolygon", "coordinates": [...] } } ``` If you omit `assetTypes`, Leaf defaults to `ortho_analytic_8b_sr`. ## Checking the subscription `GET /services/satellite/api/fields/{id}/subscription` Returns the active subscription details for a Planet-enabled field, including `planetAssetTypes`, `planetItemTypes`, and `startDate`. ## Geometry requirements Planet has stricter geometry requirements than Sentinel-2: * The geometry must be valid (no self-intersections). * Maximum vertices: 1,500. * Minimum inner ring area: 1.0 m². Field creation fails if these requirements are not met. ## What to do next * [Satellite Overview](/satellite/overview) — How the crop monitoring service works and provider comparison. * [Sentinel-2](/satellite/sentinel) — Free imagery at 10m resolution. * [API Reference: Satellite](/api-reference/satellite) — Full endpoint reference. # Sentinel-2 Source: https://docs.withleaf.io/satellite/sentinel Sentinel-2 imagery through Leaf's crop monitoring: 25 images per pass, band resolutions from 10m to 60m, NDVI and NDRE output, and cloud masking. Leaf uses Sentinel-2 L2A data (atmospherically corrected surface reflectance) from the Copernicus program. Sentinel-2 revisits each field every 3 to 5 days and is included at no additional cost. ## Images produced per pass Leaf generates 25 images for each satellite pass over a field: | Name | Resolution | Type | Projection | | :----------------: | :--------: | :--------------------------: | :--------- | | RGB.tif | 10 m | True-color | EPSG:4326 | | RGB.png | — | True-color | EPSG:3857 | | NDVI.tif | 10 m | Raw NDVI values | EPSG:4326 | | NDVI\_color.tif | 10 m | Colorized NDVI | EPSG:4326 | | NDVI.png | — | Colorized NDVI | EPSG:3857 | | NDVI\_relative.png | — | NDVI scaled to image min/max | EPSG:3857 | | NDVI\_absolute.png | — | NDVI scaled to -1 to 1 | EPSG:3857 | | NDRE.tif | 10 m | Raw NDRE values | EPSG:4326 | | NDRE\_color.tif | 10 m | Colorized NDRE | EPSG:4326 | | NDRE.png | — | Colorized NDRE | EPSG:3857 | | NDRE\_relative.png | — | NDRE scaled to image min/max | EPSG:3857 | | NDRE\_absolute.png | — | NDRE scaled to -1 to 1 | EPSG:3857 | | multi\_band.tif | 10 m | All bands stacked | EPSG:4326 | | B01.tif | 60 m | Coastal aerosol | EPSG:4326 | | B02.tif | 10 m | Blue | EPSG:4326 | | B03.tif | 10 m | Green | EPSG:4326 | | B04.tif | 10 m | Red | EPSG:4326 | | B05.tif | 20 m | Vegetation red edge | EPSG:4326 | | B06.tif | 20 m | Vegetation red edge | EPSG:4326 | | B07.tif | 20 m | Vegetation red edge | EPSG:4326 | | B08.tif | 10 m | NIR | EPSG:4326 | | B8A.tif | 20 m | Vegetation red edge | EPSG:4326 | | B09.tif | 60 m | Water vapour | EPSG:4326 | | B11.tif | 20 m | SWIR | EPSG:4326 | | B12.tif | 20 m | SWIR | EPSG:4326 | PNG files have no fixed resolution because they are upscaled 800% for display. ## NDVI output formats Leaf produces three NDVI PNG variants, each useful for different purposes: * **NDVI\_relative** — Scales the color ramp between the minimum and maximum values of that specific image. Good for seeing variation within a single date. * **NDVI\_absolute** — Scales from -1 to 1 with the full color ramp. Good for comparing across different dates. * **NDVI** (plain) — Same -1 to 1 range, but values below 0 are rendered as bright red. This is the recommended default for most use cases. The same three variants exist for NDRE. If you want to apply your own color ramp, use the raw `NDVI.tif` or `NDRE.tif` files. These contain pre-calculated index values and can be imported into any GIS tool like QGIS. ## Cloud masking Leaf uses the cloud mask provided with Sentinel-2 L2A data to calculate the cloud coverage percentage for each image. You can filter images by `maxClouds` when querying processes. ## What to do next * [Satellite Overview](/satellite/overview) — How the crop monitoring service works. * [PlanetScope](/satellite/planet) — Higher-resolution imagery from Planet. * [API Reference: Satellite](/api-reference/satellite) — Full endpoint reference. # Soil Sampling Overview Source: https://docs.withleaf.io/soil/overview Upload soil samples in 30+ formats and receive normalized output with standardized analyte values, units, and extraction methods as GeoJSON or JSON. Leaf's Soil Sampling service accepts soil lab data in over 30 file formats and normalizes it into a standard canonical format. You upload `.zip` archives containing shapefiles, CSVs, XML reports, or proprietary formats. Leaf identifies the format, extracts the soil data, and returns a flat GeoJSON result plus a canonical JSON result when that output is available. The Soil Sampling service is currently available by invitation only. ## How it works The service uses an asynchronous batch model. You upload one or more files, then poll for results. 1. **Upload** one or more `.zip` files to the batch endpoint along with a Leaf user ID. Each file becomes an entry within the batch. 2. **Processing.** Leaf classifies the file format, extracts soil sample data, normalizes analyte names and units, and produces both output formats. 3. **Retrieval.** Poll the batch status endpoint. When an entry reaches `COMPLETED`, `downloadStandardGeojson` contains the flat GeoJSON result URL. `downloadCanonicalJson` contains the hierarchical result URL when that output is available. Most files complete processing within a few minutes. Poll every 5 seconds for the first minute, then back off to every 15–30 seconds. Stop when the batch status is `COMPLETED`, `PARTIALLY_COMPLETED`, or `FAILED`. ## Key concepts A **batch** is a container for one or more uploaded files, created by a single POST request. Its status reflects the aggregate state of all entries. An **entry** is one file within a batch. Each entry is processed independently. A batch with three files has three entries, each of which may complete or fail on its own. Entries move through `PROCESSING` → `COMPLETED` or `FAILED`. Batches follow the same pattern, plus `PARTIALLY_COMPLETED` when some entries succeed and others fail. | Status | Level | Meaning | | --------------------- | ------------- | ---------------------------------------- | | `PROCESSING` | Entry / Batch | Upload received, conversion in progress | | `COMPLETED` | Entry / Batch | All entries converted successfully | | `PARTIALLY_COMPLETED` | Batch only | Some entries completed, some failed | | `FAILED` | Entry / Batch | Conversion failed (check `errorMessage`) | ## Output formats Completed entries return result URLs in the API response. All entries that reach `COMPLETED` will have a `downloadStandardGeojson` URL. Most supported formats also produce `downloadCanonicalJson`; for formats that don't, the field is `null`. `downloadStandardGeojson` is a flat GeoJSON FeatureCollection. Each Feature represents one soil sample at one depth. A sample with multiple depth layers (e.g., surface + subsoil) produces multiple Features sharing the same `sampleId`. This format works well for mapping, GIS tools, and spatial queries. When present, `downloadCanonicalJson` is the full hierarchical data model. It contains an array of `SoilSamplingEvent` objects preserving the natural tree structure: event → samples → depth layers → analyte results. It includes context not present in the GeoJSON: lab information, provenance (source file, format family, converter version), fertilizer recommendations, and category classification for each analyte result. Use this format when you need the complete data model or when you're building data pipelines that benefit from structured nesting. ### GeoJSON properties | Property | Type | Description | | -------------- | ------ | --------------------------------------------- | | `eventId` | string | Unique identifier for the sampling event | | `eventDate` | string | Sampling date (YYYY-MM-DD), or null | | `eventCode` | string | Lab report number or job ID, or null | | `sampleId` | string | Unique identifier for this sample | | `sampleNumber` | string | Lab sample number (e.g. "1", "A-1"), or null | | `depthLabel` | string | Human-readable depth (e.g. "0-6 in"), or null | | `depthTop` | number | Top of sampling depth, or null | | `depthBottom` | number | Bottom of sampling depth, or null | | `depthUnit` | string | Depth unit ("in" or "cm"), or null | | `growerName` | string | Grower name, or null | | `farmName` | string | Farm name, or null | | `fieldName` | string | Field name, or null | Depth fields are all null when the source data does not specify sampling depth. Field context properties (`growerName`, `farmName`, `fieldName`) are omitted entirely when the source has no field metadata. ### Analyte properties Each analyte result adds up to three properties per Feature: | Pattern | Type | Description | | ------------------ | ------ | ---------------------------------------------- | | `{analyte}` | number | The measured value (e.g. `pH`, `P`, `K`, `OM`) | | `{analyte}_unit` | string | Unit of measurement, present when known | | `{analyte}_method` | string | Extraction method, present when known | A Mehlich-3 phosphorus result at 42 ppm produces: ```json theme={null} { "P": 42.0, "P_unit": "ppm", "P_method": "MEHLICH_3" } ``` A pH value with no known method or unit produces just `"pH": 6.4`. ### Common analytes | Analyte | Property | Typical units | | ------------------------ | -------- | ------------- | | pH | `pH` | (unitless) | | Organic matter | `OM` | % | | Phosphorus | `P` | ppm | | Potassium | `K` | ppm | | Calcium | `Ca` | ppm | | Magnesium | `Mg` | ppm | | Cation exchange capacity | `CEC` | meq/100g | | Buffer pH | `BpH` | (unitless) | | Nitrate-nitrogen | `NO3_N` | ppm | The full set of analytes depends on the input format and lab. Leaf normalizes over 750 column name variations into approximately 70 standard analyte properties. ### GeoJSON example A two-sample FeatureCollection from a shapefile with Mehlich-3 extraction: ```json theme={null} { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-89.4523, 40.1234] }, "properties": { "eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "eventDate": "2025-10-15", "sampleId": "f1e2d3c4-b5a6-7890-fedc-ba0987654321", "sampleNumber": "1", "depthId": "d1a2b3c4-e5f6-7890-abcd-111111111111", "growerName": "Smith Farms", "farmName": "North 40", "fieldName": "Section 12", "pH": 6.4, "OM": 3.2, "OM_unit": "%", "P": 42.0, "P_unit": "ppm", "P_method": "MEHLICH_3", "K": 185.0, "K_unit": "ppm", "K_method": "MEHLICH_3", "CEC": 14.2, "CEC_unit": "meq/100g" } }, { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-89.4531, 40.1242] }, "properties": { "eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "eventDate": "2025-10-15", "sampleId": "f1e2d3c4-b5a6-7890-fedc-ba0987654322", "sampleNumber": "2", "depthId": "d1a2b3c4-e5f6-7890-abcd-222222222222", "growerName": "Smith Farms", "farmName": "North 40", "fieldName": "Section 12", "pH": 6.8, "OM": 2.9, "OM_unit": "%", "P": 38.0, "P_unit": "ppm", "P_method": "MEHLICH_3", "K": 210.0, "K_unit": "ppm", "K_method": "MEHLICH_3", "CEC": 15.1, "CEC_unit": "meq/100g" } } ] } ``` ### Canonical JSON structure The canonical JSON output is an array of `SoilSamplingEvent` objects. Each event represents one sampling trip, date, or lab report from the source file. ``` SoilSamplingEvent ├── field_context (grower, farm, field names and IDs) ├── lab (lab name, received/processed dates) ├── provenance (source file, format family, converter version) ├── samples[] │ ├── geometry (GeoJSON Point or Polygon) │ └── depths[] │ └── results[] (analyte, value, unit, method, category) └── recommendations[] (fertilizer recommendations, when present) ``` Each `AnalyteResult` in the canonical format includes a `category` field that classifies the measurement: | Category | Meaning | Examples | | ------------- | ----------------------------------- | -------------------- | | `analyte` | Direct lab measurement | pH, P, K, Ca, Mg, OM | | `index` | Calculated index | P-Index, K-Index | | `derived` | Ratio or derived value | BS-Ca, BS-K, SAR | | `sensor` | Field instrument reading | EC (Veris), Red, IR | | `passthrough` | Unrecognized column preserved as-is | Varies | The `provenance` block on every event records exactly which source file and converter produced the output. Useful for auditing and tracing data lineage. ## File size limits | Limit | Value | | -------------------- | ------------------ | | Maximum file size | 50 MB per file | | Maximum request size | 200 MB per request | ## What to do next * [Supported Formats](/soil/supported-formats) — Full catalog of accepted soil data formats. * [API Reference: Soil Sampling](/api-reference/soil) — Endpoint reference for uploading files, checking status, and retrieving results. # Supported Soil Formats Source: https://docs.withleaf.io/soil/supported-formats Catalog of soil data formats accepted by Leaf Soil Sampling: shapefiles, CSVs, XML lab reports, and proprietary formats from SST, Veris, and others. Leaf accepts soil sample data from over 30 format families. Files are uploaded as `.zip` archives. Leaf identifies the format automatically; you don't need to specify it. Regardless of input format, successful processing returns a flat [GeoJSON FeatureCollection](/soil/overview#geojson-properties) for mapping and spatial tools. Most formats also produce a [canonical JSON](/soil/overview#canonical-json-structure) result with the full data model, including lab info, provenance, and analyte categories. Check the `downloadCanonicalJson` field in the entry response — it is `null` when canonical output is not available for that format. ## Format catalog | Format family | Type | Typical analytes | | -------------------------- | ----------- | ------------------------------------------ | | SMS Shapefile (8 variants) | Shapefile | pH, P, K, OM, CEC | | AgVance | Shapefile | pH, K, CEC | | AgVance SkyMap | Shapefile | pH | | Co-Op Soil | Shapefile | pH, P, K, Ca, Mg | | E4 | Shapefile | Varies | | GeoCarta Sampling | Shapefile | pH, CEC, Ca, Mg, K, P + resistivity sensor | | GeoCarta Soil | Shapefile | pH, OM, P, K | | Generic Soil | Shapefile | pH, P | | IFARM | Shapefile | pH, P, K | | Midwest Soil Shape | Shapefile | pH, P, OM | | Premier Crop | Shapefile | pH, OM, P, K, CEC | | SoilTestPro Shapefile | Shapefile | pH, P, K, OM | | SST Polygon | Shapefile | pH, P, K, OM | | SoilTestPro | CSV | pH, P, K, OM, BpH + methods | | AgPhD | CSV | pH, P, K, H, NO3-N | | SMS CSV | CSV | pH, P, K, OM, BpH + methods | | TopCop | CSV | pH, CEC, K, OM | | MODUS (AgGateway) | XML | pH, P, K, OM | | Midwest Labs | XML | pH, P, K, Ca, Mg, OM + methods | | A\&L Labs (AJService) | XML | pH, P, K, Ca, OM + methods | | Uniform Data Adapter | XML | OM, K | | SST Soil / SoilMapshots | Proprietary | pH, P, K, OM (500+ columns, multi-depth) | | SST Backup | Proprietary | pH, P, K, OM | | Veris | Proprietary | EC, pH, OM, CEC + sensor channels | ## Shapefiles The most common format type. Includes data from Ag Leader SMS (8 column-naming variants), AgVance, cooperative labs, SST, Premier Crop, IFARM, and others. Most shapefile formats include GPS coordinates for each sample point and produce geospatially-located GeoJSON Features. Some shapefile formats can contain multiple sampling dates or multiple fields within a single zip. Leaf splits these into separate events in the output, each with its own `eventId`. ## CSV Four CSV-based formats are supported. SoilTestPro and SMS CSV are the richest, with explicit extraction method columns (e.g. Mehlich-3, Bray-1) that Leaf carries forward into the `{analyte}_method` properties. AgPhD includes explicit depth columns. TopCop uses a simpler column structure. ## XML Leaf accepts MODUS XML (the AgGateway soil data standard), along with lab-specific XML formats from Midwest Laboratories and A\&L Laboratories. XML formats sometimes lack GPS coordinates; in these cases the GeoJSON Features have `null` geometry but still contain all analyte data. ## Proprietary SST (Soil Sampling Technologies) formats often contain 500+ columns per file covering surface and multiple subsoil depth layers. Each depth layer becomes a separate Feature in the GeoJSON output, linked by `sampleId`. Veris sensor files contain high-density electrical conductivity readings (9,000+ points per field). These produce large GeoJSON outputs (10+ MB) with EC measurements at shallow and deep profiles. # Weather Overview Source: https://docs.withleaf.io/weather/overview Historical and forecast weather data at the field level. Covers models like GFS, ICON, and IFS, reanalysis data from ERA5, and queries by field or coordinates. Leaf's Weather API provides historical and forecasted weather data tied to field boundaries or arbitrary lat/lon coordinates. Data is available in daily or hourly granularity, sourced from multiple weather models worldwide. ## Forecast data The forecast service covers 5 days in the past through 10 days into the future. If you don't specify dates, Leaf returns the next 7 days by default. ### Forecast models | Model | Provider | Country | Resolution | Forecast Length | Update Frequency | | -------------- | ------------------------ | -------------- | ---------- | --------------- | ---------------- | | GFS | NOAA | United States | 3–25 km | 16 days | Every hour | | ICON | Deutscher Wetterdienst | Germany | 2–11 km | 7.5 days | Every 3 hours | | IFS | ECMWF | European Union | 44 km | 7 days | Every 6 hours | | JMA | JMA | Japan | 5–55 km | 11 days | Every 3 hours | | GEM | Canadian Weather Service | Canada | 2.5 km | 10 days | Every 6 hours | | Arpege & Arome | Météo-France | France | 1–40 km | 4 days | Every 6 hours | The default model is `gfs`. Leaf selects the best model for any given location when using the default, so you generally don't need to specify one unless you want data from a particular source. ### Forecast variables Variables available across most models (some models omit soil temperature or soil moisture): **Daily:** temperature (mean/min/max), soil temperature (mean/min/max), sunrise, sunset, max wind speed, max wind gusts, wind direction, evapotranspiration, rain sum, snowfall sum, precipitation sum. **Hourly (additional):** dew point, longwave radiation, shortwave radiation, cloud cover, relative humidity, soil moisture at multiple depths. Sunrise and sunset are daily-only. Dew point, radiation, cloud cover, and relative humidity are hourly-only. All other variables appear in both daily and hourly responses. ## Historical data Historical weather data is sourced from ERA5 reanalysis products and goes back to 1940 (ERA5) or 1950 (ERA5-Land). There is a 5-day delay on historical data availability. For weather data within the last 5 days, use the forecast endpoints instead. ### Historical models | Model | Coverage | Resolution | Temporal Resolution | Data Availability | | --------- | -------- | ---------- | ------------------- | ----------------- | | ERA5 | Global | \~25 km | Hourly | 1940–present | | ERA5-Land | Global | \~11 km | Hourly | 1950–present | The default is `era5`. ERA5-Land offers higher spatial resolution but starts a decade later. ### Historical variables The same variable set as forecast data, with soil moisture measured at different depth intervals: 0–7 cm, 7–28 cm, 28–100 cm, and 100–255 cm (vs. 0–1 cm, 1–3 cm, 3–9 cm, 9–27 cm, 27–81 cm for forecast models). ## Query options You can fetch weather data in two ways: **By field** — Requires a Leaf user ID and field ID. Leaf uses the centroid of the field boundary. ``` GET /services/weather/api/users/{leafUserId}/weather/forecast/field/{fieldId}/daily GET /services/weather/api/users/{leafUserId}/weather/historical/field/{fieldId}/hourly ``` **By lat/lon** — No Leaf user or field required. Pass coordinates directly. ``` GET /services/weather/api/weather/forecast/daily/{lat},{lon} GET /services/weather/api/weather/historical/hourly/{lat},{lon} ``` All endpoints accept these parameters: | Parameter | Description | | ----------- | -------------------------------------------------------------------------- | | `startTime` | Start date (YYYY-MM-DD). | | `endTime` | End date (YYYY-MM-DD). | | `model` | Weather model to use. Defaults to `gfs` (forecast) or `era5` (historical). | | `units` | `metric` (default) or `imperial`. | ### Request limits * Daily endpoints: maximum span of 366 days per request. * Hourly endpoints: maximum span of 30 days per request. ## Units All numeric values respect the `units` parameter: | Measurement | Metric | Imperial | | ------------------------- | ------ | -------- | | Temperature | °C | °F | | Precipitation / Rain / ET | mm | inch | | Snowfall | cm | inch | | Wind speed / gusts | km/h | mph | | Radiation | W/m² | W/m² | | Soil moisture | m³/m³ | m³/m³ | | Cloud cover / Humidity | % | % | | Wind direction | ° | ° | ## Response format All weather endpoints return GeoJSON Features. The `geometry` is a Point (field centroid or the lat/lon you specified). The `properties` object contains each weather variable as a named key with `values` (time series array) and `unit`. ```json theme={null} { "type": "Feature", "properties": { "maxTemperature": { "values": [ { "time": "2024-07-21T00:00:00", "value": 28.8 }, { "time": "2024-07-22T00:00:00", "value": 28.1 } ], "unit": "ºC" } }, "geometry": { "type": "Point", "coordinates": [-89.643, 39.802] } } ``` If no data is available for a given time/day, the value is returned as `null`. ## What to do next * [API Reference: Weather](/api-reference/weather) — Full endpoint reference with all parameters and response schemas.