Disclosure: I built the Apify Actor mentioned in the last section, and it is paid. This article was drafted with an AI assistant. The Python code and its output below were run on October 7, 2026, against the live SEC servers. I am not affiliated with the SEC.

Large investment managers in the United States must file Form 13F every quarter. The form lists the shares they hold. People search for “13f holdings” because they want that list as a table: what the manager owns, how many shares, what they are worth, and what changed since the last quarter. You can get this for free from the public filing. This guide shows the route, with code that I ran, and then the limits you should know before you use the numbers.

What a 13F filing is

A manager that controls at least $100 million in certain listed securities files Form 13F within 45 days after the end of each quarter. The filing lists long positions on the last day of the quarter. Most of them are U.S. listed shares and ETFs. The filing does not list short positions or cash.

Each filing has two parts that matter here. The cover page (primary_doc.xml) holds the report date and totals. The information table (an XML file) holds one line per holding. You want the information table.

The free route

The route has three steps. All of them use public SEC addresses.

  1. Get the manager’s filing list. Take the manager’s CIK, which is the number the SEC uses to identify a filer. Pad it with zeros to 10 digits and open https://data.sec.gov/submissions/CIK##########.json. The filings.recent block lists the latest filings. Older filings are listed in the extra files named in filings.files.
  2. Pick the filings with form type 13F-HR. The fields reportDate, filingDate and accessionNumber tell you which quarter each filing covers.
  3. Open the filing folder at https://www.sec.gov/Archives/edgar/data/<CIK>/<accession without dashes>/index.json. Find the XML file that is not primary_doc.xml. That file is the information table.

Two rules of the SEC

The SEC asks users to follow two rules. Keep to no more than 10 requests per second, even across several machines. And declare who you are in the User-Agent header, with a company name and a contact address. In the kit tests behind this article, some User-Agent strings without the word “bot” or an email address got a 403 answer from www.sec.gov. A string that contains “bot” got through. In real work, use your own name and contact address, as the SEC asks.

Python example

This script finds the latest two 13F filings of Pershing Square Capital Management (CIK 1336528), reads both information tables, adds up the lines that share a CUSIP and prints the change in shares. It waits 0.2 seconds before each request, so it stays below 5 requests per second.

import json, time, urllib.request
import xml.etree.ElementTree as ET
from collections import defaultdict

UA = "Example13fReader-bot/1.0 (blog example, no contact given)"
CIK = 1336528  # Pershing Square Capital Management, L.P.

def get(url):
    time.sleep(0.2)  # stay far below 10 requests per second
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    return urllib.request.urlopen(req).read()

sub = json.loads(get(f"https://data.sec.gov/submissions/CIK{CIK:010d}.json"))
recent = sub["filings"]["recent"]
filings = [
    (recent["reportDate"][i], recent["filingDate"][i], recent["accessionNumber"][i])
    for i, form in enumerate(recent["form"]) if form == "13F-HR"
]
filings.sort(reverse=True)

def positions(accession):
    base = f"https://www.sec.gov/Archives/edgar/data/{CIK}/{accession.replace('-', '')}/"
    index = json.loads(get(base + "index.json"))
    names = [f["name"] for f in index["directory"]["item"] if f["name"].lower().endswith(".xml")]
    table = [n for n in names if n != "primary_doc.xml"][0]
    root = ET.fromstring(get(base + table))
    ns = "{http://www.sec.gov/edgar/document/thirteenf/informationtable}"
    out = defaultdict(lambda: [0, 0, ""])
    for it in root.iter(ns + "infoTable"):
        key = it.find(ns + "cusip").text
        out[key][0] += int(it.find(ns + "shrsOrPrnAmt/" + ns + "sshPrnamt").text)
        out[key][1] += int(it.find(ns + "value").text)
        out[key][2] = it.find(ns + "nameOfIssuer").text
    return out

(cur_q, cur_f, cur_a), (old_q, old_f, old_a) = filings[0], filings[1]
print(sub["name"], "| latest 13F-HR:", cur_q, "filed", cur_f, "| prior:", old_q)
cur, old = positions(cur_a), positions(old_a)
print(f"{'issuer':<24}{'cusip':<11}{'shares':>12}{'value_usd':>16}{'change':>12}")
for cusip, (sh, val, name) in sorted(cur.items(), key=lambda x: -x[1][1]):
    chg = sh - old[cusip][0] if cusip in old else sh
    print(f"{name[:23]:<24}{cusip:<11}{sh:>12,}{val:>16,}{chg:>+12,}" + ("  new" if cusip not in old else ""))

Output on 7 October 2026:

Pershing Square Capital Management, L.P. | latest 13F-HR: 2026-03-31 filed 2026-05-15 | prior: 2025-12-31
issuer                  cusip            shares       value_usd      change
BROOKFIELD CORP         11271J107    59,697,208   2,415,946,008  -1,705,881
AMAZON COM INC          023135106    11,451,981   2,385,104,083  +1,844,157
UBER TECHNOLOGIES INC   90353T100    29,958,771   2,154,934,398    -248,963
MICROSOFT CORP          594918104     5,654,078   2,092,970,053  +5,654,078  new
RESTAURANT BRANDS INTL  76131D103    22,645,483   1,673,501,194    -221,290
META PLATFORMS INC      30303M102     2,660,861   1,522,358,404     -12,708
HOWARD HUGHES HOLDINGS  44267T102    18,852,064   1,192,581,569          +0
SEAPORT ENTMT GROUP INC 812215200     5,023,780     107,910,794          +0
ALPHABET INC            02079K107       311,726      89,421,720  -5,852,145
HERTZ GLOBAL HLDGS INC  42806J700    15,241,127      70,261,595          +0
ALPHABET INC            02079K305        32,376       9,310,043    -645,921

The run made 5 requests. The result is 11 positions for the quarter that ended on 31 March 2026. Brookfield, the largest line, went from 61,403,089 shares to 59,697,208 shares, a fall of 1,705,881.

Things the script does not handle

  • Put and call lines. The script adds lines by CUSIP only. A filing can hold a put option and the share on the same CUSIP. Add the put or call field to the key if you need them apart.
  • Sold-out positions. The loop only shows what the manager holds now. A line that was in the prior quarter and is gone does not appear. Loop over the prior table too if you want to see sold-out positions.
  • Amended filings. A correction is a separate filing of type 13F-HR/A. The script ignores it.
  • Value units. Per the kit’s checks, filings made before 3 January 2023 state value in thousands of dollars, and later filings state it in dollars. The script reads the number as it is, which is correct for the 2026 filings above. I checked this change with two filings, one on each side of the date. I did not check the exact cut-off day, so test a filing from around that date before you rely on it.
  • Tickers. The table has the issuer name and CUSIP, not a ticker.

What 13F data can and cannot tell you

13F data is useful, but it is easy to read too much into it.

  • It is delayed. A manager has up to 45 days after quarter end to file, and many file near the deadline. The data is 45 days old or more when you see it.
  • It is not real-time. You see one day per quarter. Trades inside the quarter are not visible.
  • It is not a full portfolio. It covers long positions in 13(f) securities. It has no short positions and no cash. A manager can also ask the SEC to keep some positions confidential.
  • It shows what a manager held, not why.

If you do not want to write the code

I built an unofficial Apify Actor called 13F Holdings API. It reads the same public filings. You enter one or more CIK numbers and a quarter. It returns one row per security with issuer, CUSIP, shares, value in dollars, put or call, and the change in shares against the prior filing. It can also add sold-out positions and convert the old thousand-dollar values.

It costs $1.50 per 1,000 rows on the free Apify plan. A test run on 7 October 2026 for Berkshire Hathaway (29 positions) and Pershing Square (11 positions), with the change against the prior quarter, returned 40 rows in about one second with 10 requests to the SEC. That run costs about $0.06. A very large manager costs more. The latest filing of Morgan Stanley has 45,905 lines that add up to 8,402 positions, which is about $12.60 on the free plan. The Max results setting puts a ceiling on that cost.

The Python above does the same job for one manager at no cost, and for a single lookup it is the better choice. The Actor helps if you track many managers or want to run the job each quarter on a schedule. The Actor is not an SEC product and is not approved by the SEC.

Limits of the Actor

  • It has the same limits as the filings: delay of up to 45 days, long positions only, no real-time data.
  • It compares against the prior original filing that exists, which is not always the quarter just before. It does not adjust for share splits.
  • It counts amended filings but does not merge them into the rows.
  • It reads XML information tables from the quarter ended 30 June 2013 onward. It does not read older text filings.
  • The SEC limit of 10 requests per second applies to all of your requests together, so do not start many runs at the same moment.

Source: filings published on SEC EDGAR. SEC content on sec.gov is free to access and reuse, and the SEC asks users to cite it as the source.