Skip to main content
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...
ParameterDefaultMaxNotes
limit50250Number of items in the response.
cursorOpaque 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:
{
  "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:
{
  "data": ["..."],
  "pagination": { "next_cursor": null, "has_more": false }
}

Looping through everything

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:
{ "next_cursor": null, "has_more": false }
…so you can read it unconditionally without special-casing list vs detail.

Errors

StatusCodeWhen
400invalid_cursorCursor is malformed, expired, or from another path.
400bad_requestlimit 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.