> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withleaf.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Soil Sampling

> 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).

<Note>
  The Soil Sampling service is currently available by invitation. Contact your account team to request access.
</Note>

## Base URL

```
https://api.withleaf.io/services/soil/api
```

## Endpoints

| Method                                                           | Path                       | Description                             |
| ---------------------------------------------------------------- | -------------------------- | --------------------------------------- |
| <span style={{fontWeight: 'bold', color: '#e5a00d'}}>POST</span> | `/soil/batch`              | [Upload soil files](#upload-soil-files) |
| <span style={{fontWeight: 'bold', color: '#16a34a'}}>GET</span>  | `/soil/batch/{id}`         | [Get batch status](#get-batch-status)   |
| <span style={{fontWeight: 'bold', color: '#16a34a'}}>GET</span>  | `/soil/batch/{id}/results` | [Get batch results](#get-batch-results) |
| <span style={{fontWeight: 'bold', color: '#16a34a'}}>GET</span>  | `/soil/batches`            | [List all batches](#list-all-batches)   |

***

## Upload soil files

<span style={{color: '#e5a00d', fontWeight: 'bold'}}>POST</span> `/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

<CodeGroup>
  ```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);
  ```
</CodeGroup>

<Tip>
  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)
  ```
</Tip>

### 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

<span style={{color: '#16a34a', fontWeight: 'bold'}}>GET</span> `/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

<CodeGroup>
  ```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);
  ```
</CodeGroup>

### 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

<span style={{color: '#16a34a', fontWeight: 'bold'}}>GET</span> `/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

<CodeGroup>
  ```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);
  ```
</CodeGroup>

### 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

<span style={{color: '#16a34a', fontWeight: 'bold'}}>GET</span> `/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

<CodeGroup>
  ```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);
  ```
</CodeGroup>

### 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).

<CodeGroup>
  ```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);
  ```
</CodeGroup>

## 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.
