Home / Guides / Streams, funds & search
Streams, funds & search

Analyze proxy voting records with Form N-PX

Parse Form N-PX filings to see how funds and institutional managers voted on proxy proposals, by category and against management recommendations.

Form N-PX reports how a fund voted every proxy it was entitled to vote in the year to June 30. Since the SEC's 2022 amendments took effect for votes from July 1, 2023, institutional managers that file 13F also report their say-on-pay votes on N-PX, and every report uses a structured XML format with standard vote categories.

The result is a dataset of millions of individual votes: issuer, meeting date, proposal, category, shares voted, how they were voted and what management recommended.

Fetch filings and parse the vote tables

GET /v1/n-px returns N-PX filings parsed to JSON. The cover page is in primary_doc. The vote table is a separate document in the same filing, and its name depends on the filing agent (ProxyVotingTable, proxyvotetable, or a file-specific name), so the script reads every document other than the cover page that contains a proxyTable.

import os
from collections import Counter
import requests

BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}


def as_list(x):
    return x if isinstance(x, list) else [x] if x else []


r = requests.get(f"{BASE}/v1/n-px", params={"startDate": "2026-07-01", "size": 100}, headers=HEADERS, timeout=120)
r.raise_for_status()
filings = r.json()["filings"]
print("N-PX filings since 2026-07-01:", len(filings))
print(Counter(f["data"]["primary_doc"]["formData"]["coverPage"]["reportInfo"]["reportType"] for f in filings))

votes = []
for f in filings:
    for key, doc in f["data"].items():  # the vote table's document name varies by filing agent
        if key == "primary_doc" or not isinstance(doc, dict):
            continue
        for row in as_list(doc.get("proxyTable")):
            for rec in as_list((row.get("vote") or {}).get("voteRecord")):
                votes.append((f["companyName"], row, rec))

print(f"\nVote records parsed: {len(votes):,}")
how = Counter(rec.get("howVoted") for _, _, rec in votes)
print("How shares were voted:", dict(how.most_common()))
# Only issuer proposals: on shareholder proposals filers code managementRecommendation inconsistently
issuer = [v for v in votes if v[1].get("voteSource") == "ISSUER" and v[2].get("managementRecommendation") in ("FOR", "AGAINST")]
against_mgmt = [v for v in issuer if v[2].get("howVoted") in ("FOR", "AGAINST") and v[2]["howVoted"] != v[2]["managementRecommendation"]]
print(f"Issuer proposals with a FOR or AGAINST recommendation: {len(issuer):,}")
print(f"Voted against the management recommendation: {len(against_mgmt):,} ({len(against_mgmt) / max(1, len(issuer)):.2%})")
holder = sum(1 for v in votes if v[1].get("voteSource") == "SECURITY HOLDER")
print(f"Votes on shareholder proposals: {holder:,}")

cats = Counter()
for _, row, _ in votes:
    for c in as_list((row.get("voteCategories") or {}).get("voteCategory")):
        cats[c.get("categoryType")] += 1
print("\nTop proposal categories:")
for c, n in cats.most_common(6):
    print(f"  {c:<45}{n:>7,}")

print("\nExamples of votes against management:")
seen = Counter((row["issuerName"], row["voteDescription"][:60]) for _, row, _ in against_mgmt)
for (issuer_name, desc), n in seen.most_common(5):
    print(f"  {issuer_name[:26]:<27}| {n:>3} votes | {desc}")

Output:

N-PX filings since 2026-07-01: 34
Counter({'INSTITUTIONAL MANAGER NOTICE REPORT': 21, 'FUND VOTING REPORT': 11, 'INSTITUTIONAL MANAGER VOTING REPORT': 2})

Vote records parsed: 313,400
How shares were voted: {'FOR': 281672, 'AGAINST': 20780, 'WITHHOLD': 5419, 'ABSTAIN': 4070, 'ONE YEAR': 1424, 'NO VOTE': 21, '1 YEAR': 14}
Issuer proposals with a FOR or AGAINST recommendation: 298,737
Voted against the management recommendation: 80 (0.03%)
Votes on shareholder proposals: 13,132

Top proposal categories:
  DIRECTOR ELECTIONS                           185,275
  CORPORATE GOVERNANCE                          38,654
  COMPENSATION                                  22,929
  AUDIT-RELATED                                 22,733
  CAPITAL STRUCTURE                             20,644
  SECTION 14A SAY-ON-PAY VOTES                  17,250

Examples of votes against management:
  Nestle SA                  |  16 votes | Transact Other Business (Voting)
  Constellium SE             |  10 votes | Advisory (non-binding) vote to hold an advisory (non-binding
  ANZ Group Holdings Limited |   8 votes | Approve the Spill Resolution
  Brookdale Senior Living In |   8 votes | Election of eight (8) nominees to serve as directors to the 
  Orange SA                  |   7 votes | Dismiss Pierre Chaussoneaux as Representative of Employees t

Reading the result

Of the 34 filings, 13 carry vote tables, and together they hold more than 313,000 vote records. Director elections make up well over half of the proposals.

Measuring votes against management takes care. On issuer proposals the recommendation is consistent, and in this sample the funds voted against it 80 times out of almost 300,000, mostly on foreign "other business" items, advisory votes and a board spill resolution. On shareholder proposals, filers code managementRecommendation inconsistently (some record the board's recommendation, some the direction of the proposal), so the script leaves those 13,132 votes out of the ratio. For shareholder proposal research, read howVoted directly and look up the board's recommendation in the proxy statement.

Report types

The cover page reportType tells you whether a filing has votes:

  • FUND VOTING REPORT: a registered fund's votes
  • INSTITUTIONAL MANAGER VOTING REPORT: a 13F manager's say-on-pay votes
  • INSTITUTIONAL MANAGER NOTICE REPORT: the manager did not exercise voting power, or another filer reports the votes; there is no vote table

Fields in each vote record

FieldMeaning
issuerName, cusip, isinThe company holding the meeting
meetingDateMeeting date
voteDescriptionThe proposal as described by the issuer
voteCategoriesStandard SEC categories, such as DIRECTOR ELECTIONS or SECTION 14A SAY-ON-PAY VOTES
voteSourceISSUER or SECURITY HOLDER (shareholder proposal)
sharesVoted, sharesOnLoanShares voted, and shares on loan and not recalled
vote.voteRecordOne or more records with howVoted, sharesVoted and managementRecommendation

A proposal can have several vote records when a fund split its vote, which is why the script iterates voteRecord as a list.

Ideas to extend it

  • Filter voteSource == "SECURITY HOLDER" to study support for shareholder proposals.
  • Group by voteSeries to compare how different series of the same trust voted.
  • Join on cusip with 13F holdings to weight votes by position size.

Next steps

More streams, funds & search guides

Run this guide with your own key

500 free API calls a month. Every example on this page works on the free plan.

Get free API key →