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

# Pagination

> The list envelope used across OnTime's API, its parameters, and why the public v1 endpoints don't use it.

## The public API does not paginate

<Warning>
  **Neither public v1 endpoint is paginated.** `GET /employees` returns the whole directory and `GET /attendance?date=` returns every employee's record for that date, in one response.

  Plan for a large single response rather than a page loop. If your organization has thousands of employees, stream or chunk on your side.
</Warning>

Public responses use a `meta` and `data` envelope with a count:

```json theme={null}
{
  "meta": { "count": 247 },
  "data": [ /* … */ ]
}
```

`meta.count` equals `data.length` — it is a convenience, not a total across pages.

## The list envelope elsewhere

Paginated list endpoints across the rest of the API share one envelope:

```json theme={null}
{
  "items": [ /* … */ ],
  "total": 247,
  "page": 1,
  "pageSize": 50
}
```

<ResponseField name="items" type="array">
  The rows for this page.
</ResponseField>

<ResponseField name="total" type="integer">
  Rows matching the filter across **all** pages — not `items.length`.
</ResponseField>

<ResponseField name="page" type="integer">
  The requested page, echoed back so you need not trust your own request state.
</ResponseField>

<ResponseField name="pageSize" type="integer">
  The requested page size, echoed back.
</ResponseField>

Endpoints that carry aggregates add them alongside — `kpis`, `facets`, `pending` — at the same level as `items`.

## Parameters

<ParamField query="page" default="1" type="integer">
  1-indexed. Minimum 1.
</ParamField>

<ParamField query="pageSize" default="50" type="integer">
  Rows per page. Minimum 1, **maximum 100**. Requesting more fails validation with `400`.
</ParamField>

## Iterating

Compute the page count from `total` and `pageSize` rather than looping until an empty page — it saves a request and is robust to rows being added mid-iteration.

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function* pages<T>(path: string, pageSize = 100): AsyncGenerator<T[]> {
    let page = 1;
    let totalPages = 1;

    do {
      const url = `https://app.ontime.hosai.app${path}?page=${page}&pageSize=${pageSize}`;
      const res = await fetch(url, {
        headers: { Authorization: `Bearer ${process.env.ONTIME_API_KEY}` },
      });
      if (!res.ok) throw new Error(`OnTime ${res.status}`);

      const body = await res.json();
      totalPages = Math.max(1, Math.ceil(body.total / body.pageSize));
      yield body.items as T[];
      page += 1;
    } while (page <= totalPages);
  }

  for await (const batch of pages("/api/employees")) {
    console.log(`${batch.length} rows`);
  }
  ```

  ```bash curl theme={null}
  # First page, and read `total` to work out how many there are.
  curl "https://app.ontime.hosai.app/api/employees?page=1&pageSize=100" \
    -H "Authorization: Bearer $ONTIME_API_KEY"

  # Subsequent pages.
  curl "https://app.ontime.hosai.app/api/employees?page=2&pageSize=100" \
    -H "Authorization: Bearer $ONTIME_API_KEY"
  ```
</CodeGroup>

<Note>
  Pagination is **offset-based**, not cursor-based. Rows inserted or deleted between page requests can shift the window, so a long iteration over a changing dataset may see a row twice or miss one. For anything where that matters, filter to a closed period — a past month — rather than iterating a live list.
</Note>

<Warning>
  The paginated endpoints are on the **internal** surface, which authenticates with a session cookie and may change without notice. See [Overview](/developers/overview) for why you should build against `/api/public/v1/*` instead.
</Warning>
