Pagination
List endpoints support cursor-based pagination usinglimit and starting_after_id:
has_more field indicating whether additional results exist:
id of the last item as starting_after_id.
If you’re using one of our SDKs, pagination is handled automatically with built-in iterators.
Paging search results
GET /api/search and POST /api/search page by number instead of by cursor,
and the SDK search methods return one page per call rather than an iterator.
Pass limit (1–200, default 20) and a 1-indexed page. While has_more is
true, increment page and repeat the same search criteria, filters, and
limit:
has_more is false, even if the current page is full.
Search requires a nonblank text query or an uploaded image and returns a
limited number of the most relevant matches in relevance order, not every
match in your library. GET /api/search requires text; POST /api/search
accepts text, an image, or both. A request without either returns 422, even
when structured filters are supplied. Use GET /api/assets for browsing with
only structured filters and follow its cursor pagination.
Each result can carry match_strength. best results are strong matches and
come first; possible results follow and are often unrelated. It is null when
the search did not judge the result, such as an image search.
distance remains in each search result for compatibility, but is deprecated.
Don’t use it to judge text-search relevance or result order; use
match_strength for text-search relevance.
A text query search can return fewer than limit results, or none, and
has_more can be false after a short or empty page. An empty first page
means nothing within your filters closely matches that wording: rephrase or
broaden the query, or change the filters, instead of requesting another page.
Filtering
Use query parameters to filter results:Filtering albums by name
You can filterGET /api/albums with the optional name query parameter. For
example, GET /api/albums?name=summer matches album names containing summer,
regardless of case. The match is a literal substring, so % and _ are
treated as characters. Gumnut trims surrounding whitespace; omitting name or
sending it blank applies no name filter. Filtering happens before cursor
pagination, so the next page continues through matching albums.
Filtering by capture time
local_datetime_after and local_datetime_before filter on the asset’s capture time:
- Send offsetless values such as
2024-01-01T00:00:00when you want wall-clock filtering. - Send offset-aware values such as
2024-01-01T00:00:00-05:00when you want an absolute time window. - Keep both bounds consistent. Mixing an offsetless value with an offset-aware value in the same request is rejected.
- Assets with a known capture offset are compared using that offset. Assets without one fall back to wall-clock comparison, so offsetless captures still participate in aware range filters.
Filtering semantic search
UseGET /api/search when you want natural-language or metadata search. The endpoint accepts the same canonical date and album filters as asset lists:
album_id. You can also pass person_ids for one or more people. These filters narrow a content search; they do not replace the required nonblank query. The same filters are available on POST /api/search, which requires text, an image, or both in the multipart request. Use GET /api/assets when you only need album, people, rating, media-type, date, or location filters.
Use the canonical parameter names shown above. The legacy search spellings captured_after, captured_before, and album_ids are no longer supported; replace them with local_datetime_after, local_datetime_before, and album_id.
Filtering by location
GET /api/assets and GET /api/assets/counts support the same two location
filter shapes:
center+radiusfor a circular search area in metersbboxfor a rectangular viewport inmin_longitude,min_latitude,max_longitude,max_latitudeorder
center and radius together. Don’t combine them with bbox.
Spatial count filters compose with the endpoint’s album, people, rating,
media-type, date, and lifecycle filters. When a count response has more pages,
repeat the same spatial selector with starting_after_bucket.
To count all and only assets with coordinates, send the full-world box:
min_longitude over ±180° to max_longitude, so you don’t split it into two requests.
Because longitude order carries that meaning, it’s significant: a box with west and east transposed reads as a crossing viewport rather than an error. A viewport 360° or wider must be sent as the full range -180,...,180,..., which the wrapped form can’t express.
For semantic search, GET /api/search supports both center + radius and bbox as mutually exclusive location filters. Include a nonblank query; location only narrows that content search. Use GET /api/assets to browse by coordinates without text.
See Requests & Responses for how local_datetime is serialized, and see the API Reference tab for the full list of available filter parameters for each endpoint.
Map views with geo clusters
When you need a map-friendly summary of geotagged assets inside a viewport, useGET /api/assets/geo-clusters instead of paging through GET /api/assets and grouping results client-side.
The endpoint takes a bbox string in min_longitude,min_latitude,max_longitude,max_latitude order plus a cell_size in decimal degrees.
library_id, album_id, person_ids, local_datetime_after, local_datetime_before, and state.
Each item in data represents one non-empty grid cell and includes a centroid (latitude, longitude), the number of matching assets (count), and a representative_asset_id you can use as a cover image or lookup key.
GET /api/assets/geo-clusters is not paginated. The response is capped at 1000 non-empty cells, so a dense viewport returns 422 instead of partial results. If that happens, increase cell_size or zoom in and retry. A viewport crossing the antimeridian is clustered as one wrap-around band — send it as a single box whose west longitude is greater than its east longitude. Note that the 1000-cell cap then applies to that one response, so a viewport both very wide and very dense across ±180° may need a coarser cell_size than each half would have.