Get startedDashboard

Rate limits

Read your remaining quota from response headers, recognise each 429 response and retry at the right time.

View llms.txt

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_of dates 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.

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: 32

That 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:

Terminal
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:

headers
HTTP/2 200
x-ratelimit-limit-day: 100
x-ratelimit-limit-hour: 40
x-ratelimit-remaining-day: 82
x-ratelimit-remaining-hour: 39

The two-ticker price request returned 400 and the manager search returned 402, each without quota headers:

headers
HTTP/2 400
content-type: application/json

The unknown ticker returned 404 and used one request, the first counted since the 200:

headers
HTTP/2 404
x-ratelimit-limit-day: 100
x-ratelimit-limit-hour: 40
x-ratelimit-remaining-day: 81
x-ratelimit-remaining-hour: 38

These 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.

Terminal
curl -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/index/sp500?as_of=2001-01-31&limit=1"
response.json
{"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 example 100: your plan's daily request quota. Sent on every response counted against your quota: 200 responses, including ones served from cache, and 404 and 409 errors, with a key or on keyless resolve and search requests. Absent on 400, 402 and 429 responses, 401, 403, 405, the unknown-path 404, 5xx responses and /v1/status.
  • X-RateLimit-Remaining-Day, for example 97: the requests left today, never below 0. Sent and absent as X-RateLimit-Limit-Day.
  • X-RateLimit-Limit-Hour, for example 40: the hourly request quota. Sent on the same responses as X-RateLimit-Limit-Day, for Free keys and keyless requests only. Absent on Pro and higher plans.
  • X-RateLimit-Remaining-Hour, for example 37: the requests left in the current clock hour. Sent and absent as X-RateLimit-Limit-Hour.
  • X-Data-Floor, for example 2025-09-27: the earliest date Free can read, one year before today in UTC. Sent to Free keys on 200 responses 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 example 30: your rows counted today, including this response. Sent on 200 responses built for a keyed request. Absent on responses served from cache, on errors and on keyless requests.
  • X-Entities-Today, for example 3: the distinct entities in your responses today, including this one. Sent and absent as X-Rows-Today.
  • X-Xfinlink-Client-Latest: the client version the API recommends, which the update message names. Sent when the User-Agent names an xfinlink Python client older than that version, on 200 data responses, including cached ones, on 400, 402, 404 and 409 errors from data endpoints, on the hourly, daily and row-allowance 429 responses, and on /v1/status and /v1/auth/usage. Absent for other clients and user agents, and on every other response.
  • X-Xfinlink-Client-Update, true: sent and absent as X-Xfinlink-Client-Latest.
  • X-Xfinlink-Client-Update-Message: a message of the form xfinlink: Python client <version> is outdated. Run 'pip install -U xfinlink' to upgrade to <version>. Sent and absent as X-Xfinlink-Client-Latest.
  • Warning: 299 xfinlink followed by the same message in quotes. Sent and absent as X-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:

Terminal
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"
headers
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 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.

Terminal
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:

headers
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:

headers
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:

headers
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:

headers
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:

Terminal
curl -s -D - -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/prices/AAPL"
headers
HTTP/2 429
content-type: application/json
...
response.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:

Terminal
curl -s -D - -H "X-API-Key: YOUR_API_KEY" "https://api.xfinlink.com/v1/prices/AAPL"
headers
HTTP/2 429
content-type: application/json
...
response.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:

response.json
{"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:

response.json
{"error":"hourly_usage_limit","code":"hourly_usage_limit","status":429,"detail":"Hourly usage limit reached. Try again shortly."}

The rows-per-day allowance returns:

response.json
{"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 429 with retry_after_seconds, wait that many seconds, then retry.
  • On hourly_usage_limit, wait until the next clock hour in UTC. On row_limit_exceeded and asof_limit_exceeded, wait until 00:00 UTC.
  • On 500, 502, 503 and 504, retry a few times with growing waits. For 504, narrow the request first.
  • Do not retry other 4xx errors. 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:

retry.py
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:

stdout
429 Burst limit reached (40 requests/hour). Try again shortly. retry in 3152 s
12:00:05 5 rows

Run again after the account had used its 100 requests for the day, it stopped:

output
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 s

The Python client does not retry 429. It raises XfinlinkError with the wait in the message:

stdout
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-Day and X-RateLimit-Remaining-Hour and slow down before they reach 0.
  • 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, end and fields to cut rows. Rows count toward the row allowances, not the request quota.
  • Store what you download and fetch again only when /v1/status shows newer data. The recipe Refresh only what changed shows how.

Was this page helpful?