---
title: "Cluster assets by map location"
method: GET
path: "/api/assets/geo-clusters"
tags: ["assets"]
---

# Cluster assets by map location

`GET /api/assets/geo-clusters`

Clusters geotagged assets in a map viewport (bounding box) onto a grid of square cells and returns one entry per non-empty cell — its centroid, asset count, and a representative cover asset. Use this to render a clustered map or to count how many photos fall in each part of a viewport at a chosen zoom granularity.

The result is a single un-paginated list capped at 1000 cells; a viewport that is too dense at the given `cell_size` returns 422 (coarsen `cell_size` or zoom in). To list the individual assets behind a cell, call `list_assets` with a tighter bounding box over the same filters. Album and person filters compose using AND.

## Query parameters

- `bbox` string, required — Bounding-box (map viewport) location filter: four comma-separated decimal-degree numbers `min_longitude,min_latitude,max_longitude,max_latitude` (west,south,east,north), e.g. `-77.1,38.9,-77.0,39.0`. A box whose `min_longitude` exceeds `max_longitude` crosses the antimeridian: it selects the band running east from `min_longitude` over ±180° to `max_longitude`, so there is no need to split it client-side. Longitude order is therefore significant — transposed corners read as a crossing viewport, not as an error. A viewport 360° or wider must be sent as the full range `-180,...,180,...`, which the wrapped form cannot express.
- `cell_size` number, required — Grid cell edge in decimal degrees — the clustering granularity. Larger values give coarser clusters; the client maps map-zoom to `cell_size`. Must be at least 0.0001 (~11 m).
- `library_id` string, nullable — Library to cluster assets from. Optional if the user has a single live (non-trashed) library; required when they have multiple.
- `album_id` string, nullable — Return only assets in this album — the album's `album_` ID, not its name.
- `person_ids` string[], nullable — Cluster only assets containing faces belonging to ALL of these people (intersection, not union). Accepts up to 200 IDs across repeated `person_ids=` query params or comma-delimited values. Person IDs are carried by the entries of an asset's `people` field (returned with `include=people`).
- `local_datetime_after` string, date-time, nullable — Only include assets captured strictly after this instant (ISO 8601; exclusive). Convert a relative or natural-language date phrase ('in 2023') into an explicit bound before sending. `local_datetime` is the photo's wall-clock time in the device's own timezone. Naive values compare directly against `local_datetime`. Timezone-aware values: assets with a known offset are compared in UTC (`local_datetime - offset`); assets without an offset fall back to wall-clock comparison against `local_datetime`.
- `local_datetime_before` string, date-time, nullable — Only include assets captured strictly before this instant (ISO 8601; exclusive). Same conversion requirement and awareness/offset semantics as `local_datetime_after`.
- `state` 'live' | 'trashed' | 'all' — Which set of assets to cluster: `live` (default — excludes trashed assets), `trashed` (only trashed assets), or `all` (both).

## Response `200`

Successful Response

- GeoClustersResponse
  - `data` GeoCluster[], required — Non-empty grid cells within the requested viewport. Not paginated: the list is capped at 1000 cells and a denser viewport returns 422 instead — coarsen `cell_size` or zoom in.
    - `latitude` number, required — Cluster centroid latitude in decimal degrees — the average latitude of the cell's members.
    - `longitude` number, required — Cluster centroid longitude in decimal degrees — the average longitude of the cell's members.
    - `count` integer, required — Number of assets in this grid cell.
    - `representative_asset_id` string, required — ID of a cover asset for the cell — the most recently captured geotagged asset in it (ties broken by descending id).

## Other responses

- `401` — Missing, invalid, or expired credentials.
- `403` — The credentials are valid but not authorized for this operation — for example an API key whose action or library scope excludes it, or a credential type this operation does not accept.
- `404` — Not found
- `422` — Validation Error
- `429` — Rate limit exceeded. Retry after the interval in the `Retry-After` header.

---

[API](https://skmtc.net/gumnut-ai/apis/gumnut-api.md) · [All operations](https://skmtc.net/gumnut-ai/apis/gumnut-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/gumnut-ai/gumnut-api/versions/e71db45f5d4a/schema)
