Pagination
List and search operations return a bounded window of results, and you advance the window yourself: there is no single pagination convention across the API (there are three) and no client-side auto-iterator. Every operation is invoked the same way — POST /v1/op/{operation_id} with a JSON body and a Bearer token — so pagination is expressed entirely through fields in that JSON body, never through query strings or path segments, and which fields you set depends on which of the three conventions the operation follows.
The three conventions
Some provider-compatible operations carry their upstream pagination shape alongside the native ones. Rather than hide that, the API exposes each operation’s real input fields. There are three families:
| Convention | Fields | Defaults | Where you see it |
|---|---|---|---|
| Offset paging | limit, offset | limit 100, offset 0 | Most native list/search ops |
| Page paging | page, size | varies (see below) | Provider-compat list ops |
| Search paging | limit, page | limit 10, page 0 | STAC-style search ops |
The window size and the cursor are independent fields in all three. The only thing that
changes is the name of the size field (limit vs size) and the name of the
cursor field (offset vs page), and whether the cursor counts records (offset) or
windows (page).
Always confirm an operation’s real fields in the operation reference before you page it. The fields below are authoritative, but an operation you have not paged before may belong to a different family than you expect.
Offset paging — limit + offset
The most common convention. limit is the maximum number of records to return; offset is how many records to skip from the start of the result set. To walk the
whole set you hold limit fixed and increase offset by limit each round.
Operations in this family include items.list, items.search, items.search_org, clip.jobs.list, notifications.list, and alerts.events.list.
{ "project_id": "proj_123", "limit": 100, "offset": 0 }limit defaults to 100 and offset defaults to 0, so omitting both returns the
first 100 records. The next window is offset: 100, then offset: 200, and so on.
Page paging — page + size
Used by the provider-compatible order and tasking operations such as orders.list.
Here size is the window size and page is the zero-based page index — page 0 is the first window, page 1 is records size..2·size, and so on. size defaults
to 20 and page defaults to 0.
{ "status": "DELIVERED", "size": 20, "page": 0 }Search paging — limit + page
Used by STAC-style search such as stac.search. It mixes the two: limit is the
window size (as in offset paging) but the cursor is a zero-based page index (as in
page paging). For stac.search, limit defaults to 10 (range 1–10000) and page defaults to 0. processing.jobs.list follows the same limit + page shape
with limit defaulting to 20.
{ "collections": ["sentinel-2-l2a"], "limit": 50, "page": 0 }Token paging — catalog.search
catalog.search (and catalog.federated_search) are the exception: they cap a single
window with limit (default 100, range 1–500) and return an opaque next token for the following window. You do not compute an offset — you echo the token
back in the next field of the next request, and stop when the response carries no
token. Treat the token as a black box: do not parse it or construct one yourself.
{ "host_name": "earth-search", "collections": ["sentinel-2-l2a"], "limit": 100, "next": null }Reading the response
List and search operations return their records as a JSON array (or an object wrapping
one). The API does not guarantee a total count field on these responses, so do
not rely on one to decide when to stop. Instead, use the universal stopping rule:
Keep requesting the next window while the previous window came back full (its length equals the
limit/sizeyou asked for). The first short or empty window is the last page.
For token paging, the rule is simpler: stop when the response contains no next token.
This rule works for every convention and never over-reads: a final window that happens to be exactly full costs you one extra empty request, which is correct and cheap.
There is no auto-pagination
Neither the Python package (geopera) nor the TypeScript SDK (@geopera/sdk) ships a
lazy iterator that fetches subsequent pages for you. Each SDK call performs exactly one
operation invocation and returns exactly one window. You write the loop. This keeps
the SDKs thin and predictable — one call in, one typed result out — and leaves spend and
rate-limit behaviour fully in your hands. See rate limits before paging large result sets aggressively.
Worked example: list every item in a project
The project below has more items than one window holds. We page with items.list (offset paging) until a short window tells us we have reached the end.
Raw HTTP
First window:
POST /v1/op/items.list HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json
{ "project_id": "proj_123", "limit": 100, "offset": 0 }If that returns 100 items, request the next window with offset advanced by limit:
POST /v1/op/items.list HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json
{ "project_id": "proj_123", "limit": 100, "offset": 100 }Continue until a response holds fewer than 100 items.
Python
from geopera import Geopera
client = Geopera(token="gpra_your_api_key")
def list_all_items(project_id: str, page_size: int = 100):
offset = 0
items: list = []
while True:
window = client.op(
"items.list",
{"project_id": project_id, "limit": page_size, "offset": offset},
)
items.extend(window)
if len(window) < page_size:
break # short window → last page
offset += page_size
return items
all_items = list_all_items("proj_123")
print(f"fetched {len(all_items)} items")TypeScript
import { Geopera } from '@geopera/sdk';
const client = new Geopera({ token: 'gpra_your_api_key' });
async function listAllItems(projectId: string, pageSize = 100) {
let offset = 0;
const items: unknown[] = [];
for (;;) {
const window = await client.op('items.list', {
project_id: projectId,
limit: pageSize,
offset
});
items.push(...window);
if (window.length < pageSize) break; // short window → last page
offset += pageSize;
}
return items;
}
const allItems = await listAllItems('proj_123');
console.log(`fetched ${allItems.length} items`);Paging the page-based and search conventions
The loop is identical in shape; only the field names and the cursor arithmetic change.
Page paging (orders.list) — increment page, hold size:
def list_all_orders(status: str, size: int = 20):
page, orders = 0, []
while True:
window = client.op(
"orders.list",
{"status": status, "size": size, "page": page},
)
orders.extend(window)
if len(window) < size:
break
page += 1
return ordersSearch paging (stac.search) — increment page, hold limit:
def search_all(collections: list[str], limit: int = 50):
page, results = 0, []
while True:
window = client.op(
"stac.search",
{"collections": collections, "limit": limit, "page": page},
)
results.extend(window)
if len(window) < limit:
break
page += 1
return resultsToken paging (catalog.search) — echo the returned token, stop when it is absent:
def search_catalog(host_name: str, collections: list[str], limit: int = 100):
token, results = None, []
while True:
resp = client.op(
"catalog.search",
{
"host_name": host_name,
"collections": collections,
"limit": limit,
"next": token,
},
)
results.extend(resp["features"])
token = resp.get("next")
if not token:
break
return resultsGotchas
- Do not mix conventions. Sending
offsetto apage/sizeoperation, orsizeto alimit/offsetoperation, is an unknown field and may be rejected. Check the operation’s input model in the operation reference. pageis zero-based in every page/search operation here. The first page is0, not1.- Respect the bounds.
stac.searchcapslimitat10000andcatalog.searchcaps it at500. Exceeding a documented maximum is a validation error — see errors. - Results can shift under you. Offset and page cursors index into a live result
set; if records are added or removed between requests, a deep window can skip or
repeat a record. For exact snapshots, narrow the query (by
datetime,status, orcollection_id) rather than paging the entire set. - No
totalto trust. Drive your loop off window length (or thenexttoken), not off a count field the response may not include. - Each window is a billable, rate-limited call. Use the largest
limit/sizethe operation allows to minimise round trips, and see rate limits.
Related
- Operation reference — the authoritative input fields for every list and search operation.
- Core concepts — the operation model these calls follow.
- Rate limits — what to expect when paging large result sets.
- Errors — how validation failures (out-of-range
limit, unknown fields) are reported.