Export executive compensation to CSV and Excel
Pull Summary Compensation Tables for several companies and save them as a CSV file and a formatted Excel workbook with one sheet per company.
Compensation analysts and board consultants live in spreadsheets. This tutorial pulls the Summary Compensation Table from the latest proxy statement of five companies with /v1/executive-compensation and writes:
executive_compensation.csv: one row per executive per fiscal year, all companies, ready for pandas, a database or a BI toolexecutive_compensation.xlsx: one sheet per company, dollar formatting, bold headers, frozen first column
It uses requests and openpyxl (pip install requests openpyxl).
The script
import csv
import os
import requests
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}
TICKERS = ["MSFT", "NVDA", "WMT", "AAPL", "GOOGL"]
COLUMNS = ["ticker", "accessionNo", "name", "year", "salary", "bonus", "stockAwards", "nonEquityIncentive", "pensionAndDeferred", "allOther", "total"]
rows = []
for t in TICKERS:
d = requests.get(f"{BASE}/v1/executive-compensation", params={"ticker": t}, headers=HEADERS, timeout=120).json()
if "executives" not in d:
print(f"{t}: skipped ({d.get('error')})")
continue
for e in d["executives"]:
rows.append({"ticker": t, "accessionNo": d["accessionNo"], **e})
print(f"{len(rows)} rows from {len(TICKERS)} proxy statements")
# 1. CSV: one row per executive per year
with open("executive_compensation.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=COLUMNS, extrasaction="ignore")
w.writeheader()
w.writerows(rows)
# 2. Excel: a sheet per company, money formatted, header bold, widths set
wb = Workbook()
wb.remove(wb.active)
for t in TICKERS:
ws = wb.create_sheet(t)
ws.append(COLUMNS[2:])
for r in (r for r in rows if r["ticker"] == t):
ws.append([r.get(c) for c in COLUMNS[2:]]) # pensionAndDeferred only appears when the table has that column
for cell in ws[1]:
cell.font = Font(bold=True)
for row in ws.iter_rows(min_row=2, min_col=3):
for cell in row:
cell.number_format = "$#,##0"
ws.column_dimensions["A"].width = 60
for i in range(2, len(COLUMNS) - 1):
ws.column_dimensions[get_column_letter(i)].width = 16
ws.freeze_panes = "B2"
wb.save("executive_compensation.xlsx")
for path in ("executive_compensation.csv", "executive_compensation.xlsx"):
print(f"{path:<30} {os.path.getsize(path):>8,} bytes")
print()
print(open("executive_compensation.csv").read().splitlines()[0])
print(open("executive_compensation.csv").read().splitlines()[1])
Output
70 rows from 5 proxy statements
executive_compensation.csv 9,407 bytes
executive_compensation.xlsx 12,643 bytes
ticker,accessionNo,name,year,salary,bonus,stockAwards,nonEquityIncentive,pensionAndDeferred,allOther,total
MSFT,0001193125-25-245150,Satya Nadella Chairman and Chief Executive Officer,2025,2500000.0,0.0,84245496.0,9555000.0,,196294.0,96496790.0
The WMT sheet's first data row reads Doug McMillon, 2026, 1500000, 0, 21051605, 4032000, 2215278, 442047, 29240930, shown in Excel as dollars.
About the columns
| Column | Summary Compensation Table column |
|---|---|
salary | Salary |
bonus | Bonus (discretionary cash, not formula based) |
stockAwards | Stock awards at grant date fair value |
nonEquityIncentive | Non-equity incentive plan compensation (formula based cash bonus) |
pensionAndDeferred | Change in pension value and nonqualified deferred compensation earnings |
allOther | All other compensation: security, aircraft, 401(k) match, perquisites |
total | Total as reported |
Not every company's table has every column. Walmart reports pension and deferred compensation earnings; Microsoft does not, so its cells stay empty. The script uses r.get(c) for that reason, and extrasaction="ignore" so a column you did not list (for example option awards) does not break the CSV.
Tips
- Check
totalagainst the parts. Totals are copied from the proxy as printed. A quickabs(sum(parts) - total)check catches footnote markers or misread cells before they reach a report. - Handle companies without a table. The script skips a ticker when the response has no
executiveskey and prints the reason. Some proxies put the table in an unusual layout, or the company filed its compensation disclosure in a 10-K amendment instead. - Keep the accession number. It points to the exact proxy statement each row came from, which reviewers will ask for.
- Refresh yearly. Proxy statements are filed once a year, mostly between March and May. Cache the results.
Next steps
- Compare executive compensation across companies.
- Executive Compensation API and Pay Versus Performance API.
- Financial statements to Excel for the same workflow with XBRL statements.
- DEF 14A dataset and the API reference.