> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usehasp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> The cursor-based {data, meta} envelope every /v1 list endpoint returns, and how to page through results.

Every `/v1` (and Data API) `index` endpoint — API keys, webhooks, agents, models, audit events, knowledge documents, and the rest — returns results through the same cursor-paginated envelope. Learn it once, use it everywhere.

## The envelope

```json theme={null}
{
  "data": [
    { "id": "whep_01JQREQ7XZQK5N6PZ1VVXHYB8T", "url": "https://example.com/hooks/hasp", "..." : "..." },
    { "id": "whep_01JQRETMXBWQXKPRT8N2E9YQ0V", "url": "https://example.com/hooks/backup", "..." : "..." }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6MTIzfQ",
    "has_more": true
  }
}
```

`data` is always a flat list of the resource's own JSON shape — the same shape a single-resource `GET`/`POST` returns for one item. `meta.has_more` tells you whether another page exists; `meta.next_cursor` is an opaque token to fetch it. Both fields are always present, even on the last page (`next_cursor: null`, `has_more: false`).

## Paging through results

Request the first page with no `cursor`, then pass back `meta.next_cursor` on each subsequent request until `has_more` is `false`:

```bash theme={null}
curl "https://api.usehasp.com/v1/webhooks?limit=50" \
  -H "Authorization: Bearer hasp_api_live_<key>"

curl "https://api.usehasp.com/v1/webhooks?limit=50&cursor=eyJpZCI6MTIzfQ" \
  -H "Authorization: Bearer hasp_api_live_<key>"
```

```javascript theme={null}
async function listAllWebhooks() {
  const results = [];
  let cursor = null;

  do {
    const url = new URL('https://api.usehasp.com/v1/webhooks');
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, { headers: { Authorization: 'Bearer hasp_api_live_...' } });
    const body = await res.json();

    results.push(...body.data);
    cursor = body.meta.has_more ? body.meta.next_cursor : null;
  } while (cursor);

  return results;
}
```

## `cursor` is opaque — treat it as a token, not data

`cursor` is an encoded pointer into the underlying result set. Its contents are an implementation detail that can change between API versions — never decode it, parse it, or construct one by hand. Always pass back exactly the string `meta.next_cursor` gave you. A cursor from one endpoint is never valid on a different endpoint.

An unrecognized or stale cursor (e.g. one replayed after its underlying data shifted enough to become unresolvable) does not error or restart from the beginning — it resolves to whatever page would logically follow, which may be empty (`data: []`, `has_more: false`) rather than duplicate earlier results.

## `limit`

Every list endpoint accepts an optional `limit` query parameter bounding page size:

|                                      | Value                                                      |
| ------------------------------------ | ---------------------------------------------------------- |
| Default (omitted)                    | 20                                                         |
| Maximum                              | 100 — values above this are silently clamped, not rejected |
| Invalid (`0`, negative, non-numeric) | Falls back to the default (20)                             |

There is no `offset`/`page`-number pagination on `/v1` — cursor pagination is the only mode, since it stays correct under concurrent writes (an offset-based page can skip or repeat rows when items are inserted or deleted between requests; a cursor cannot).

## Sort order

Unless an endpoint's own reference page documents otherwise, list endpoints return results newest-first (most recently created first), and pagination walks strictly forward through that order — a cursor never re-orders results underneath a caller mid-walk.
