Pagination
Page through large results with cursors or offsets, using each endpoint's page sizes, page limits and row order.
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}/holdingsand/v1/managers. Each page hands you anext_cursorfor the next one. - Offset pages on
/v1/search,/v1/index/{index_name}and/v1/index/{index_name}/events. You stepoffsetforward bylimit.
/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/metricsRows 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: noneThe 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:
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"{
"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:truewhen another page follows.meta.next_cursor: the value forcursor, ornullon 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.
curl -H "X-API-Key: YOUR_API_KEY" \
"https://api.xfinlink.com/v1/insiders/AAPL?limit=1"{
"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.
curl -H "X-API-Key: YOUR_API_KEY" \
"https://api.xfinlink.com/v1/prices/AAPL?start=2026-09-01&cursor=abc"{
"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:
{
"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: default1000, maximum5000.limit=99999is applied as5000;limit=0andlimit=abcreturn HTTP 400./v1/fundamentals: default1000, maximum5000./v1/metrics: default20, maximum100.limit=500is applied as100./v1/insiders: default1000, maximum5000./v1/holdings: default1000, maximum5000./v1/managers/{manager_id}/holdings: default1000, maximum5000./v1/managers: default1000, maximum5000./v1/search: default50, maximum500. A larger value returns HTTP 400./v1/index/{index_name}: default1000, maximum1000. A larger value returns HTTP 400./v1/index/{index_name}/events: default1000, maximum1000. 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:
{
"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:
{
"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 eventsRows 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: 0Rows 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:
curl -H "X-API-Key: YOUR_API_KEY" \
"https://api.xfinlink.com/v1/index/sp500?as_of=2000-01-03&limit=100&offset=400"{
"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:
curl -H "X-API-Key: YOUR_API_KEY" \
"https://api.xfinlink.com/v1/search?q=bank&limit=500&offset=100"{
"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_idascending, thendateascending (oldest first)./v1/fundamentals:entity_idascending, thenperiod_endascending, then the date the row became known ascending, thenperiod_type(annualbeforequarterly), then the row's own filing. Underview=as_reported, the filings of one period appear oldest first./v1/metrics:entity_idascending, thenperiod_endascending, then the statement behind the row./v1/insiders:transaction_datedescending (newest first), thendocument_iddescending, derivative lines before non-derivative lines, then line sequence descending./v1/holdings:report_datedescending, thensharesdescending (largest position first), thenmanager_iddescending, then the position's own keys./v1/managers/{manager_id}/holdings:report_datedescending, thenvalue_usddescending, then the position's own keys./v1/managers:manager_nameignoring letter case, thenmanager_id./v1/search: withq, by match strength (exact ticker, then ticker prefix, then name prefix, then any other match); withoutq, by name, thenentity_id./v1/index/{index_name}:entity_nameascending, then each member's own key./v1/index/{index_name}/events:effective_dateascending,addedbeforeremovedon the same date, thenentity_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.
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"])page 1: 100 rows, next_cursor=djJ8ZXlKeUlqb2ljSEpwWTJWeklpd2ljU0k2SWxwNVdqWXRSelE1WTJoS1NtMXBUMEZXTURKMlJXY2lMQ0p3SWpveUxDSnJJam9pVFZSQmQwMUVUVEpOZWxZNFRXcEJlVTVwTUhkT1V6QjVUak4zZVNKOXw1aHprVkZyWm5jY2JlMUJZdTVmRWNB
page 2: 84 rows, next_cursor=None
184 rows from 2026-01-02 to 2026-09-25The 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.
#!/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))
donepage 1: next_cursor=djJ8ZXlKeUlqb2ljSEpwWTJWeklpd2ljU0k2SWxwNVdqWXRSelE1WTJoS1NtMXBUMEZXTURKMlJXY2lMQ0p3SWpveUxDSnJJam9pVFZSQmQwMUVUVEpOZWxZNFRXcEJlVTVwTUhkT1V6QjVUak4zZVNKOXw1aHprVkZyWm5jY2JlMUJZdTVmRWNB
page 2: next_cursor=nonepage_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.
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())184 2026-01-02 2026-09-25The 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
| status | error code | when it happens | how to fix |
|---|---|---|---|
| 400 | bad_request | cursor 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. |
| 400 | pagination_limit_reached | The 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. |
| 400 | bad_request | limit 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. |
| 400 | bad_request | limit 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?
Thank you for helping improve xfinlink documentation.
- Need help? Get in touch.
- Building with an LLM? Read llms.txt.
- Using Claude or ChatGPT? Connect the MCP server.