Home / Guides / Boards & pay
Boards & pay

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 tool
  • executive_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

ColumnSummary Compensation Table column
salarySalary
bonusBonus (discretionary cash, not formula based)
stockAwardsStock awards at grant date fair value
nonEquityIncentiveNon-equity incentive plan compensation (formula based cash bonus)
pensionAndDeferredChange in pension value and nonqualified deferred compensation earnings
allOtherAll other compensation: security, aircraft, 401(k) match, perquisites
totalTotal 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 total against the parts. Totals are copied from the proxy as printed. A quick abs(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 executives key 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

More boards & pay 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 →