Rate limits
Read your remaining quota from response headers, recognise each 429 response and retry at the right time.
Requests made with a key count against your account's quotas. This page covers the quota numbers, how requests and rows are counted, the headers that report your usage, each 429 response and how to retry. Plans and limits compares everything else the plans include.
Quotas per plan
- Requests per UTC day: 100 on Free, 10,000 on Pro, 50,000 on Max and 500,000 on Redistribution.
- Requests per UTC clock hour: 40 on Free. Pro, Max and Redistribution have no hourly limit.
- Rows per UTC clock hour: 100,000 on Free, 1,000,000 on Pro, 5,000,000 on Max and 50,000,000 on Redistribution.
- Rows per UTC day: 500,000 on Free, 5,000,000 on Pro, 25,000,000 on Max and 250,000,000 on Redistribution.
- Distinct index
as_ofdates per UTC day: 51 on Free. Pro, Max and Redistribution have no limit. - Distinct entities per UTC day: counted on every plan. On Free, responses slow down after 500 distinct entities, by up to 30 seconds.
- Active API keys per account: 1 on Free, 1 on Pro, 5 on Max and 10 on Redistribution.
Responses slow down as an account approaches its daily allowance.
Without a key, only /v1/status, /v1/resolve and /v1/search answer. Keyless resolve and search requests are limited per IP address to 40 requests per clock hour and 100 per UTC day, the same numbers as Free, and report them in the same headers. The MCP server needs a key. /v1/status responses carry no rate-limit headers.
HTTP/2 200
cache-control: no-store
x-ratelimit-limit-day: 100
x-ratelimit-limit-hour: 40
x-ratelimit-remaining-day: 92
x-ratelimit-remaining-hour: 32That is the response to curl "https://api.xfinlink.com/v1/resolve/AAPL" with no key. Its remaining counts include every keyless request sent from the same IP address.
How requests are counted
Each ticker or entity_id in a request counts as one request. /v1/prices/AAPL,MSFT counts as two. Requests without a ticker, such as index rosters, index events, search, manager search and manager holdings, count as one.
Each page of a cursor-paged result is a request of its own and counts the same way. A page of a 100-ticker prices request counts 100, so a result that runs to three pages counts 300. A page holds up to 5,000 rows, or up to 100 on /v1/metrics, so a request sized to fit one page counts once per ticker. Pagination lists the page size of every endpoint. The recipe Pull many tickers within your limits on Recipes sizes its requests this way.
A request is accepted or refused whole. With 50 requests left in the UTC day, a request for 100 tickers returns the daily-limit 429 and uses none of the 50.
A request counts when it is answered: a 200, also when served from cache, or a 404 or 409 error, one request per ticker sent. A 5xx response to a request that reached the quota check also counts. A refused request does not count and carries no quota headers: any 400, such as an invalid parameter, too many tickers or a page past your plan's page limit, the 402 for an endpoint your plan does not include, a row-allowance 429 and the request-limit 429 responses. On Free, a request with two tickers is refused with 400 and uses none of the day's requests:
curl -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/prices/AAPL?fields=close&limit=1"
curl -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/prices/AAPL,MSFT"
curl -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/managers?search=berkshire"
curl -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/prices/XQZQX"The first request returned 200:
HTTP/2 200
x-ratelimit-limit-day: 100
x-ratelimit-limit-hour: 40
x-ratelimit-remaining-day: 82
x-ratelimit-remaining-hour: 39The two-ticker price request returned 400 and the manager search returned 402, each without quota headers:
HTTP/2 400
content-type: application/jsonThe unknown ticker returned 404 and used one request, the first counted since the 200:
HTTP/2 404
x-ratelimit-limit-day: 100
x-ratelimit-limit-hour: 40
x-ratelimit-remaining-day: 81
x-ratelimit-remaining-hour: 38These responses also do not count and carry no quota headers: 401, 403, 405, the 404 for an unknown path, the index as_of 429 and the Temporarily blocked 429.
The daily count resets at 00:00 UTC. The hourly count resets at the start of each clock hour in UTC. It is a fixed hour, not a rolling 60 minutes: a Free key refused at 10:50:15 UTC was told to wait 585 seconds, until 11:00:00.
All keys on one account share the account's counts: requests, rows, distinct entities and index as_of dates are counted per account, not per key.
Usage on the dashboard
Today's usage on the dashboard is the count your daily quota is checked against, across every key on the account, so a request for AAPL,MSFT adds two. xfl.usage() and GET /v1/auth/usage return the same count as requests_today, with daily_limit and resets_at. The seven-day chart and the endpoint table on the dashboard count API calls, one per call whatever the number of tickers, so the same request adds one there.
Rows
Rows are the entries in the data array of a 200 response, and on resolve the entities across all tickers. A response served from cache does not add rows. When your rows for the clock hour or the UTC day pass your plan's allowance, later requests in that hour or day return 429.
Ask for fewer rows with start, end, fields and limit. Each 200 response built for your request reports your running total in X-Rows-Today.
Index snapshots
On Free, index roster requests with as_of also count distinct dates. On Free, roster requests with as_of slow down after 20 distinct dates, by up to 10 seconds. The request that brings the day to 51 distinct dates is answered after 30 seconds; after it, every roster request with as_of returns 429 for the rest of that UTC day, including dates already requested. Roster requests without as_of, which return today's members, are not affected, and neither are index events.
curl -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/index/sp500?as_of=2001-01-31&limit=1"{"error":"asof_limit_exceeded","code":"asof_limit_exceeded","status":429,"detail":"Historical index query limit reached. Contact hello@xfinlink.com for extended access."}This body has no retry_after_seconds. The allowance starts again at 00:00 UTC. Paid plans have no as_of allowance, on the REST API and on the MCP get_index tool alike.
Response headers
X-RateLimit-Limit-Day, for example100: your plan's daily request quota. Sent on every response counted against your quota:200responses, including ones served from cache, and404and409errors, with a key or on keylessresolveandsearchrequests. Absent on400,402and429responses,401,403,405, the unknown-path404,5xxresponses and/v1/status.X-RateLimit-Remaining-Day, for example97: the requests left today, never below0. Sent and absent asX-RateLimit-Limit-Day.X-RateLimit-Limit-Hour, for example40: the hourly request quota. Sent on the same responses asX-RateLimit-Limit-Day, for Free keys and keyless requests only. Absent on Pro and higher plans.X-RateLimit-Remaining-Hour, for example37: the requests left in the current clock hour. Sent and absent asX-RateLimit-Limit-Hour.X-Data-Floor, for example2025-09-27: the earliest date Free can read, one year before today in UTC. Sent to Free keys on200responses from prices, fundamentals, metrics and index events, including ones served from cache. Absent on paid plans, on error responses and on other endpoints such as index rosters.X-Rows-Today, for example30: your rows counted today, including this response. Sent on200responses built for a keyed request. Absent on responses served from cache, on errors and on keyless requests.X-Entities-Today, for example3: the distinct entities in your responses today, including this one. Sent and absent asX-Rows-Today.X-Xfinlink-Client-Latest: the client version the API recommends, which the update message names. Sent when theUser-Agentnames an xfinlink Python client older than that version, on200data responses, including cached ones, on400,402,404and409errors from data endpoints, on the hourly, daily and row-allowance429responses, and on/v1/statusand/v1/auth/usage. Absent for other clients and user agents, and on every other response.X-Xfinlink-Client-Update,true: sent and absent asX-Xfinlink-Client-Latest.X-Xfinlink-Client-Update-Message: a message of the formxfinlink: Python client <version> is outdated. Run 'pip install -U xfinlink' to upgrade to <version>.Sent and absent asX-Xfinlink-Client-Latest.Warning:299 xfinlinkfollowed by the same message in quotes. Sent and absent asX-Xfinlink-Client-Latest.
The API sends no Retry-After header, including on 429 responses. The wait is in the retry_after_seconds body field.
Browsers can read all of these headers from JavaScript: every response lists them in Access-Control-Expose-Headers.
Free
A Free request for past prices, not served from cache:
curl -s -D - -o /dev/null -H "X-API-Key: YOUR_API_KEY" \
"https://api.xfinlink.com/v1/prices/MSFT?start=2026-02-02&end=2026-02-06&fields=close"HTTP/2 200
content-type: application/json
cache-control: public, max-age=86400
x-data-floor: 2025-09-27
x-entities-today: 2
...
x-ratelimit-limit-day: 100
x-ratelimit-limit-hour: 40
x-ratelimit-remaining-day: 98
x-ratelimit-remaining-hour: 38
x-rows-today: 10
...Paid plans
Paid plans have no hourly headers and no X-Data-Floor. X-RateLimit-Limit-Day carries the plan's daily limit. The nocache parameter in these requests is not an API parameter: the API ignores it, and a value not sent before makes a new URL, so the response is built for the request rather than served from cache.
curl -s -D - -o /dev/null -H "X-API-Key: YOUR_API_KEY" \
"https://api.xfinlink.com/v1/prices/MSFT?start=2026-03-09&end=2026-03-11&fields=close&nocache=1790512345"
curl -s -D - -o /dev/null -H "X-API-Key: YOUR_API_KEY" \
"https://api.xfinlink.com/v1/prices/GM?start=2026-03-09&end=2026-03-11&fields=close&nocache=1790512345"The first request, with a Pro key:
HTTP/2 200
content-type: application/json
cache-control: public, max-age=86400
x-entities-today: 6266
...
x-ratelimit-limit-day: 10000
x-ratelimit-remaining-day: 5573
x-rows-today: 188357
...The second request, with a Redistribution key:
HTTP/2 200
content-type: application/json
cache-control: public, max-age=86400
x-entities-today: 18
...
x-ratelimit-limit-day: 500000
x-ratelimit-remaining-day: 489647
x-rows-today: 55
...Max responses carry x-ratelimit-limit-day: 50000.
Served from cache
Sending the Pro request again returned it from cache. A cached response carries an Age header, still counts one request, and has no X-Rows-Today or X-Entities-Today:
HTTP/2 200
content-type: application/json
accept-ranges: bytes
age: 174
cache-control: public, max-age=86400
last-modified: Sun, 27 Sep 2026 12:47:26 GMT
x-ratelimit-limit-day: 10000
x-ratelimit-remaining-day: 5571
...On Free, the rows in a cached response are not added to X-Rows-Today. A Free account at 10 rows received a cached 5-row response, then a 20-row response that reported x-rows-today: 30.
Client update headers
When the User-Agent names an xfinlink Python client older than the version the API recommends, responses also carry the update headers. The message reads xfinlink: Python client <version> is outdated. Run 'pip install -U xfinlink' to upgrade to <version>., naming the client's version and the release to install. This is the cached Free response above, requested with User-Agent: xfinlink-python/0.12.0:
HTTP/2 200
age: 7
cache-control: public, max-age=86400
warning: 299 xfinlink "xfinlink: Python client 0.12.0 is outdated. Run 'pip install -U xfinlink' to upgrade to ..."
x-data-floor: 2025-09-27
x-ratelimit-limit-day: 100
x-ratelimit-limit-hour: 40
x-ratelimit-remaining-day: 97
x-ratelimit-remaining-hour: 37
x-xfinlink-client-latest: ...
x-xfinlink-client-update: true
x-xfinlink-client-update-message: xfinlink: Python client 0.12.0 is outdated. Run 'pip install -U xfinlink' to upgrade to ...
...The Python client shows the message once per process as a warning. Requests from xfinlink-python/0.13.1 carry none of these headers.
429 responses
Each quota has its own 429 body. None carries quota headers or Retry-After.
Hourly request limit
Free keys and keyless requests allow 40 requests per clock hour. The 41st in the hour is refused until the hour ends:
curl -s -D - -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/prices/AAPL"HTTP/2 429
content-type: application/json
...{"error":"Burst limit reached (40 requests/hour). Try again shortly.","code":"burst_limit_reached","status":429,"detail":"Burst limit reached (40 requests/hour). Try again shortly.","message":"Burst limit reached (40 requests/hour). Try again shortly.","retry_after_seconds":585}retry_after_seconds is the number of seconds until the next clock hour in UTC. The request was sent at 10:50:15 UTC.
Daily request limit
When your account has used its requests for the UTC day:
curl -s -D - -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/prices/AAPL"HTTP/2 429
content-type: application/json
...{"error":"Daily limit reached (100 requests/day). Resets at midnight UTC.","code":"daily_limit_reached","status":429,"detail":"Daily limit reached (100 requests/day). Resets at midnight UTC.","message":"Daily limit reached (100 requests/day). Resets at midnight UTC.","retry_after_seconds":43181}retry_after_seconds is the number of seconds until 00:00 UTC: the request was sent at 12:00:19 UTC, 43,181 seconds before midnight. The number in error is your plan's daily limit, written with thousands separators on paid plans, for example 10,000.
Keyless requests
Keyless resolve and search requests count against 40 per clock hour and 100 per UTC day for each IP address, and return the hourly and daily bodies above when those run out. Keyless requests that continue from the same IP address after the hourly limit can also receive this body, with retry_after_seconds set to the seconds until the next clock hour in UTC:
{"error":"Rate limit exceeded","code":"rate_limit_exceeded","status":429,"detail":"Rate limit exceeded. Sign up for an API key at https://xfinlink.com/signup","message":"Rate limit exceeded. Sign up for an API key at https://xfinlink.com/signup","retry_after_seconds":...}Row allowances
Row allowances return bodies with an error code and no retry_after_seconds. The hourly one lifts at the start of the next clock hour in UTC, the daily one at 00:00 UTC.
The rows-per-hour allowance returns:
{"error":"hourly_usage_limit","code":"hourly_usage_limit","status":429,"detail":"Hourly usage limit reached. Try again shortly."}The rows-per-day allowance returns:
{"error":"row_limit_exceeded","code":"row_limit_exceeded","status":429,"detail":"Daily data volume limit reached. Resets at midnight UTC."}Index snapshot allowance
asof_limit_exceeded is shown under Index snapshots in How requests are counted. It lifts at 00:00 UTC.
Retry guidance
- On a
429withretry_after_seconds, wait that many seconds, then retry. - On
hourly_usage_limit, wait until the next clock hour in UTC. Onrow_limit_exceededandasof_limit_exceeded, wait until 00:00 UTC. - On
500,502,503and504, retry a few times with growing waits. For504, narrow the request first. - Do not retry other
4xxerrors. The same request fails the same way. See Errors.
This helper applies those rules and gives up when the wait is longer than an hour:
import time
from datetime import datetime, timezone
import httpx
KEY = "YOUR_API_KEY"
MAX_WAIT = 3600
def wait_seconds(body):
if "retry_after_seconds" in body:
return body["retry_after_seconds"]
now = datetime.now(timezone.utc)
elapsed_hour = now.minute * 60 + now.second
if body.get("error") == "hourly_usage_limit":
return 3600 - elapsed_hour
return 86400 - now.hour * 3600 - elapsed_hour
def get_json(url, params=None, attempts=4):
for attempt in range(attempts):
r = httpx.get(url, params=params, headers={"X-API-Key": KEY}, timeout=120)
if r.status_code == 429:
body = r.json()
wait = wait_seconds(body)
print(f"429 {body['error']} retry in {wait} s")
if wait > MAX_WAIT:
raise RuntimeError(f"quota exhausted; retry in {wait} s")
time.sleep(wait)
continue
if r.status_code in (500, 502, 503, 504):
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return r.json()
raise RuntimeError(f"no success after {attempts} attempts")
body = get_json("https://api.xfinlink.com/v1/prices/AAPL", {"start": "2026-09-21", "end": "2026-09-25", "fields": "close"})
print(datetime.now(timezone.utc).strftime("%H:%M:%S"), len(body["data"]), "rows")Run with a Free key that had used its 40 requests for the hour, it waited for the next hour and then succeeded:
429 Burst limit reached (40 requests/hour). Try again shortly. retry in 3152 s
12:00:05 5 rowsRun again after the account had used its 100 requests for the day, it stopped:
429 Daily limit reached (100 requests/day). Resets at midnight UTC. retry in 43175 s
Traceback (most recent call last):
File "retry.py", line 39, in <module>
body = get_json("https://api.xfinlink.com/v1/prices/AAPL", {"start": "2026-09-21", "end": "2026-09-25", "fields": "close"})
File "retry.py", line 28, in get_json
raise RuntimeError(f"quota exhausted; retry in {wait} s")
RuntimeError: quota exhausted; retry in 43175 sThe Python client does not retry 429. It raises XfinlinkError with the wait in the message:
xfinlink: Rate limit reached (Burst limit reached (40 requests/hour). Try again shortly.). Fix: wait 578s, or reduce requests by adding start/end date filters and using max_rows parameter.The client retries 502, 503, 504 and network timeouts itself, up to three attempts. See Python client reference.
Using less quota
- Watch
X-RateLimit-Remaining-DayandX-RateLimit-Remaining-Hourand slow down before they reach0. - A request with several tickers saves round trips but counts each ticker, so list only the tickers you need.
- Size a multi-ticker request to fit one page, up to 5,000 rows or up to 100 on
/v1/metrics. Every further page counts each ticker again. - Narrow
start,endandfieldsto cut rows. Rows count toward the row allowances, not the request quota. - Store what you download and fetch again only when
/v1/statusshows newer data. The recipe Refresh only what changed shows how.
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.