<!-- Source: https://docs.geopera.com/api-reference/pagination · Markdown for LLMs -->

# 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](/api-reference/operations) 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 `1`–`10000`) 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 `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.

```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](/api-reference/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](/api-reference/operations).
- **`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](/api-reference/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](/api-reference/rate-limits).

## Related

- [Operation reference](/api-reference/operations) — the authoritative input fields for every list and search operation.
- [Core concepts](/api-reference/concepts) — the operation model these calls follow.
- [Rate limits](/api-reference/rate-limits) — what to expect when paging large result sets.
- [Errors](/api-reference/errors) — how validation failures (out-of-range `limit`, unknown fields) are reported.
