---
title: "Pagination"
description: "How Tapetide collections page with an opaque cursor, and how price history pages with end_date."
canonical: "https://developers.tapetide.com/pagination/"
---

# Pagination

Tapetide never truncates a response to fit. When a result is larger than one page, the response says so and gives you the exact value to ask for the next page.

## Cursor pagination (collections)

Collection endpoints return an array in `data`, paging fields in `meta`, and a ready-made next URL in `links`:

```json
{
  "data": [
    { "date": "2026-10-20", "name": "Diwali Laxmi Pujan", "exchange": "NSE", "type": "FULL" }
  ],
  "meta": {
    "as_of": "2026-10-16T09:30:00.000Z",
    "route": "listMarketHolidays",
    "count": 1,
    "limit": 100,
    "has_more": true,
    "next_cursor": "eyJkIjoiMjAyNi0xMC0yMCJ9"
  },
  "links": { "next": "/v1/market/holidays?from=2026-01-01&cursor=eyJkIjoiMjAyNi0xMC0yMCJ9" }
}
```

| Field | Meaning |
| --- | --- |
| `meta.count` | Items in this page. |
| `meta.limit` | The page size that was applied. |
| `meta.has_more` | `true` when another page exists. |
| `meta.next_cursor` | Opaque string for the next page, or `null` on the last page. |
| `links.next` | Path and query for the next page with your filters kept, or `null`. Prefix it with `https://api.tapetide.com`. |

### The rules

-   **`limit`** defaults per endpoint (shown in the reference) and has a per-endpoint maximum. A value above the maximum is rejected with `400 invalid_request`. It is never silently clamped.
-   **`cursor`** is opaque. Pass `meta.next_cursor` back unchanged, with the same filters. A cursor the API did not issue returns `400 invalid_request` with `details.param = "cursor"`.
-   Paging is keyset, not offset, so pages stay stable while new rows arrive at the other end of the range.
-   Each page is one request and costs one unit.
-   Unknown query parameters are rejected with a `400` that lists the accepted ones, so a typo never silently returns unfiltered data.

### Follow every page

```python
import os, requests

BASE = "https://api.tapetide.com"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['TAPETIDE_TOKEN']}"

url = "/v1/market/holidays?from=2020-01-01&limit=500"
rows = []
while url:
    body = s.get(BASE + url, timeout=30).json()
    rows.extend(body["data"])
    url = body["links"]["next"]
print(len(rows))
```

```js
let url = "/v1/market/holidays?from=2020-01-01&limit=500";
const rows = [];
while (url) {
  const res = await fetch(`https://api.tapetide.com${url}`, {
    headers: { Authorization: `Bearer ${process.env.TAPETIDE_TOKEN}` },
  });
  const body = await res.json();
  rows.push(...body.data);
  url = body.links.next;
}
```

## Window pagination (price history)

`/v1/stocks/{symbol}/price` serves at most 2,000 calendar days per call. It reports the window it actually served in `meta.window`:

```json
"window": {
  "requested_days": 2000,
  "served_days": 2000,
  "max_days": 2000,
  "truncated": false,
  "first_date": "2021-04-26",
  "last_date": "2026-10-14",
  "has_more": true,
  "next_cursor": "2021-04-25"
}
```

To page further back, pass `meta.window.next_cursor` as `end_date` on the next call and repeat until `has_more` is `false`. Always read `truncated`: when it is `true` you asked for more days than one call serves.

## Bounded lists without a cursor

Some older routes (for example `/v1/stocks/{symbol}/events`, `/deals`, `/v1/market/news`) return the most recent N items with a `limit` parameter and no cursor. The reference states the default and maximum for each.
