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

# 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                                                               |
| ---------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------- |
| <span style={{fontWeight: 'bold', color: '#e5a00d'}}>POST</span> | `/query`                   | [Execute a SQL query](#execute-a-sql-query)                               |
| <span style={{fontWeight: 'bold', color: '#16a34a'}}>GET</span>  | `/query`                   | [Execute a SQL query (via query parameter)](#execute-a-sql-query)         |
| <span style={{fontWeight: 'bold', color: '#e5a00d'}}>POST</span> | `/export-query-geoparquet` | [Export query results as GeoParquet](#export-query-results-as-geoparquet) |

***

## Execute a SQL query

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

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

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

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

### Spatial query example

You can also filter by geography using spatial functions and a WKT boundary polygon.

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

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

<Note>
  A `204 No Content` response has no body. Check the status code before attempting to parse JSON.
</Note>

***

## Export query results as GeoParquet

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

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

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