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:

ConventionFieldsDefaultsWhere you see it
Offset paginglimit, offsetlimit 100, offset 0Most native list/search ops
Page pagingpage, sizevaries (see below)Provider-compat list ops
Search paginglimit, pagelimit 10, page 0STAC-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.

json
{ "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.

json
{ "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 110000) and page defaults to 0. processing.jobs.list follows the same limit + page shape with limit defaulting to 20.

json
{ "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 1500) 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.

json
{ "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/size you 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:

http
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:

http
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

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

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:

python
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 orders

Search paging (stac.search) — increment page, hold limit:

python
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 results

Token paging (catalog.search) — echo the returned token, stop when it is absent:

python
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 results

Gotchas

  • Do not mix conventions. Sending offset to a page/size operation, or size to a limit/offset operation, is an unknown field and may be rejected. Check the operation’s input model in the operation reference.
  • page is zero-based in every page/search operation here. The first page is 0, not 1.
  • Respect the bounds. stac.search caps limit at 10000 and catalog.search caps it at 500. 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, or collection_id) rather than paging the entire set.
  • No total to trust. Drive your loop off window length (or the next token), not off a count field the response may not include.
  • Each window is a billable, rate-limited call. Use the largest limit/size the 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.