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

# Pagination

> Cursor-based pagination on every list endpoint.

Every list endpoint paginates with cursors, not page numbers. Cursors are stable across inserts and deletes and don't drift when the underlying ordering changes — a page-numbered API will eventually serve you a duplicate or skip a row; a cursor API won't.

## Parameters

```
GET /v1/cards?limit=50&cursor=eyJ...
```

| Parameter | Default | Max | Notes                                    |
| --------- | ------- | --- | ---------------------------------------- |
| `limit`   | 50      | 250 | Number of items in the response.         |
| `cursor`  | —       | —   | Opaque token from the previous response. |

Treat the cursor as opaque. It's a base64-encoded payload today; that's an implementation detail and may change.

## Response shape

Every list response carries a `pagination` block:

```json theme={"dark"}
{
  "data": [{ "id": "et-100", "...": "..." }, "..."],
  "pagination": {
    "next_cursor": "eyJpZCI6ImV0LTE1MCJ9",
    "has_more": true
  },
  "meta": { "request_id": "req_01H...", "cached": true }
}
```

* `next_cursor` is the token to pass on the next request, or `null` if there are no more pages.
* `has_more` is a boolean shortcut. Equivalent to `next_cursor !== null`.

When you reach the end:

```json theme={"dark"}
{
  "data": ["..."],
  "pagination": { "next_cursor": null, "has_more": false }
}
```

## Looping through everything

```javascript theme={"dark"}
async function* paginate(path, key) {
  let cursor = null;
  do {
    const url = new URL(path, 'https://api.elestrals.com');
    url.searchParams.set('limit', '250');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${key}` },
    });
    const body = await res.json();

    yield* body.data;
    cursor = body.pagination.next_cursor;
  } while (cursor);
}

for await (const card of paginate('/v1/cards', process.env.ELESTRALS_KEY)) {
  console.log(card.id, card.name);
}
```

A few things to notice:

* The first request omits `cursor` entirely. Don't pass `cursor=null` or `cursor=` — that's a `bad_request`.
* `limit=250` is the highest the API allows. You almost always want it for back-fills and full syncs; use a smaller `limit` for interactive UIs where time-to-first-result matters.
* The cursor is the *only* state you need to persist between pages. If a sync job crashes mid-way, store the cursor and resume.

## Single-resource endpoints

Endpoints that return one resource — `/v1/cards/{id}`, `/v1/sets/{code}` — don't paginate. The `pagination` block on these responses is always:

```json theme={"dark"}
{ "next_cursor": null, "has_more": false }
```

…so you can read it unconditionally without special-casing list vs detail.

## Errors

| Status | Code             | When                                                |
| ------ | ---------------- | --------------------------------------------------- |
| 400    | `invalid_cursor` | Cursor is malformed, expired, or from another path. |
| 400    | `bad_request`    | `limit` is non-numeric or > 250.                    |

Cursors are bound to the path *and* the filter parameters that produced them. Re-using a cursor from `/v1/cards?element=fire` against `/v1/cards?element=water` is `invalid_cursor`. The fix: drop the cursor and start over.
