# Pagination

> The pagination envelope every list response carries, and how to walk it: auto-iterate, one page at a time, collect everything, or by hand.

Source: https://docs.levelfour.ai/pagination


Every list endpoint returns one page of items plus the metadata to find the next. The SDKs will walk it for you, hand you one page at a time, or stay out of the way while you drive `page` and `page_size` yourself.

| Approach                                      | Reach for it when                                             |
| --------------------------------------------- | ------------------------------------------------------------- |
| [Auto-pagination](#auto-pagination)           | You want every result and would rather not think about pages  |
| [Collect all items](#collect-all-items)       | You want every result in one list and the result set is small |
| [Page-level iteration](#page-level-iteration) | You process results a batch at a time                         |
| [Manual pagination](#manual-pagination)       | You decide which page is fetched and when                     |

## Response format

Paginated endpoints are offset-based. You ask for a page number and a page size, and the response reports where that page sits in the full result set.

> **Note**
>
> Page numbers are 1-indexed. The first page is `page=1`.

```json
{
    "success": true,
    "data": {
        "items": [],
        "pagination": {
            "total_items": 142,
            "total_pages": 15,
            "current_page": 1,
            "page_size": 10,
            "has_next": true,
            "has_previous": false
        }
    },
    "timestamp": "2025-10-15T10:00:00Z"
}
```

`success`, `data` and `timestamp` are the standard envelope on every LevelFour response, described in the [API reference](/api-reference#response-format).

## Auto-pagination

The SDK fetches the next page when you reach the end of the current one, so your loop reads as though the whole result set were already in memory.

**Python**

```python  
for rec in client.recommendations.list(page_size=50):
    print(rec.recommendation_id)
```

**TypeScript**

```typescript  
for await (const rec of await client.recommendations.list({ page_size: 50 })) {
    console.log(rec.recommendation_id);
}
```

**Go**

```go  
page, err := client.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{
    PageSize: levelfour.Int(50),
})
if err != nil {
    log.Fatal(err)
}

iter := page.Iterator()
for iter.Next(ctx) {
    rec := iter.Current()
    fmt.Println(rec.RecommendationID)
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}
```

### Async (Python)

The async client takes the same call. Await it once, then iterate with `async for`.

```python
async for rec in await async_client.recommendations.list(page_size=50):
    print(rec.recommendation_id)
```

### Collect all items

Fetch every page and return all items as a single collection.

> **Warning**
>
> Collecting walks every page before it returns anything, and holds the whole result set in memory at once. On a large result set, iterate instead.

**Python**

```python  
all_recs = list(client.recommendations.list(page_size=100))
```

**TypeScript**

```typescript  
import { collectAll } from "levelfour";

const allRecs = await collectAll(await client.recommendations.list());
```

**Go**

```go  
page, err := client.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{
    PageSize: levelfour.Int(100),
})
if err != nil {
    log.Fatal(err)
}

allRecs, err := levelfour.CollectAll(ctx, page)
if err != nil {
    log.Fatal(err)
}
```

## Page-level iteration

Each turn of the loop hands you a whole page rather than a single item.

**Python**

```python  
for page in client.recommendations.list(page_size=50).iter_pages():
    print(f"Processing {len(page.items)} items")
    for item in page.items:
        process(item)
```

**TypeScript**

```typescript  
const page = await client.recommendations.list({ page: 1, page_size: 50 });
console.log(page.data);

while (page.hasNextPage()) {
    await page.getNextPage();
    console.log(page.data);
}
```

**Go**

```go  
page, err := client.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{
    PageSize: levelfour.Int(50),
})
if err != nil {
    log.Fatal(err)
}

for {
    for _, rec := range page.Results {
        fmt.Println(rec.RecommendationID)
    }
    nextPage, err := page.GetNextPage(ctx)
    if errors.Is(err, levelfour.ErrNoPages) {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
    page = nextPage
}
```

> **Note**
>
> The Go client signals a finished walk by returning `levelfour.ErrNoPages` from `GetNextPage`. Match it with `errors.Is` before treating the error as a failure.

### Async pages (Python)

`iter_pages()` works the same way on the async client.

```python
async for page in (await async_client.recommendations.list(page_size=50)).iter_pages():
    print(f"Processing {len(page.items)} items")
```

## Manual pagination

Pass `page` and `page_size` directly and handle navigation yourself.

**Python**

```python  
page = client.recommendations.list(page=1, page_size=50)
while page:
    for item in page.items:
        process(item)
    page = page.next_page()
```

**TypeScript**

```typescript  
const page = await client.recommendations.list({ page: 1, page_size: 50 });
console.log(page.data);

if (page.hasNextPage()) {
    await page.getNextPage();
}
```

**Go**

```go  
page, err := client.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{
    Page:     levelfour.Int(1),
    PageSize: levelfour.Int(50),
})
if err != nil {
    log.Fatal(err)
}

nextPage, err := page.GetNextPage(ctx)
if errors.Is(err, levelfour.ErrNoPages) {
    fmt.Println("No more pages")
}
```

## Paginated endpoints

Every method below returns a paginated result. The [resource pages](/resources) carry each one's parameters and response shape.

| What it lists                      | Python                                              | TypeScript                                       | Go                                               |
| ---------------------------------- | --------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------ |
| Recommendations                    | `recommendations.list()`                            | `recommendations.list()`                         | `Recommendations.List()`                         |
| Recommendations for one provider   | `recommendations.list_by_provider()`                | `recommendations.listByProvider()`               | `Recommendations.ListByProvider()`               |
| Potential savings for one provider | `recommendations.list_provider_potential_savings()` | `recommendations.listProviderPotentialSavings()` | `Recommendations.ListProviderPotentialSavings()` |
| Realized savings                   | `recommendations.audit.list()`                      | `recommendations.audit.list()`                   | `Recommendations.Audit.List()`                   |
| Realized savings for one provider  | `recommendations.audit.list_by_provider()`          | `recommendations.audit.listByProvider()`         | `Recommendations.Audit.ListByProvider()`         |
| Cost breakdown                     | `costs.list()`                                      | `costs.list()`                                   | `Costs.List()`                                   |
| Cost breakdown for one provider    | `costs.list_by_provider()`                          | `costs.listByProvider()`                         | `Costs.ListByProvider()`                         |

> **Note**
>
> There is no `providers` namespace behind these. `client.providers` lists your connected providers and nothing else. Everything scoped to a single provider lives on `recommendations` or `costs` and takes a `provider_id`.

## Defaults

These are the values a request falls back to when you leave the parameter out. `page` is always 1.

The page size is not the same everywhere. The two savings lists and the recommendations list are sized for scanning a backlog; the breakdowns return one row per resource and are capped tighter.

| Endpoint                                            | `page_size` default | Maximum |
| --------------------------------------------------- | ------------------- | ------- |
| `recommendations.list()`                            | 20                  | 200     |
| `recommendations.list_by_provider()`                | 20                  | 200     |
| `recommendations.list_provider_potential_savings()` | 20                  | 200     |
| `recommendations.audit.list()`                      | 10                  | 100     |
| `recommendations.audit.list_by_provider()`          | 10                  | 100     |
| `costs.list()`                                      | 10                  | 100     |
| `costs.list_by_provider()`                          | 10                  | 100     |

> **Warning**
>
> Asking for more than the maximum is a `422`, not a silent clamp. The auto-paginating iterators above sidestep the whole question, because they carry the page size the server gave them.

## Next

* [Error handling](/error-handling) maps each status code to its typed class and lists what the SDKs retry before you see the failure
* [Resources](/resources) documents every method, paginated or not, in all three SDKs
* [Python](/sdks/python), [TypeScript](/sdks/typescript) and [Go](/sdks/go) name the pager type each client returns
