Contents Lesson 1 of 17

5 min read · foundations

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, and Financials.
  • "ETF" — General, Technicals, and an ETF_Data block. No income statement, because this endpoint does not model a fund as an operating company.
  • "FUND" — a mutual fund. General plus MutualFund_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 for Sharpe gets nothing.
  • Holdings_Count counts 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_Name is null for IWM, VTI and QQQ, so the benchmark has to come from the fund's name or its own documents.
  • Sector_Weights has its own vocabulary. The fund's key is Consumer Cyclicals; a stock's General.Sector for the same kind of company reads Consumer Cyclical, and GicSector is a third list again. Joining a fund's weights to its holdings' sectors is a mapping you write by hand.

Try it now

  1. Here are /fundamentals/AAPL.US, /fundamentals/SPY.US and /fundamentals/GSPC.INDX, each filtered down to General::Type and one more field. Read General::Type from each. Three calls, three different words, three different response shapes.
Live API response: mda12 apple type and dividend yield
Live API response: mda12 spy type and yield
Live API response: sp500 index general
  1. Write the branch before you write the parser, with one arm per type: ETF reads ETF_Data, FUND reads MutualFund_Data, Common Stock reads Financials, and INDEX reads General and stops. A catch-all else that reaches for Financials is the exact failure the next lesson is about.

  2. The first two tables carry Highlights.DividendYield for a dividend-paying stock and ETF_Data.Yield for an ETF. Write both down with their units. That is the habit that stops a 100x error.

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

Live API response: mda4 spy etf data scales
Live API response: mda4 iwm etf data identity
Live API response: mda4 spy sector weight key
Live API response: mda4 amzn two taxonomies