Home / Guides / LLM workflows
LLM workflows

Analyze SEC comment letters

Find SEC staff comment letters (UPLOAD) and company responses (CORRESP), read a full letter thread as text, and measure which topics the staff asks about with full-text search.

When the SEC's Division of Corporation Finance reviews a filing, it sends the company a comment letter. The company answers, the staff may follow up, and when the review is complete the staff closes it with a short letter. The SEC publishes the whole exchange on EDGAR, generally no earlier than 20 business days after the review is complete:

  • UPLOAD is a letter from the SEC staff to the company.
  • CORRESP is the company's response.

Comment letters show what the regulator is pushing on (non-GAAP measures, segment reporting, revenue recognition), which makes them useful for accounting research, for preparing your own filings, and as input for an LLM that summarizes review themes.

Setup

import html
import os
import re
import textwrap
import requests

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

def get(path, **params):
    r = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=60)
    r.raise_for_status()
    return r

Step 1: how many letters are there?

for y in (2022, 2023, 2024, 2025):
    n = get("/v1/filings", query=f'formType:"UPLOAD" AND filedAt:[{y}-01-01 TO {y}-12-31]', size=1).json()["total"]
    print(y, "UPLOAD", n)

Output:

2022 UPLOAD {'value': 8517, 'relation': 'eq'}
2023 UPLOAD {'value': 8214, 'relation': 'eq'}
2024 UPLOAD {'value': 8571, 'relation': 'eq'}
2025 UPLOAD {'value': 5271, 'relation': 'eq'}

About 8,500 staff letters a year. The lower 2025 count partly reflects the release lag: letters from reviews that finished late in the year appear on EDGAR in the following months.

Step 2: one company's thread

Query both form types for one company and one year:

thread = get("/v1/filings", query='ticker:NVDA AND formType:("UPLOAD", "CORRESP") AND filedAt:[2025-01-01 TO 2025-12-31]', size=10).json()["filings"]
for f in thread:
    print(f["filedAt"], f["formType"], f["accessionNo"])
2025-09-10 UPLOAD 0000000000-25-009788
2025-07-31 CORRESP 0001045810-25-000192
2025-07-22 UPLOAD 0000000000-25-007684

Read from the bottom up: the staff wrote on July 22, NVIDIA answered on July 31, and the staff closed the review on September 10. Staff letters have accession numbers starting with 0000000000 because the SEC itself posts them.

Step 3: read a letter as text

The staff's letter is a PDF, but EDGAR also posts a plain text extract of it as a separate .txt file in the same filing. Company responses are usually HTML. This helper prefers the text extract and falls back to stripping HTML:

def letter_text(f):
    files = get("/v1/files", cik=f["cik"], accessionNo=f["accessionNo"]).json()
    acc = f["accessionNo"]
    txt = [x for x in files if x["name"].endswith(".txt") and not x["name"].startswith(acc)]
    htm = [x for x in files if x["name"].endswith((".htm", ".html")) and "index" not in x["name"]]
    doc = (txt or htm)[0]
    raw = get("/v1/download", url=doc["url"]).text
    return doc["name"], re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", raw))).strip()

first = [f for f in thread if f["formType"] == "UPLOAD"][-1]  # the oldest letter opens the thread
name, text = letter_text(first)
print(first["filedAt"], name, len(text), "chars")
print(textwrap.fill(text[:2200], 110))

Output (first part of the letter):

2025-07-22 filename2.txt 3894 chars
TEXT-EXTRACT 2 filename2.txt July 22, 2025 Colette Kress Executive Vice President and Chief Financial Officer
NVIDIA Corporation 2788 San Tomas Expressway Santa Clara, California 95051 Re: NVIDIA Corporation Form 10-K
for the Fiscal Year Ended January 26, 2025 Form 8-K furnished May 28, 2025 File No. 000-23985 Dear Colette
Kress: We have limited our review of your filing to the financial statements and related disclosures and have
the following comments. Please respond to this letter within ten business days by providing the requested
information or advise us as soon as possible when you will respond. [...]
1. We note that you disclose non-GAAP margin and Non-GAAP diluted earnings per share excluding the $4.5
billion inventory charge taken in the first quarter of fiscal 2026. Please provide us more information as to
the nature of the charge and explain to us why you do not believe that it represents a normal, recurring
operating cost of your business. [...]
2. We note that during fiscal year 2025 your revenue has increased significantly. [...] Please tell us the
amount of revenue related to licensing or other services, and if that amount is over 10% of consolidate[d]

Two comments: one on a non-GAAP adjustment in the earnings release (Exhibit 99.1 of an 8-K), one on revenue disaggregation in the 10-K. The company's July 31 response (8,937 characters as text) repeats each comment and answers it in turn, which makes the pair easy to align by comment number. The closing letter of September 10 is 615 characters: "We have completed our review of your filings."

Step 4: which topics come up most?

Full-text search covers comment letters too. Count 2025 staff letters that mention a phrase:

for phrase in ['"non-GAAP"', '"segment reporting"', '"cybersecurity"', '"climate"', '"going concern"']:
    r = get("/v1/full-text-search", query=phrase, formTypes="UPLOAD", startDate="2025-01-01", endDate="2025-12-31").json()
    print(f"{phrase:22} {r['total']['value']} documents")
"non-GAAP"             424 documents
"segment reporting"    158 documents
"cybersecurity"        34 documents
"climate"              10 documents
"going concern"        118 documents

Each staff letter is indexed twice, as the PDF (fileType LETTER) and as the text extract (TEXT-EXTRACT), so divide by two for an approximate number of letters: about 212 letters mentioning non-GAAP measures, against about 5 mentioning climate. Each result includes the ticker and company, so you can go straight to the thread:

r = get("/v1/full-text-search", query='"non-GAAP"', formTypes="UPLOAD", startDate="2025-01-01", endDate="2025-12-31").json()
for f in r["filings"][:4]:
    print(f["filedAt"], f["ticker"], f["companyNameLong"].split("  (")[0], f["fileType"])
2025-04-01 SWK STANLEY BLACK & DECKER, INC. LETTER
2025-04-01 SWK STANLEY BLACK & DECKER, INC. TEXT-EXTRACT
2025-08-22 INDI indie Semiconductor, Inc. TEXT-EXTRACT
2025-08-22 INDI indie Semiconductor, Inc. LETTER

Handing letters to an LLM

Comment letters are short (a few thousand characters) and well structured with numbered comments, so a whole thread fits easily in a model's context window. Pass the staff letter and the response together and ask for one row per comment: topic, the staff's request, the company's position, and whether it agreed to change future filings. With the SecFeed MCP server, an assistant can do the search, file listing and download steps itself.

Next steps

More llm workflows 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 →