Best ETF Holdings APIs 2026: N-PORT vs Daily
Best ETF holdings API compared by source: SEC N-PORT vs daily issuer files, full holdings vs top 10, CUSIP/ISIN identifiers, history and pricing.

Search for the best ETF holdings API and you get vendor product pages, a marketplace list and a few scraping tutorials. Each page tells you what fields come back. Almost none tell you where the rows come from, and that is the question that decides whether a holdings dataset fits your use. An ETF's portfolio is published twice: once to the SEC on Form N-PORT, and once every business day on the issuer's own website. The two disagree in ways that break naive code, and a vendor feed may be built on either one, or on neither.
This comparison sorts ETF holdings APIs by provenance first, then by freshness, completeness, identifiers, history and access. We build one of these APIs, StockFit, so we hold our own rows to the same rule as everyone else's: every vendor claim below is quoted from that vendor's documentation as fetched on 2026-10-30, and every StockFit number comes from a live response on the same day, with the SEC filing it traces to. If you only want the grid, jump to the ETF holdings API comparison.
What an ETF holdings API should return
Six questions separate one ETF holdings data API from another. Ask them before you compare prices.
- Source. Is each row from a regulatory filing, from the issuer's daily file, or from a vendor's own aggregation? A filing gives you a document to check the number against. An issuer file gives you today's book. An unnamed source gives you neither.
- Lag. What date is the snapshot, and how long after that date could you first read it? A response should carry an explicit as-of date, not leave you to infer it from the fetch time.
- Full holdings vs top 10. Does the response list every line, including cash, money market funds and securities-lending collateral, or only the largest positions? For a concentrated fund the top 10 can be half the portfolio, which means the other half is invisible.
- Identifiers. Which of CUSIP, ISIN, SEDOL, FIGI and ticker come back per holding, and is the holding mapped to the issuer's SEC CIK? The identifier decides how much join code you write.
- History. Can you ask for the holdings as of a past date, and how far back? Backtests and turnover studies need point-in-time snapshots, not today's list applied to the past.
- Access. Which plan unlocks holdings, and can you check coverage before you pay?
ETF holdings data sources: N-PORT vs daily issuer files
Form N-PORT: the filing
Registered funds report their portfolios to the SEC on Form N-PORT. In the SEC's words on its N-PORT data sets page, it is “the reporting form used for monthly reports of registered management funds other than money market funds, and exchange-traded funds organized as unit investment trusts.” The form is standardized: every holding carries an asset category, an issuer category, a fair-value level, and a CUSIP or ISIN where one exists, plus a flag for cash collateral from securities lending. Today the holdings become public quarterly, about 60 days after the quarter ends. Amplify Cybersecurity ETF (HACK) is a typical case: its N-PORT for the period ending 2026-06-30 was filed on 2026-08-28, 59 days later, and on 2026-10-30 it is still the latest public filing for the fund.
Rule 6c-11 daily files: the issuer's website
The ETF Rule (Rule 6c-11) requires transparent ETFs to post their portfolio holdings on their website every business day before the market opens. The SEC's ETF small entity compliance guide lists what each holding must show: “ticker symbol; CUSIP or other identifier; description of holding; quantity of each security or other asset; and percentage weight of the holding in the portfolio”. Note the words “CUSIP or other identifier”. The rule does not fix the identifier, the file format or the column names, so each issuer picks its own. The daily file is current to the prior close, but it arrives in whatever shape the issuer chose. Our daily ETF holdings walkthrough covers the six file formats we parse to normalize them.
Vendor feeds: a third party in between
Most commercial ETF holdings APIs sit on top of one of those two sources, or on a data partner that does. Some say which. ETF Global is named on two of the products below. Others describe their source in general terms or not at all. That is not a defect in itself, but it means you cannot tell from the docs whether a row reflects a quarter-old filing or yesterday's file, and that matters for every downstream number. How much an ETF's book actually moves between those two dates depends on the fund type, which we measured in how often ETF holdings update.
The best ETF holdings APIs compared
Nine products from seven providers, read on the six criteria above. We list both StockFit endpoints separately because they answer different questions. We left out Financial Modeling Prep: its documentation returned HTTP 403 to us on 2026-10-30, and we do not describe an API we could not read.
| API | Where the rows come from | Freshness | Identifiers per holding | History | Access |
|---|---|---|---|---|---|
| StockFit /api/fund/holdings | SEC Form N-PORT, one filing per response | Quarterly, public about 60 days after quarter end | CUSIP, ISIN, ticker, mapped CIK and symbol, fair-value level | Quarterly snapshots (IVV back to 2019-09-30) | Starter ($19/mo), ETF or Pro |
| StockFit /api/fund/holdings/daily | Issuer website file (Rule 6c-11) | Daily, as the issuer publishes | What the issuer publishes: ticker, CUSIP or SEDOL, currency, sector | About 45 days, accumulating since early August 2026 | ETF $59/mo or Pro; coverage list Free |
| Intrinio ETF Holdings | Not stated on the docs page | Snapshots by as_of_date | FIGI, ISIN, RIC, SEDOL, ticker | Needs "historical holdings entitlements" | Not stated on the docs page |
| Intrinio ETF Global holdings | ETF Global (the /etfg/ endpoint family) | Snapshot "on a single date" | CUSIP, ISIN, FIGI, ticker | By date parameter | Not stated on the docs page |
| Massive (ETF Global partner) | ETF Global | "Updated daily" | ISIN, FIGI, SEDOL, US_CODE | "Records date back to April 3, 2017" | $99/month per dataset (individual) |
| API Ninjas /v1/etf | Not stated for holdings | last_updated per holding | ticker, ISIN, CUSIP (or CINS) | Not described | Holdings field "Premium only" |
| EODHD Fundamentals | Not stated | Not stated | Ticker code only (Code, Name, Sector, Assets_%) | Not described | Not stated on the docs page |
| Finnworlds | Not stated | Claims "real-time and historical data" | ISIN, CUSIP, LEI | Claimed, depth not stated | Not checked |
| Tradefeeds | "reliable partners in the financial industry" | Claims "real-time and historical data" | Not stated | "current and historical ETF holdings data" | Not on the product page |
Quoted text is copied from each vendor's own documentation or product page, fetched on 2026-10-30. “Not stated” means the page we read does not say, not that the vendor lacks it.
StockFit: two labeled sources on one key
/api/fund/holdings returns the holdings from one N-PORT filing per response, sorted by weight, with CUSIP, ISIN, asset category, fair-value level and, where StockFit can match the holding to an SEC-filing company, its mappedCik and mappedSymbol. Pass reportDate for a past quarter. /api/fund/holdings/daily returns the issuer's daily file in the same JSON shape, with sedol, currency and sector when the issuer provides them, but no CIK mapping and no fair-value level. The coverage list, /api/fund/holdings/daily/supported-funds, is on the Free plan, so you can check whether your funds are covered before you subscribe. N-PORT holdings are on the Starter ($19/month, or $180/year), ETF and Pro plans; daily holdings need the ETF plan ($59/month, or $468/year, which is $39/month billed annually) or Pro.
Where StockFit is weaker: daily history is short. Snapshots have accumulated since early August 2026 and about 45 days are retained per fund, so for years of daily ETF constituents the ETF Global products below go much further back.
Intrinio: two holdings endpoints
Intrinio documents two. The ETF Holdings endpoint, /etfs/{identifier}/holdings, “Returns holdings data that details all the constituent securities in each ETF with names, identifiers, and the weights for each security”. Each holding carries share_class_figi, isin, ric and sedol, plus quantity_held and weighting. Its as_of_date parameter will “Return the most recent holdings snapshot on or before this date”, and the docs add that this “Requires historical holdings entitlements.” The second, ETF Global ETF Holdings at /etfg/etfs/{identifier}/holdings, “Returns the constituent securities held by an ETF on a single date, with weights, market values and share counts”, with CUSIP, ISIN and FIGI per holding. Neither page shows a price.
Massive: ETF Global constituents with deep history
Massive resells ETF Global data. Its constituents endpoint, GET /etf-global/v1/constituents, is described as a way to “Retrieve the underlying holdings and constituents of U.S. listed ETFs”, is “Updated daily”, and states that “Records date back to April 3, 2017”. Identifiers include ISIN, FIGI and SEDOL. The ETF Global partner page lists individual pricing at “$99/month” per dataset. If you need daily ETF constituents going back years, this is the deepest daily history in this comparison.
API Ninjas: holdings inside a broader ETF endpoint
The API Ninjas ETF API, /v1/etf, takes a ticker or an isin. Its holdings field is marked Premium only and is described as an “Array of all holdings in the ETF. Each entry contains ticker, name (security or position name), isin, cusip (CUSIP or CINS), num_shares (number of shares held), weight (portfolio weight as a decimal fraction), value (position value in the listing's trading currency), and last_updated (Unix timestamp).” The page also says that cash, futures and money-market sweeps come back with an empty ticker. It does not say where the holdings come from.
EODHD: holdings in the fundamentals response
EODHD returns ETF holdings inside its fundamentals data feed rather than as a separate endpoint: Holdings_Count, Top_10_Holdings and a full Holdings list keyed by ticker, each with Code, Name, Sector and Assets_%. The page describes no CUSIP or ISIN per holding and no historical holdings.
Finnworlds and Tradefeeds
The Finnworlds ETF holdings README shows a request to /api/v1/etfholdings by ticker and response fields including lei, isin, cusip, percent_value, asset_category, fair_value_level and cash_collateral. It says the API “provides real-time and historical data on ETF fundamentals, holding allocation as well as periodical returns” and does not name its source. Tradefeeds says its data “is sourced from reliable partners in the financial industry: investments funds, brokerages and financial advisors” and that “You can get current and historical ETF holdings data in a JSON format or excel and CSV files.” Its product page shows no endpoint or price.
One fund, two sources: HACK in the StockFit ETF holdings API
Here is the same ETF, Amplify Cybersecurity ETF (HACK, CIK 1633061), from both endpoints on 2026-10-30. First the N-PORT holdings:
curl -H "Authorization: Bearer $TOKEN" \
"https://api.stockfit.io/v1/api/fund/holdings?symbol=HACK&pageSize=30"// 25 rows; three shown, some fields omitted
{
"reportDate": "2026-06-30",
"totalResults": 25,
"data": [
{ "name": "Palo Alto Networks Inc", "ticker": "PANW",
"cusip": "697435105", "isin": "US6974351057",
"balance": 500136, "units": "NS",
"valueUsd": 170556378.72, "pctVal": 6.5824318463,
"assetCat": "EC", "issuerCat": "CORP", "country": "US",
"fairValueLevel": 1, "mappedCik": 1327567, "mappedSymbol": "PANW" },
{ "name": "General Dynamics Corp", "ticker": "GD",
"cusip": "369550108", "isin": "US3695501086",
"balance": 360289, "units": "NS",
"valueUsd": 127628775.36, "pctVal": 4.9256892163,
"assetCat": "EC", "issuerCat": "CORP", "country": "US",
"fairValueLevel": 1, "mappedCik": 40533, "mappedSymbol": "GD" },
{ "name": "Mount Vernon Liquid Assets Portfolio, LLC",
"balance": 21019335, "units": "NS",
"valueUsd": 21019335, "pctVal": 0.8112176228,
"assetCat": "STIV", "issuerCat": "PF", "country": "US" }
]
}Every holding value in that response is in the SEC filing; mappedCik and mappedSymbol are StockFit's mapping, added on top. The receipt is HACK's N-PORT-P, accession 0001193125-26-372674, filed 2026-08-28 for the period ending 2026-06-30. In it, Palo Alto Networks is 500,136 shares worth $170,556,378.72 and General Dynamics is 360,289 shares worth $127,628,775.36, matching the response line for line. One practical note: Amplify files N-PORTs for its whole trust on the same day, so search EDGAR by the series (Amplify Cybersecurity ETF, S000082278), not just the trust CIK, or you will open the wrong fund's report.
Two things in this response are hard to get elsewhere. First, the full book: HACK reports 25 lines, and its ten largest add up to 52.13% of net assets, so an API that returns only the top 10 leaves out 15 lines worth 48.73%. (The 25 lines sum to 100.86%, not 100%, because N-PORT weights are percentages of net assets and the book includes the lending-collateral line covered below.) Second, the mapping: General Dynamics (4.93%) and Northrop Grumman (4.30%), two defense contractors, are 9.23% of a cybersecurity ETF. That is a fact about the data, not a judgment about the fund. Both sit inside the top 10, so even a short list shows them; what the N-PORT rows add is a mapped CIK on 22 of the 25 lines (12 of the 15 outside the top 10): every stock in the book except a Japan-listed holding that does not file with the SEC, so you can join those holdings to the issuer's own SEC filings without matching on names. The other two rows without one are the lending-collateral line and a money market fund.
Now the issuer's daily file for the same fund:
curl -H "Authorization: Bearer $TOKEN" \
"https://api.stockfit.io/v1/api/fund/holdings/daily?symbol=HACK&pageSize=12"// 25 rows; three shown, fields as returned
{
"reportDate": "2026-10-30",
"totalResults": 25,
"data": [
{ "name": "Broadcom Inc", "ticker": "AVGO", "cusip": "11135F101",
"balance": 652807, "units": "NS", "currency": "USD",
"valueUsd": 225218415, "pctVal": 6.1 },
{ "name": "Crowdstrike Holdings Inc", "ticker": "CRWD", "cusip": "22788C105",
"balance": 815891, "units": "NS", "currency": "USD",
"valueUsd": 213069934.65, "pctVal": 5.77 },
{ "name": "Northrop Grumman Corp", "ticker": "NOC", "cusip": "666807102",
"balance": 303662, "units": "NS", "currency": "USD",
"valueUsd": 170767362.32, "pctVal": 4.62 }
]
}This one is four months fresher: the report date is 2026-10-30, against 2026-06-30 for the filing. The order changed too. Broadcom now leads at 6.10%, and Netskope, which entered the fund during the quarter ended 2026-06-30 at 2.83% (per /api/fund/changes), is now 5.17%. What the daily rows do not carry is just as visible: no isin, no mappedCik, no assetCat, no fairValueLevel. You get the issuer's view of today, and you do the mapping yourself.
N-PORT vs daily holdings: three pitfalls when you combine them
The natural next step is to diff the two sources to see what a fund bought since its last filing. Three things in the HACK data will mislead that diff if you do not handle them.
1. Share counts across a stock split
The N-PORT row for CrowdStrike (CUSIP 22788C105) shows 188,031 shares on 2026-06-30. The daily row shows 815,891 shares on 2026-10-30. Read naively, HACK grew its CrowdStrike position by 334%. It did not. CrowdStrike split its stock 4-for-1, and split-adjusted trading began on July 2, 2026, two days after the N-PORT period ended. Adjusted, the filing figure is 752,124 shares, so the real change is about 8.5%. The N-PORT number is not wrong; it is the share count on its report date. Any cross-source share comparison has to check for splits between the two dates, or compare weights and values instead.
2. Which fields a daily file carries depends on the issuer
Rule 6c-11 asks for “CUSIP or other identifier”, and issuers take that literally. Three daily files, pulled on 2026-10-30, show three different shapes:
| Fund | Daily file as of | Rows | Fields present | Not in the file |
|---|---|---|---|---|
| HACK | 2026-10-30 | 25 | ticker, cusip, currency, valueUsd | isin, sedol, sector |
| SPY | 2026-10-28 | 505 | ticker, cusip, sedol, currency | valueUsd, isin, sector |
| IHAK | 2026-10-28 | 52 | ticker, currency, valueUsd, sector, assetCat, country | cusip, sedol, isin |
SPY's file has a SEDOL and no market value per row. IHAK's has sector and country but no CUSIP, and it labels its asset class Equity where N-PORT uses the code EC. The daily endpoint returns only the fields the issuer published and omits the rest rather than sending nulls, so code that assumes row["cusip"] exists will fail on the first IHAK row. Plan your join key per fund, not per API.
3. Securities-lending collateral appears only in the filing
The N-PORT response includes a line for Mount Vernon Liquid Assets Portfolio, LLC: 0.81% of the fund, asset category STIV (short-term investment vehicle), issuer category PF, no CUSIP, no ISIN. It is cash collateral the fund received for lending out securities, and the filing marks it with its cash-collateral flag set to Y. Our response does not expose that flag as a field, so we identify the row from assetCat plus the filing itself. The issuer's daily file has no such line. Neither source is wrong; they answer different questions. Decide whether collateral belongs in your weights, and filter STIV rows on purpose rather than by accident. The mutual fund holdings API guide walks through the other non-equity lines an N-PORT book can contain.
ETF constituents API in Python: join the two sources
The script below pulls both books for one fund, joins them on the best identifier each row has, and carries the N-PORT CIK onto the daily rows. It compares weights rather than share counts, for the split reason above.
import os
import re
import requests
BASE = "https://api.stockfit.io/v1/api/fund"
HEADERS = {"Authorization": f"Bearer {os.environ['STOCKFIT_TOKEN']}"}
CUSIP = re.compile(r"^[0-9A-Z]{9}$")
def fetch_all(path, **params):
"""Page through a holdings endpoint; return (reportDate, rows)."""
rows, page = [], 1
while True:
r = requests.get(f"{BASE}{path}", headers=HEADERS,
params={**params, "page": page, "pageSize": 1000})
r.raise_for_status()
body = r.json()
rows.extend(body["data"])
if page >= body.get("totalPages", 1):
return body.get("reportDate"), rows
page += 1
def join_key(row):
"""CUSIP if it looks like one, then ISIN, then ticker."""
cusip = row.get("cusip")
if cusip and CUSIP.match(cusip):
return ("cusip", cusip)
if row.get("isin"):
return ("isin", row["isin"])
if row.get("ticker"):
return ("ticker", row["ticker"].upper())
return None
nport_date, nport = fetch_all("/holdings", symbol="HACK")
daily_date, daily = fetch_all("/holdings/daily", symbol="HACK")
# Drop N-PORT short-term vehicles (assetCat STIV: lending collateral, money market)
# on purpose. Daily rows carry no assetCat, so the issuer's cash and money market
# lines stay in and print as unmatched.
nport = [r for r in nport if r.get("assetCat") != "STIV"]
index = {}
for r in nport:
for k in (join_key(r), ("ticker", (r.get("ticker") or "").upper())):
if k:
index.setdefault(k, r)
print(f"N-PORT as of {nport_date}, daily as of {daily_date}")
for d in daily:
match = index.get(join_key(d)) or index.get(("ticker", (d.get("ticker") or "").upper()))
cik = match.get("mappedCik") if match else None
before = round(match["pctVal"], 2) if match else None
print(f"{d.get('ticker', d['name']):6} cik={cik} "
f"weight {before} -> {d['pctVal']}")Three details in that code are deliberate. It reads reportDate from each response instead of assuming it, because the daily endpoint can return an older snapshot when an issuer publishes only month-end files. It validates the CUSIP format before trusting it as a key, since the rule allows “other identifier” in that position. And it falls back to the ticker, which is the one identifier every daily file in our sample carried. If you need the reverse question, which funds hold a given stock, that is a single call to /api/fund/reverse-lookup, covered in find all ETFs that hold a stock.
Historical ETF holdings: how far back each source goes
History is where the sources differ most, and where you should be most careful about what “historical” means on a product page.
- N-PORT holdings: one snapshot per quarter end. In StockFit,
/api/fund/holdings?symbol=IVV&reportDate=2019-09-30returns 508 rows, and a request for 2019-06-30 returns an empty set, so IVV's filing history starts at 2019-09-30. /api/fund/profile (Free) lists the report dates available for a fund. Each snapshot is tied to a filing you can open. - StockFit daily holdings: about 45 days per fund, accumulating since early August 2026. For HACK on 2026-10-30, a request for 2026-09-25 returned data and one for 2026-09-20 returned nothing. Pass
reportDateand you get the latest snapshot on or before that date. - Massive (ETF Global): “Records date back to April 3, 2017”, updated daily. The deepest daily history in this comparison.
- Intrinio: point-in-time by
as_of_dateordate; the ETF Holdings endpoint notes that history “Requires historical holdings entitlements.” - Finnworlds and Tradefeeds: both say historical data is available; neither page states how far back.
For most backtests the choice comes down to this: quarterly snapshots you can audit against a filing, or daily snapshots from a vendor feed you cannot. Quarter-to-quarter membership changes are available directly from /api/fund/changes, which for HACK between 2026-03-31 and 2026-06-30 reports one addition (Netskope), no removals and 23 reweighted positions.
Which ETF holdings API to choose
- You need numbers you can trace to an SEC filing, mapped to CIKs: N-PORT holdings, from StockFit's
/api/fund/holdingsor from the SEC data sets directly if you will parse the XML yourself. - You need today's book for funds in coverage, and the history you need is weeks, not years: StockFit daily holdings, with the N-PORT endpoint on the same key for the mapping.
- You need daily constituents going back years: an ETF Global product, through Massive or Intrinio.
- You need SEDOL, RIC or FIGI on every row as the primary key: Intrinio's ETF Holdings endpoint lists all of them.
- You only need sector weights and a holdings list by ticker inside an existing fundamentals subscription: EODHD.
Whichever you pick, test it on one fund you know well: check the as-of date, count the rows against the filing, and look for collateral and cash lines. If you want to try StockFit's side first, the supported-funds list and fund profiles work on a free key; to get one. For the same provenance test applied to company financials, see our fundamentals API comparison.
FAQ
Q.What is the best ETF holdings API?
/api/fund/holdings serves Form N-PORT data on the Starter, ETF and Pro plans. For the current daily book, StockFit's /api/fund/holdings/daily serves issuer files on the ETF plan. For daily history back to April 3, 2017, Massive's ETF Global constituents endpoint goes furthest. If you key on FIGI, RIC or SEDOL, Intrinio's ETF Holdings endpoint lists all three per row.Q.Where does ETF holdings data come from?
Q.How often are ETF holdings updated?
Q.Is there a free ETF holdings API?
Q.Can I get historical ETF holdings by API?
Q.Do ETF holdings APIs return full holdings or only the top 10?
Q.How do I match ETF holdings to CUSIP, ISIN or CIK?
Ready to build?
Free API key, no credit card.