Why does one endpoint return four different objects?
You write a function called get_fundamentals(ticker). It works beautifully on Apple. You point it at an S&P 500 ticker and it throws a KeyError on Highlights. Nothing is broken. You have just met the single most important fact about /fundamentals/{ticker}: it is not one endpoint returning one schema. It is four schemas behind one path, and the response tells you which one you got.
The field that decides everything
Every response carries General.Type. The four values you will meet in practice, and what each one brings with it:
"Common Stock"— the big one.General,Highlights,Valuation,SharesStats,Technicals,SplitsDividends,AnalystRatings,Holders,InsiderTransactions,ESGScores,outstandingShares,Earnings, andFinancials."ETF"—General,Technicals, and anETF_Datablock. No income statement, because this endpoint does not model a fund as an operating company."FUND"— a mutual fund.GeneralplusMutualFund_Data."INDEX"—General, and never any statements. Whether anything else sits beside it is an entitlement question. The next lesson is entirely about this case, because getting it wrong is a classic.
Read General.Type before you read anything else. That one branch removes most of the surprises this endpoint has to offer.
What that looks like on real tickers
AAPL.US returns Type: "Common Stock". Its General block alone carries CIK: "0000320193", ISIN: "US0378331005", FiscalYearEnd: "September", IPODate: "1980-12-12", an officers list, cross-listings on other exchanges, and UpdatedAt. Its Highlights carries MarketCapitalization, PERatio, EarningsShare, RevenueTTM, MostRecentQuarter.
SPY.US returns Type: "ETF". Instead of statements you get ETF_Data: Company_Name ("State Street Investment Management"), Index_Name ("S&P 500 Index"), Inception_Date ("1993-01-22"), NetExpenseRatio, TotalAssets, Asset_Allocation, Sector_Weights, World_Regions, Top_10_Holdings, Holdings, and a Performance block.
SWPPX.US returns Type: "FUND", with Fund_Family: "Schwab Funds" and Fund_Category: "Large Blend" sitting in General.
Two things worth knowing before you trust a number
Units are not consistent between shapes. On AAPL.US, Highlights.DividendYield came back as 0.0031 on 2026-07-28 — a fraction, meaning 0.31%. On SPY.US, ETF_Data.Yield came back the same day as the string "1.010000" — a percentage, meaning 1.01%. Same concept, different scale, different type, one endpoint. In the same Apple response, SharesStats.PercentInstitutions was 66.499, a percentage number. All three are snapshots and all three had moved by 2026-09-15; the scales had not. Check the scale of every ratio you use rather than assuming.
Holdings lists are a top-N slice. SPY's Holdings block came back with 50 entries and Holdings_Count: 50, for a fund that tracks roughly 500 companies. It is a leaderboard, not the basket. One of those 50 entries also resolved to a German listing rather than the US line, so if you join holdings to prices by symbol, verify the mapping rather than assuming it.
Inside ETF_Data: two conventions in one block
Read on 29 September 2026, SPY's block held these, exactly as sent: NetExpenseRatio "0.00095", AnnualHoldingsTurnover "0.02000", Yield "0.980000", Performance.Returns_1Y "18.48", and 8.14511 as the first Assets_% in Top_10_Holdings. The first two are fractions: a fee of 0.095% a year and 2% of the portfolio replaced. The other three are percentages. Nothing in the block marks the boundary, so a fee read on the same scale as its neighbours comes out a hundred times too large.
Four more things the block does quietly:
- The Sharpe ratio is spelled
3y_SharpRatio, without the e. Code that asks forSharpegets nothing. Holdings_Countcounts what the API carries, not what the fund owns: 50 for SPY, VTI and QQQ, and 1,976 for IWM, whose full basket is in the response.Index_Nameisnullfor IWM, VTI and QQQ, so the benchmark has to come from the fund's name or its own documents.Sector_Weightshas its own vocabulary. The fund's key isConsumer Cyclicals; a stock'sGeneral.Sectorfor the same kind of company readsConsumer Cyclical, andGicSectoris a third list again. Joining a fund's weights to its holdings' sectors is a mapping you write by hand.
Try it now
- Here are
/fundamentals/AAPL.US,/fundamentals/SPY.USand/fundamentals/GSPC.INDX, each filtered down toGeneral::Typeand one more field. ReadGeneral::Typefrom each. Three calls, three different words, three different response shapes.
Write the branch before you write the parser, with one arm per type:
ETFreadsETF_Data,FUNDreadsMutualFund_Data,Common StockreadsFinancials, andINDEXreadsGeneraland stops. A catch-allelsethat reaches forFinancialsis the exact failure the next lesson is about.The first two tables carry
Highlights.DividendYieldfor a dividend-paying stock andETF_Data.Yieldfor an ETF. Write both down with their units. That is the habit that stops a 100x error.Here are the scales, the identity fields and the sector keys, for SPY, for IWM, and for Amazon as a single stock. Rewrite every number in the first table as a percentage, and mark the two you had to multiply by 100. Then write the mapping entry that joins the fund's sector key to Amazon's, and say which of Amazon's two sector fields you mapped.