# Pagination and filtering

> Work through large collections without pulling your whole account down the wire.


Every collection endpoint is paginated. There is no way to ask for everything at once, on purpose: accounts with hundreds of thousands of inventory items exist, and an endpoint that tried to serialise all of them would fail for the people who need it most.

## The envelope

```json
{
    "data": [
        { "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5", "username": "farm_001" },
        { "id": "019fb42e-9a7e-728d-b960-8b4c2162898c", "username": "farm_002" }
    ],
    "meta": {
        "page": 1,
        "per_page": 50,
        "total": 418,
        "last_page": 9
    }
}
```

Single records are returned bare, with no envelope:

```json
{
    "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
    "username": "farm_001"
}
```

## Paging

| Parameter | Default | Max | Notes |
| --- | --- | --- | --- |
| `page` | `1` | | 1-indexed |
| `per_page` | `50` | `200` | Values above the max are clamped, not rejected |

Asking for `per_page=5000` gives you 200 and says so in `meta.per_page`. Read the value back rather than assuming you got what you asked for.

```bash tab=curl
curl "https://dashboard.steamlabs.dev/api/v1/accounts?page=2&per_page=200" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```python tab=Python
def all_accounts():
    page = 1

    while True:
        body = requests.get(
            "https://dashboard.steamlabs.dev/api/v1/accounts",
            headers={"Authorization": f"Bearer {api_key}"},
            params={"page": page, "per_page": 200},
        ).json()

        yield from body["data"]

        if page >= body["meta"]["last_page"]:
            return

        page += 1
```

```javascript tab=Node
async function* allAccounts() {
    let page = 1;

    while (true) {
        const response = await fetch(
            `https://dashboard.steamlabs.dev/api/v1/accounts?page=${page}&per_page=200`,
            { headers: { Authorization: `Bearer ${apiKey}` } },
        );

        const { data, meta } = await response.json();
        yield* data;

        if (page >= meta.last_page) return;
        page += 1;
    }
}
```

> [!TIP]
> Use `per_page=200`. Walking 400 accounts costs 2 requests instead of 8, which matters against your [rate limit](/docs/api/en/concepts/rate-limits).

## Sorting

| Parameter | Notes |
| --- | --- |
| `sort` | A field name from that endpoint's own list |
| `direction` | `asc` or `desc`. Most listings default to `desc` |

Each endpoint's page lists the fields it can sort by. Sorting by anything else returns `422`, so a typo tells you rather than quietly falling back to the default.

Two listings do it differently and say so on their own page: `GET /api/v1/profiles` and `GET /api/v1/profile-groups` take the direction inside `sort` (`?sort=-times_used`) and have no `direction` parameter. `GET /api/v1/trade-offers` and `GET /api/v1/trade-routing-templates` spell the direction `order` instead of `direction`.

## Filtering

Filters are plain query parameters. Every endpoint documents its own, but the shared conventions are:

| Shape | Example | Meaning |
| --- | --- | --- |
| Exact | `?boost=boosting` | Equals |
| Multiple | `?ban_status[]=vac&ban_status[]=game` | Any of. Repeat the parameter with `[]` |
| Search | `?search=farm_0` | Substring match on the endpoint's searchable fields |
| Date range | `?created_after=2026-07-01&created_before=2026-07-31` | Inclusive, ISO 8601 |
| Presence | `?details_never_refreshed=true` | Booleans take `true` / `false` (or `1` / `0`) |

A filter that takes several values is an array parameter. Sending `?ban_status=vac` where the endpoint expects a list is a `422`, not a single-value match: the rule is `array`, and being told is better than being silently ignored. Two exceptions, both documented where they live: `group_ids` on `GET /api/v1/profiles` also accepts a comma-separated string, and `status` on `GET /api/v1/boost/plans` accepts a bare value as well as a list.

Filters combine with AND:

```bash
curl "https://dashboard.steamlabs.dev/api/v1/accounts?session=online&ban_status[]=vac&sort=created_at&direction=desc" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

Filter server-side wherever you can. Pulling 5,000 accounts to keep 40 of them costs 25 requests and a lot of waiting, where the filtered call costs one.

## Counting without fetching

`meta.total` is the count of everything matching your filters, not just the current page. To count something, request `per_page=1` and read `meta.total`:

```bash
curl "https://dashboard.steamlabs.dev/api/v1/tasks?status=failed&per_page=1" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

## A note on stable paging

Paging is offset-based, so a collection that changes while you walk it can shift under you: a row inserted at the front pushes one row from page 1 onto page 2, and you would see it twice.

For collections that change constantly (tasks, inventory during a sell run), either sort by something stable such as `created_at` and filter by a fixed upper bound, or de-duplicate by ID as you collect.
