Who owns a stock: 13F holders by CUSIP
List every institutional manager that reported a stock on Form 13F for a quarter, clean the obvious filer errors, and rank holders by shares.
Institutional managers with more than $100 million in 13(f) securities file Form 13F every quarter, listing each position by CUSIP. Flip the data around, from manager to security, and you get the institutional holder list for any stock.
This tutorial builds the holder list for NVIDIA common stock (CUSIP 67066G104) for the quarter ended June 30, 2026.
Find the CUSIP
If you start from a ticker, map it first. GET /v1/mapping/cusip?cusip=... goes the other way (CUSIP to issuer and ticker), and the 13F holdings rows themselves carry the issuer name, so you can confirm you have the right security in the first result.
Fetch every holding of the CUSIP
GET /v1/form-13f/holdings with cusip and period returns the rows sorted by value, up to 1,000 per page. Page with from until a page comes back short.
import os
from collections import defaultdict
import requests
BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}
CUSIP, PERIOD = "67066G104", "06-30-2026" # NVIDIA common stock, Q2 2026 reports
def holders(cusip, period):
rows, offset = [], 0
while True:
r = requests.get(f"{BASE}/v1/form-13f/holdings", headers=HEADERS, timeout=120,
params={"cusip": cusip, "period": period, "from": offset, "size": 1000})
r.raise_for_status()
page = r.json()
rows += page
if len(page) < 1000:
return rows
offset += 1000
rows = holders(CUSIP, PERIOD)
# Sanity check: value / shares is the implied price. 13F rows are typed by hand, and some filers report value in
# thousands or swap the columns. Keep rows whose implied price is within 2x of the median.
prices = sorted(h["value"] / h["shares"] for h in rows if h["shares"] and not h["put_call"])
median = prices[len(prices) // 2]
bad = [h for h in rows if h["shares"] and not h["put_call"] and not (median / 2 <= h["value"] / h["shares"] <= median * 2)]
print(f"Median implied price ${median:,.2f}; rows outside 0.5x to 2x of it: {len(bad)}")
for h in sorted(bad, key=lambda h: -h["shares"])[:3]:
print(f" {h['filer'][:40]:<40} shares {h['shares']:>16,.0f} value {h['value']:>18,.0f}")
bad_ids = {(h["accession"], h["seq"]) for h in bad}
rows = [h for h in rows if (h["accession"], h["seq"]) not in bad_ids]
print()
by_filer = defaultdict(lambda: {"shares": 0.0, "value": 0.0, "options": 0})
for h in rows:
f = by_filer[h["filer"]]
if h["put_call"]:
f["options"] += 1
continue # puts and calls are reported on the underlying shares, keep them out of the share totals
f["shares"] += h["shares"]
f["value"] += h["value"]
total_shares = sum(f["shares"] for f in by_filer.values())
print(f"{rows[0]['issuer']} ({CUSIP}), period {PERIOD}")
print(f"Holdings rows: {len(rows):,} | managers: {len(by_filer):,} | shares held: {total_shares:,.0f}")
print(f"Managers that also report puts or calls: {sum(1 for f in by_filer.values() if f['options']):,}")
print(f"\n{'Manager':<40}{'Shares':>16}{'Value':>20}{'Share of 13F total':>20}")
for name, f in sorted(by_filer.items(), key=lambda x: -x[1]["shares"])[:10]:
print(f"{name[:39]:<40}{f['shares']:>16,.0f}{f['value']:>20,.0f}{f['shares'] / total_shares:>20.1%}")
Output:
Median implied price $200.09; rows outside 0.5x to 2x of it: 798
CALIFORNIA STATE TEACHERS RETIREMENT SYS shares 7,007,449,934 value 35,021,490
PRICE T ROWE ASSOCIATES INC /MD/ shares 334,210,126 value 66,872,105
SG Americas Securities, LLC shares 62,298,007 value 12,465,208
NVIDIA CORPORATION (67066G104), period 06-30-2026
Holdings rows: 9,386 | managers: 5,760 | shares held: 17,367,848,482
Managers that also report puts or calls: 344
Manager Shares Value Share of 13F total
STATE STREET CORP 2,019,566,874 404,095,135,818 11.6%
BlackRock, Inc. 1,941,918,386 388,558,449,933 11.2%
VANGUARD CAPITAL MANAGEMENT LLC 1,540,816,358 308,301,945,072 8.9%
FMR LLC 1,026,051,548 205,302,654,261 5.9%
GEODE CAPITAL MANAGEMENT, LLC 606,216,492 120,957,505,504 3.5%
VANGUARD PORTFOLIO MANAGEMENT LLC 517,816,949 103,609,993,325 3.0%
JPMORGAN CHASE & CO 449,404,578 87,620,410,963 2.6%
MORGAN STANLEY 356,420,089 71,316,096,193 2.1%
Invesco Ltd. 329,593,488 65,948,361,031 1.9%
NORGES BANK 325,951,734 65,219,682,456 1.9%
Why the sanity check matters
13F information tables are prepared by each manager, and errors are common in a dataset this size. The script computes each row's implied price (value divided by shares) and drops rows that are more than a factor of two away from the median. For this quarter that removed 798 of 10,184 rows. About two thirds of them (508) have an implied price close to one thousandth of the real one, the signature of value entered in thousands of dollars, the format the SEC used before 2023. The largest are worse: one row reports more than 7 billion shares, and another reports a position worth about 20 cents a share.
Without the check, those rows would put a pension fund at the top of the holder list with 28% of all reported shares. Always validate before you rank.
What the numbers mean
- Managers counts distinct 13F filers. Large firms often file more than one 13F (for example separate Vanguard entities) and report a position in several rows, so the script sums rows per filer.
- Shares held is the total across 13F filers, not the share of shares outstanding. Divide by the issuer's shares outstanding from
/v1/shares-outstandingto estimate institutional ownership. - Puts and calls are reported on the underlying shares. They are excluded from the share totals and counted separately.
Compare quarters
Run the same request with the previous period (for example 03-31-2026) and join on filer to see who added, trimmed or exited. 13F reports are due 45 days after quarter end, so a quarter's list is complete by mid February, May, August or November.