How do you pull fundamentals for a whole exchange without melting the API?
You want to screen every listed company on an exchange by price-to-earnings. Calling /fundamentals/{ticker} once per symbol works for ten tickers, is tedious for a thousand, and is a bad idea for a whole market. /bulk-fundamentals/{EXCHANGE} exists for exactly this, and it comes with constraints you need to design around rather than discover.
The endpoint
/bulk-fundamentals/{EXCHANGE} takes an exchange code in the path (US, LSE, TO) and these query parameters:
symbols— comma-separated list, to restrict to specific tickers.offset— records to skip, default0.limit— records to return, maximum 500, default 500.version— response format.fmt—json.
There is a parallel /v1.1/bulk-fundamentals/{EXCHANGE} on the newer schema, where fmt supports json only — no CSV. If a pipeline of yours parses CSV, that version will break it.
The response is not an array. It is an object keyed by position as a string, "0", "1", "2" and on, and every page starts again at "0" (checked 28 September 2026). So pages do not concatenate, and merging two pages as objects silently overwrites the first with the second. Read the symbol from General.Code inside each record, never from the key.
Each record is a trimmed fundamentals object, and trimmed is not the same as shortened. Live it carries General, Highlights, Valuation, Technicals, SplitsDividends, Earnings and Financials — so the statements are there, contrary to what the endpoint's reputation suggests. What is missing is the history: Financials::Balance_Sheet holds quarterly_last_0 to quarterly_last_3 and yearly_last_0 to yearly_last_3, four quarters and four years and nothing older, and Earnings holds Last_0 through Last_3.
Those key names exist nowhere else in the API. A parser written against Financials::Balance_Sheet::quarterly — the shape Unit 1 taught you four lessons ago — reads nothing here and raises nothing either. Check the keys before you map them.
So the rule survives, for a better reason than "no statements": four quarters and four years is enough to rank a universe and not enough to analyse one. Bulk is for narrowing.
The arithmetic of a full sweep
Say the exchange returns 40,000 listed lines. At limit=500, that is:
40,000 ÷ 500 = 80 requests.
Against 40,000 individual /fundamentals calls, that is 500 times fewer requests. That ratio is the entire argument for the endpoint.
It is also why the limit exists. A single request that tried to serialise 40,000 fundamentals objects would be enormous and slow, and the documented failure mode is a 504 gateway timeout — the request dies after the server has already done the work. Pushing limit to its maximum is not free; if you are seeing timeouts, the fix is a smaller page, not a retry loop.
Two more things to plan for. Access to bulk fundamentals sits behind the Extended Fundamentals entitlement, so a 401 or 403 here may be a plan boundary rather than a bad key. And a failed bulk request generally still costs quota — the arithmetic above is a best case, not a guarantee.
The pagination pattern that survives contact
Naively: loop offset from 0 in steps of 500 until a page comes back short. Two refinements decide whether the job finishes unattended.
Checkpoint every page. Write each page to disk keyed by its offset, and make the loop resumable from the last completed offset. Eighty requests is enough that one of them will fail eventually, and re-running from zero wastes the whole quota.
Do not assume the universe is stable across pages. Eighty sequential requests take time. If the underlying list changes between page 3 and page 60, a symbol can appear twice or be skipped entirely. De-duplicate on General.Code at the end rather than trusting that the pages tile perfectly.
What you have when you are done
A snapshot, on one date, of a whole exchange's headline metrics. That is genuinely powerful — it is how you go from "every listed company" to "the 200 that meet my criteria" without paying for 40,000 detailed calls.
Three honest limits on it, though:
- It is a snapshot.
HighlightsandValuationare current values with no history. Re-running next month gives you a different snapshot, not a series, unless you store each run with its date. - The universe as returned is the current universe. Companies that delisted are not in it. Building a historical study out of repeated present-day sweeps reintroduces exactly the survivorship problem the Markets Foundations material warns about.
- Ranking is not analysis. A
PERatiocolumn sorts a list. What each of those ratios means for the specific business requires the long yearly series and the full statement history that bulk trims away.
Try it now
- Here is
/bulk-fundamentals/US?limit=1: theGeneralblock of its one record, read through the key"0". The sections every record carried on 28 September 2026 are the seven named above. List which of the thirteen from Unit 1 are missing.
- Write the paging loop with a checkpoint file. Run it for three pages, kill it, and restart it. If it resumes rather than restarting, it is ready for eighty.
- Here are two pages of
/bulk-fundamentals/USatlimit=100, pulled on 28 September 2026. Together they held 200 records with 200 distinctGeneral.Codevalues.
offset |
status | keys | bytes |
|---|---|---|---|
| 0 | 200 | "0" to "99" |
2,067,749 |
| 100 | 200 | "0" to "99" |
2,115,236 |
Say what a naive merge of the two objects would have kept, and why the de-duplication step still belongs in the loop even though nothing appeared twice here.