Get startedDashboard

Pagination

Page through large results with cursors or offsets, using each endpoint's page sizes, page limits and row order.

View llms.txt

Endpoints that can return many rows split them into pages. Two styles are in use:

  • Cursor pages on /v1/prices, /v1/fundamentals, /v1/metrics, /v1/insiders, /v1/holdings, /v1/managers/{manager_id}/holdings and /v1/managers. Each page hands you a next_cursor for the next one.
  • Offset pages on /v1/search, /v1/index/{index_name} and /v1/index/{index_name}/events. You step offset forward by limit.

/v1/resolve and /v1/status return everything in one response.

/v1/changes is cursor-paginated through since, which takes the meta.next_cursor of the previous response, as Change feed describes.

Cursor pagination

Send limit to set the page size. When a page is not the last, the response carries a next_cursor string. Send the same request again with cursor set to that string to get the next page. The last page has next_cursor: null.

limitintDefault: 1000; 20 on /v1/metrics

Rows per page. Maximum 5000, or 100 on /v1/metrics. A whole number from 1 to 999,999,999; a value above the maximum is lowered to the maximum, and any other non-empty value returns HTTP 400. meta.limit reports the value applied on prices, fundamentals and metrics.

cursorstrDefault: none

The next_cursor from the previous page, passed back unchanged with the same path and the same other parameters. limit may change from one page to the next. An empty cursor= requests the first page. URL-encode it, as it can contain =.

On /v1/prices, /v1/fundamentals and /v1/metrics, the page fields are inside meta:

Terminal
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://api.xfinlink.com/v1/prices/AAPL?start=2026-01-02&end=2026-09-25&fields=close&limit=100"
response.json
{
  "data": [
    {
      "entity_id": 10003635,
      "ticker": "AAPL",
      "entity_name": "APPLE INC",
      "gics_sector": "Information Technology",
      "date": "2026-01-02",
      "close": 271.01,
      "price_basis": "trade"
    },
    ...
  ],
  "meta": {
    ...
    "data_through": "2026-05-27",
    "count": 100,
    "limit": 100,
    "has_more": true,
    "next_cursor": "djJ8ZXlKeUlqb2ljSEpwWTJWeklpd2ljU0k2SWxwNVdqWXRSelE1WTJoS1NtMXBUMEZXTURKMlJXY2lMQ0p3SWpveUxDSnJJam9pVFZSQmQwMUVUVEpOZWxZNFRXcEJlVTVwTUhkT1V6QjVUak4zZVNKOXw1aHprVkZyWm5jY2JlMUJZdTVmRWNB",
    ...
  }
}
  • meta.count: rows on this page.
  • meta.limit: the page size applied.
  • meta.has_more: true when another page follows.
  • meta.next_cursor: the value for cursor, or null on the last page.

On /v1/insiders, /v1/holdings, /v1/managers/{manager_id}/holdings and /v1/managers, count sits at the top level of the body, next to data, and meta carries has_more and next_cursor. A top-level next_cursor holds the same value as meta.next_cursor. These four endpoints need Pro or higher.

Terminal
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://api.xfinlink.com/v1/insiders/AAPL?limit=1"
response.json
{
  "tickers": ["AAPL"],
  "data": [
    {
      "entity_id": 1,
      "ticker": "AAPL",
      "entity_name": "APPLE INC",
      "transaction_date": "2026-10-02",
      "filing_date": "2026-10-05",
      "form_type": "4",
      ...
    }
  ],
  "data_through": "2026-10-05",
  "count": 1,
  "next_cursor": "djJ8ZXlKeUlqb2lhVzV6YVdSbGNuTWlMQ0p4SWpvaVNuRmhRVEp4WXpCMGRrbHNkbE5PUld4a1EwTmFVU0lzSW5BaU9qSXNJbXNpT2lKTmFrRjVUbWt3ZUUxRE1IZE5ibmQzVFVSQmVFMVVVWGROZWxsNFRGUkpNa3hVUVhwUFJGa3pUa2gzZDJaRVl6MGlmUXxqTHJpUjFjWEwxeTRKZFlON2psQm93",
  "meta": {
    "entity_id_is": "issuer",
    ...
    "has_more": true,
    "next_cursor": "djJ8ZXlKeUlqb2lhVzV6YVdSbGNuTWlMQ0p4SWpvaVNuRmhRVEp4WXpCMGRrbHNkbE5PUld4a1EwTmFVU0lzSW5BaU9qSXNJbXNpT2lKTmFrRjVUbWt3ZUUxRE1IZE5ibmQzVFVSQmVFMVVVWGROZWxsNFRGUkpNa3hVUVhwUFJGa3pUa2gzZDJaRVl6MGlmUXxqTHJpUjFjWEwxeTRKZFlON2psQm93"
  }
}

Treat a cursor as an opaque string. A cursor works only with the request that returned it: the same path and the same values for the endpoint's parameters, except limit, which may change from one page to the next. A value the API did not issue, a cursor that has been altered, and a cursor sent with another path, a changed parameter or another endpoint each return HTTP 400 with a detail that begins Invalid cursor. Repeat the request without cursor to start again from the first page.

Terminal
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://api.xfinlink.com/v1/prices/AAPL?start=2026-09-01&cursor=abc"
response.json
{
  "error": "bad_request",
  "code": "bad_request",
  "status": 400,
  "detail": "Invalid cursor: not a cursor this API issued, or one from an earlier paging scheme. Repeat the request without cursor to start from the first page."
}

The next_cursor from the first prices example above, sent with /v1/prices/MSFT and the same parameters:

response.json
{
  "error": "bad_request",
  "code": "bad_request",
  "status": 400,
  "detail": "Invalid cursor: it belongs to a different request. Send it with the same path and parameters as the request that returned it (only limit may change). Repeat the request without cursor to start from the first page."
}

Page sizes

Default and maximum limit per endpoint:

  • /v1/prices: default 1000, maximum 5000. limit=99999 is applied as 5000; limit=0 and limit=abc return HTTP 400.
  • /v1/fundamentals: default 1000, maximum 5000.
  • /v1/metrics: default 20, maximum 100. limit=500 is applied as 100.
  • /v1/insiders: default 1000, maximum 5000.
  • /v1/holdings: default 1000, maximum 5000.
  • /v1/managers/{manager_id}/holdings: default 1000, maximum 5000.
  • /v1/managers: default 1000, maximum 5000.
  • /v1/search: default 50, maximum 500. A larger value returns HTTP 400.
  • /v1/index/{index_name}: default 1000, maximum 1000. A larger value returns HTTP 400.
  • /v1/index/{index_name}/events: default 1000, maximum 1000. A larger value returns HTTP 400.

Cursor endpoints lower an oversized limit without an error. Offset endpoints reject it.

Pages per query

A cursor query on any of the seven cursor endpoints, /v1/prices, /v1/fundamentals, /v1/metrics, /v1/insiders, /v1/holdings, /v1/managers/{manager_id}/holdings and /v1/managers, can run to a fixed number of pages, counting the first:

  • Free: 10 pages
  • Pro: 50 pages
  • Max: 100 pages
  • Redistribution: 200 pages

At the maximum limit of 5,000 rows, a Pro query reaches 250,000 rows. The last allowed page still carries a next_cursor; requesting it returns HTTP 400 with code pagination_limit_reached, and error reads Pagination limit reached:

response.json
{
  "error": "Pagination limit reached",
  "code": "pagination_limit_reached",
  "status": 400,
  "detail": "Pro tier supports up to 50 pages per query.",
  "message": "Pro tier supports up to 50 pages per query."
}

On Free the message adds an upgrade link:

response.json
{
  "error": "Pagination limit reached",
  "code": "pagination_limit_reached",
  "status": 400,
  "detail": "Free tier supports up to 10 pages per query. Upgrade to Pro for 50-page queries: https://xfinlink.com/pricing",
  "message": "Free tier supports up to 10 pages per query. Upgrade to Pro for 50-page queries: https://xfinlink.com/pricing"
}

To read more rows than one query allows, split the request by date window or by ticker. Each page counts toward your rate limits and row quotas; see Rate limits.

Offset pagination

/v1/search, /v1/index/{index_name} and /v1/index/{index_name}/events take limit and offset, and report meta.total, the number of rows that match.

limitintDefault: 50 on search; 1000 on index and index events

Rows per page. Maximum 500 on search and 1000 on index and index events. A value above the maximum, or below 1, returns HTTP 400.

offsetintDefault: 0

Rows to skip. A negative value returns HTTP 400.

/v1/index/{index_name} and /v1/index/{index_name}/events report meta.count, meta.total and meta.has_more:

Terminal
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://api.xfinlink.com/v1/index/sp500?as_of=2000-01-03&limit=100&offset=400"
response.json
{
  "data": [
    {
      "entity_id": 10007858,
      "ticker": "VO",
      "entity_name": "SEAGRAM LTD",
      "added_date": "1957-03-01",
      "removed_date": "2000-12-08",
      "member_grain": "security"
    },
    {
      "entity_id": 10027193,
      "ticker": "SEE",
      "entity_name": "SEALED AIR CORP NEW",
      "added_date": "1998-04-01",
      "removed_date": "2023-12-15",
      "member_grain": "security"
    },
    ...
  ],
  "meta": {
    "index": "SP500",
    "as_of": "2000-01-03",
    "data_through": "2026-09-21",
    "entity_id_grain": "security",
    "coverage_end": "2026-09-26",
    "count": 100,
    "total": 500,
    "has_more": false,
    ...
  }
}

/v1/search reports meta.count, meta.limit, meta.offset, meta.total and meta.has_more, which is true while offset + count is less than total:

Terminal
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://api.xfinlink.com/v1/search?q=bank&limit=500&offset=100"
response.json
{
  "data": [...],
  "meta": {
    "count": 500,
    "limit": 500,
    "offset": 100,
    "total": 1268,
    "has_more": true,
    ...
  }
}

Row order

Every endpoint returns rows in a fixed order, and each next_cursor continues from the end of its page:

  • /v1/prices: entity_id ascending, then date ascending (oldest first).
  • /v1/fundamentals: entity_id ascending, then period_end ascending, then the date the row became known ascending, then period_type (annual before quarterly), then the row's own filing. Under view=as_reported, the filings of one period appear oldest first.
  • /v1/metrics: entity_id ascending, then period_end ascending, then the statement behind the row.
  • /v1/insiders: transaction_date descending (newest first), then document_id descending, derivative lines before non-derivative lines, then line sequence descending.
  • /v1/holdings: report_date descending, then shares descending (largest position first), then manager_id descending, then the position's own keys.
  • /v1/managers/{manager_id}/holdings: report_date descending, then value_usd descending, then the position's own keys.
  • /v1/managers: manager_name ignoring letter case, then manager_id.
  • /v1/search: with q, by match strength (exact ticker, then ticker prefix, then name prefix, then any other match); without q, by name, then entity_id.
  • /v1/index/{index_name}: entity_name ascending, then each member's own key.
  • /v1/index/{index_name}/events: effective_date ascending, added before removed on the same date, then entity_name, then each member's own key.

Each order ends in a key that no two rows share, so a page can end between any two rows without skipping or repeating one. Two index members with the same name, such as Alphabet's GOOG and GOOGL, each appear once.

A multi-ticker price request returns each security's full date range before the next security's, in entity_id order.

Under view=as_reported, a fundamentals page that would end partway through one period's filings ends before that period instead, and the next page starts with it. When one period alone has more filings than limit, the page ends inside that period and the next page continues with its remaining filings.

A complete loop

This Python loop reads every page of a cursor endpoint. It reads next_cursor from meta or from the top level, so it works on all seven cursor endpoints. It uses httpx, which installs with the xfinlink client.

paginate.py
import httpx

API_KEY = "YOUR_API_KEY"
url = "https://api.xfinlink.com/v1/prices/AAPL"
params = {"start": "2026-01-02", "end": "2026-09-25", "fields": "close", "limit": 100}

rows, page = [], 0
with httpx.Client(headers={"X-API-Key": API_KEY}, timeout=60) as client:
    while True:
        r = client.get(url, params=params)
        r.raise_for_status()
        body = r.json()
        page += 1
        rows.extend(body["data"])
        meta = body.get("meta", {})
        cursor = meta.get("next_cursor") or body.get("next_cursor")
        print(f"page {page}: {len(body['data'])} rows, next_cursor={cursor}")
        if not cursor:
            break
        params["cursor"] = cursor

print(len(rows), "rows from", rows[0]["date"], "to", rows[-1]["date"])
stdout
page 1: 100 rows, next_cursor=djJ8ZXlKeUlqb2ljSEpwWTJWeklpd2ljU0k2SWxwNVdqWXRSelE1WTJoS1NtMXBUMEZXTURKMlJXY2lMQ0p3SWpveUxDSnJJam9pVFZSQmQwMUVUVEpOZWxZNFRXcEJlVTVwTUhkT1V6QjVUak4zZVNKOXw1aHprVkZyWm5jY2JlMUJZdTVmRWNB
page 2: 84 rows, next_cursor=None
184 rows from 2026-01-02 to 2026-09-25

The same loop in the shell saves each page to a file. curl -G --data-urlencode encodes the cursor, and python3 reads the next cursor from the saved page.

paginate.sh
#!/bin/bash
BASE="https://api.xfinlink.com/v1/prices/AAPL"
CURSOR=""
PAGE=1
while :; do
  curl -s -G "$BASE" -H "X-API-Key: YOUR_API_KEY" \
    --data-urlencode "start=2026-01-02" --data-urlencode "end=2026-09-25" \
    --data-urlencode "fields=close" --data-urlencode "limit=100" \
    ${CURSOR:+--data-urlencode "cursor=$CURSOR"} > "page_$PAGE.json"
  CURSOR=$(python3 -c 'import json, sys; b = json.load(open(sys.argv[1])); print(b.get("meta", {}).get("next_cursor") or b.get("next_cursor") or "")' "page_$PAGE.json")
  echo "page $PAGE: next_cursor=${CURSOR:-none}"
  [ -z "$CURSOR" ] && break
  PAGE=$((PAGE + 1))
done
stdout
page 1: next_cursor=djJ8ZXlKeUlqb2ljSEpwWTJWeklpd2ljU0k2SWxwNVdqWXRSelE1WTJoS1NtMXBUMEZXTURKMlJXY2lMQ0p3SWpveUxDSnJJam9pVFZSQmQwMUVUVEpOZWxZNFRXcEJlVTVwTUhkT1V6QjVUak4zZVNKOXw1aHprVkZyWm5jY2JlMUJZdTVmRWNB
page 2: next_cursor=none

page_1.json holds 100 rows from 2026-01-02 to 2026-05-27 with meta.has_more: true. page_2.json holds 84 rows from 2026-05-28 to 2026-09-25 with meta.has_more: false.

For an offset endpoint, add limit to offset after each page and stop when offset reaches meta.total.

Paging in the Python client

xfl.prices(), xfl.fundamentals() and xfl.metrics() follow next_cursor for you, requesting pages of up to 5,000 rows (100 on metrics) and stopping at max_rows or after 50 pages. In these functions limit caps the total rows returned, not the page size.

client_paging.py
import xfinlink as xfl

xfl.set_api_key("YOUR_API_KEY")

df = xfl.prices("AAPL", start="2026-01-02", end="2026-09-25", fields=["close"])
print(len(df), df["date"].min().date(), df["date"].max().date())
stdout
184 2026-01-02 2026-09-25

The client asked for pages of up to 5,000 rows, so those 184 rows arrived on one page; a longer window arrives over several pages and the client joins them. xfl.insiders(), xfl.holdings(), xfl.manager_holdings() and xfl.managers() return one page per call, up to 5,000 rows; narrow the date window per call, or use the loop above, to read further. xfl.index() and xfl.index_events() take limit and offset and return one page per call. Python client lists every argument.

Pagination errors

statuserror codewhen it happenshow to fix
400bad_requestcursor is not a value the API issued, has been altered, or came from another request or endpoint. detail begins "Invalid cursor".Pass next_cursor back unchanged and URL-encoded, with the same path and parameters, or repeat the request without cursor.
400pagination_limit_reachedThe query asks for more pages than your plan allows. error reads Pagination limit reached.Split the query by date window or ticker, or move to a plan with more pages per query.
400bad_requestlimit on /v1/search is above 500 ("limit max is 500"), or on /v1/index/{index_name} or /events above 1000 ("limit max is 1000").Lower limit and page with offset.
400bad_requestlimit on an offset endpoint is below 1 ("limit must be a positive integer"), or offset is negative ("offset must be a non-negative integer").Send limit of at least 1 and offset of 0 or more.

Was this page helpful?