Pagination & filtering
GET /releases is the only paginated endpoint. GET /projects returns a single project, and GET /project-languages always returns the complete set, however many languages a project has.
Parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
page | integer | 1 | 1-based. |
page_size | integer | 20 | Values above 100 are silently clamped to 100, not rejected. |
search | string | — | Case-insensitive substring match on release name and tag. |
sort | string | created_at | One of name, tag, status, published_at, created_at. |
order | string | desc | asc or desc. |
An unrecognised sort or order value falls back to the default rather than erroring, so a typo produces sensibly-ordered results rather than a failure — check your sorting if the order surprises you.
curl -s "$LOCALEO_API/releases?search=v1.4&sort=published_at&order=desc&page_size=50" \ -H "Authorization: Bearer $LOCALEO_TOKEN"The response envelope
{ "items": [ { "id": "7bWq0R", "name": "Spring campaign copy", "tag": "v1.4.0", "status": "published", "published_at": "2026-03-02T14:05:00Z", "created_at": "2026-03-01T11:20:00Z" } ], "page_info": { "total_count": 42, "page": 1, "page_size": 20, "total_pages": 3 }}page_info describes the page you were given. total_count is how many releases match the request — it respects search, so it changes when you filter — and total_pages is that count over page_size, rounded up. page and page_size echo the values actually used, which is how you see that a page_size above 100 was clamped.
total_count counts only published releases, the same set the page itself is drawn from. There is no next-page link; page numbers are the whole contract.
Paging through every release
Request pages until page reaches page_info.total_pages, then stop. Because drafts are excluded by the query rather than filtered out of the page afterwards, a page is only ever short when it is the last one.
- Use
page_size=100. Fewer, larger pages cost less quota. - Prefer not to page at all. Most integrations want the newest release, not the full history — and for the newest release there is a better route that needs no API call whatsoever. See always fetching the latest release.
async function listAllReleases() { const all = []
for (let page = 1; ; page++) { const res = await fetch(`${API}/releases?page=${page}&page_size=100`, { headers }) if (!res.ok) throw new Error(`releases page ${page}: ${res.status}`) const { items, page_info } = await res.json() all.push(...items) if (page >= page_info.total_pages) break }
return all}A project with no published releases returns total_count: 0 and total_pages: 0, so the loop above stops after one request.
Sorting caveats
sort=status is accepted but not useful here: every release the public API returns has status: "published", so it sorts a constant. It exists because the parameter set is shared with the dashboard’s own release list, which does show drafts.
Sorting by published_at and by created_at can differ. A release created in March but published in April sorts differently under each, and created_at — the default — is about when the release was cut, not when it went live. If you mean “most recently live”, ask for sort=published_at.
Caching interacts with paging
Each distinct query string is cached separately for 60 seconds. Iterating pages 1..N produces N cache entries, and repeating the same iteration within the minute is served entirely from cache and costs no rate limit quota. Note that the cache key is the literal URL, so reordering parameters creates a separate entry. See Caching.