# Pagination

> How list endpoints paginate, when to use limit/offset versus the export stream, and how to walk large workspaces without hitting limits.

Source: https://productos.ca/documentation/api/v1/pagination (last reviewed 2026-09-22)

Every list endpoint returns paginated results. ProductOS uses **limit + offset**, with a separate **export** endpoint that returns every row of an entity in one response.

## Default: limit + offset

```
GET /api/v1/feedback?limit=50&offset=100
```

| Parameter | Default | Max | Meaning |
| :--- | :--- | :--- | :--- |
| `limit` | 50 | 200 | Rows per page |
| `offset` | 0 | – | Rows to skip |
| `sort` | per-entity default | – | Sort field; see the entity reference |
| `order` | per-entity default | – | `asc` \| `desc` |

Response envelope:

```json
{
  "data": [ /* up to limit rows */ ],
  "pagination": {
    "limit": 50,
    "offset": 100,
    "total": 423
  },
  "links": {
    "self": "https://productos.ca/api/v1/feedback?limit=50&offset=100",
    "next": "https://productos.ca/api/v1/feedback?limit=50&offset=150",
    "prev": "https://productos.ca/api/v1/feedback?limit=50&offset=50"
  }
}
```

`pagination.total` is the **total matching rows** for your filter, useful for "Showing 51–100 of 423" UIs.

Trashed rows and drafts are left out of list responses unless you pass `include_deleted=true` or `include_drafts=true`.

## When limit/offset gets slow

`OFFSET` is O(rows skipped) at the database level. On workspaces with >10k rows of a single entity, paginating past offset ~10k starts to feel slow (>500ms). Two options:

### Filter your way down

The easier fix: combine with filters. `GET /api/v1/feedback?status=new&offset=5000` is much faster than `GET /api/v1/feedback?offset=5000` because the index narrows the scan.

### Use the export endpoint

For full-dataset exports (analytics, backups, migrations), use the export endpoint:

```
GET /api/v1/{entity}/export?format=json
```

It returns every row of the entity in one response, oldest first, including drafts and trashed rows (check `deleted_at`). `format=json` returns `{ "data": [...], "count": n }`; `format=csv` (the default) returns a file download:

```bash
curl 'https://productos.ca/api/v1/feedback/export?format=csv' \
  -H "Authorization: Bearer $TOKEN" \
  -o feedback.csv
```

Exports have their own rate bucket, 6 per minute per user, separate from list reads.

## Sort stability

The default sort field varies per entity (`created_at` for most; `votes` for feedback, `date` for releases). Rows are ordered by the one field you pick, with no automatic tiebreaker, so when many rows share a value (say `votes = 5`) the same `offset=50` can land on different rows between calls and you'd see duplicates or gaps. For a stable walk, sort by a unique field such as `id_number` or `created_at`.

## Filtering

Every entity lists its filterable fields on its reference page (for example [feedback](https://productos.ca/documentation/api/v1/reference/feedback)). Repeat a filter to OR multiple values (a comma-separated value is matched literally, not split):

```
GET /api/v1/feedback?status=new&status=under-review
```

Filters AND across different fields, OR within the same field. There's no support for negation or range filters via the URL syntax, use the export endpoint + downstream filtering for those cases.

## Search

`?q=substring` runs a case-insensitive substring match across the entity's `searchable` fields. It's a `LIKE %term%` query, not full-text, fine for short queries on workspaces up to ~100k rows. Semantic search across all entities (`POST /api/ai/search`) is available to the in-app session only, not to API tokens; the [MCP server](https://productos.ca/documentation/api/v1/mcp) offers `search_workspace` for connected clients.

## Junction expansion

For entities with junction tables, request inline expansion to avoid N+1:

```
GET /api/v1/feedback?expand=themes,features&limit=50
```

Name any of the entity's junctions, comma-separated; unknown names are ignored. Expansion counts as a slightly larger query but **one** request against the rate limit, not one per row.

## Next

- [Rate limits](https://productos.ca/documentation/api/v1/rate-limits): what to watch when walking large datasets.
- [Feedback entity reference](https://productos.ca/documentation/api/v1/reference/feedback): what fields are filterable and sortable per entity.
