SEC EDGAR Companyfacts API Tutorial
The SEC EDGAR APIs provide public filing data. For developers building US equity research pipelines, two resources are especially useful:
- Companyfacts: XBRL facts reported by one company.
- Submissions: filing history and filing metadata for one company.
The data is valuable, but it is not a ready-made point-in-time fundamentals panel. You still need to resolve tickers to CIKs, select tags and units, distinguish annual from quarterly facts, track filing dates, handle amendments and restatements, and prevent future information from leaking into historical tests.
This tutorial covers that plumbing directly. If you would rather inspect a finished example first, Tradevo Data publishes a no-signup proof pack at https://github.com/christianpichichero-max/pit-fundamentals.
The two SEC endpoints you need
SEC requests use a zero-padded, 10-digit Central Index Key rather than a ticker.
https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json
https://data.sec.gov/submissions/CIK0000320193.json
The first URL returns company facts grouped broadly as:
facts -> taxonomy -> concept -> units -> observations
A typical observation may contain:
fy: fiscal yearfp: fiscal period, such asFYorQ1form: filing form, such as10-Kor10-Qfiled: filing datestartandend: fact period boundariesval: reported valueaccn: filing accession numberframe: an SEC calendar frame when available
The submissions response contains filing metadata, including accession numbers, forms, filing dates, report dates, and primary documents. It is useful for joining facts back to filings and inspecting amendments.
Respect SEC access rules
Consult the SEC's current developer and fair-access guidance before operating an automated client. Identify requests through a descriptive User-Agent, use a real application name and contact address, and apply conservative rate limiting.
A conservative serial Python session looks like this:
import time
import requests
HEADERS = {
"User-Agent": "FundamentalsResearch/1.0 research@example.com",
"Accept-Encoding": "gzip, deflate",
}
session = requests.Session()
session.headers.update(HEADERS)
def sec_get(url):
response = session.get(url, timeout=30)
response.raise_for_status()
time.sleep(0.11)
return response.json()
For production ingestion, add caching, exponential backoff, retry limits, logging, and a shared rate limiter. Do not let multiple workers independently assume they each have the full request allowance.
Resolve a ticker to a CIK
The SEC publishes a company-ticker mapping at:
https://www.sec.gov/files/company_tickers.json
Use it to build a case-insensitive lookup and pad the numeric CIK:
TICKERS_URL = "https://www.sec.gov/files/company_tickers.json"
def ticker_to_cik(ticker):
records = sec_get(TICKERS_URL)
target = ticker.upper()
for record in records.values():
if record["ticker"].upper() == target:
return str(record["cik_str"]).zfill(10)
raise KeyError(f"Ticker not found: {ticker}")
Cache this mapping. Tickers are convenient identifiers, but they can change or be reused; keep the CIK in your internal security model.
Download companyfacts and submissions
def get_companyfacts(cik):
url = f"https://data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json"
return sec_get(url)
def get_submissions(cik):
url = f"https://data.sec.gov/submissions/CIK{cik}.json"
return sec_get(url)
cik = ticker_to_cik("AAPL")
companyfacts = get_companyfacts(cik)
submissions = get_submissions(cik)
Companyfacts may include standard us-gaap concepts and company-specific extension concepts. Do not assume every issuer reports the same economic item under the same tag throughout its history.
Extract a concept without losing provenance
Here is a minimal extractor for observations associated with a concept and preferred unit:
import pandas as pd
def extract_concept(data, concept, taxonomy="us-gaap", unit="USD"):
concept_data = data["facts"][taxonomy][concept]
observations = concept_data["units"].get(unit, [])
rows = []
for item in observations:
rows.append({
"entity": data.get("entityName"),
"cik": data.get("cik"),
"taxonomy": taxonomy,
"concept": concept,
"label": concept_data.get("label"),
"unit": unit,
"start": item.get("start"),
"end": item.get("end"),
"value": item.get("val"),
"filed": item.get("filed"),
"form": item.get("form"),
"fiscal_year": item.get("fy"),
"fiscal_period": item.get("fp"),
"accession": item.get("accn"),
"frame": item.get("frame"),
})
return pd.DataFrame(rows)
revenue = extract_concept(companyfacts, "RevenueFromContractWithCustomerExcludingAssessedTax")
revenue = revenue[revenue["form"].isin(["10-K", "10-Q", "10-K/A", "10-Q/A"])]
Keep the accession number, filing date, form, period boundaries, taxonomy, concept, and unit. Collapsing immediately to fiscal_year -> value destroys information needed to audit duplicates and revisions.
Gotcha 1: XBRL tag drift
Revenue is not always represented by one universal tag. An issuer may use a legacy standard tag, migrate to a newer tag, or report an extension concept. Similar problems occur with cash flow and balance-sheet items.
A durable concept mapping therefore needs:
- Candidate tags for each economic concept.
- Valid units and expected period type.
- Issuer- and date-aware precedence rules.
- Overlap checks when tags coexist.
- QA flags rather than silent guesses.
Do not add every revenue-like tag together. Two tags may be alternative representations of the same total, or one may be a component of another.
Gotcha 2: annual, quarterly, and Q4 values
A fact from a 10-Q may cover a quarter or a year-to-date interval. A 10-K annual fact does not automatically provide a separately tagged fourth quarter.
You can sometimes derive Q4 as annual minus the first three quarters, but only when periods, units, concepts, and accounting scope align. Label derived values explicitly. Per-share values and diluted share counts are especially unsafe to derive through simple subtraction because averaging and weighting matter.
Treat fp, form type, start, end, and duration together. The frame field can help, but it is not present or sufficient for every observation.
Gotcha 3: restatements and duplicate periods
The same fiscal period may appear in an original filing, a later comparative filing, and an amendment. Those values can differ.
For each logical fact, preserve at least:
- the first date it became public;
- the first-reported value;
- later reported values;
- accession and form provenance;
- a revision or restatement indicator.
A latest-only table answers, “What does the record say now?” A point-in-time table answers, “What could have been known on this date?” Those are different datasets.
Gotcha 4: filing lag and lookahead bias
Fiscal period end is not the information-availability date. If a company's year ends in December and its filing arrives later, a backtest must not expose that filing in December.
For an as-of date, filter facts using filing availability:
def as_of_view(df, as_of):
cutoff = pd.Timestamp(as_of)
work = df.copy()
work["filed"] = pd.to_datetime(work["filed"])
work = work[work["filed"] <= cutoff]
keys = ["cik", "concept", "unit", "start", "end"]
return (
work.sort_values([*keys, "filed", "accession"])
.groupby(keys, as_index=False)
.tail(1)
)
This is only a starting point. Same-day treatment, amendments, tag equivalence, duplicate contexts, and filing acceptance timestamps may require an explicit policy. For the broader research issue, see https://tradevodata.com/blog/lookahead-bias-fundamental-backtests?utm_content=blog-sec-edgar-companyfacts-api-tutorial and https://tradevodata.com/blog/point-in-time-fundamentals-data?utm_content=blog-sec-edgar-companyfacts-api-tutorial.
Raw EDGAR versus a normalized API
| Question | Build directly on SEC EDGAR | Use Tradevo Data |
|---|---|---|
| Source | Public SEC filing data | Point-in-time US equity fundamentals from SEC EDGAR |
| Access model | Raw companyfacts and submissions JSON | One normalized JSON endpoint plus Pro bulk downloads |
| Concept mapping | You design and maintain it | 16 annual concepts and 7 quarterly concepts |
| Point-in-time logic | You implement filing-date and revision rules | Server-side as_of, inclusive when first_filed <= as_of |
| Restatements | You identify and retain revisions | original_value, latest_value, restatement flag, and qa_status |
| Coverage | Depends on the pipeline you build | Annual: 5,168 US companies and 632,466 rows; quarterly: 5,382 companies and 1,089,635 rows; no non-US or delisted coverage |
| Formats | SEC JSON and your own storage | JSON API and bulk downloads; Parquet is not included |
| Cost | Public source; engineering and operations remain | Card-backed seven-day trial that renews at $29/month unless canceled |
Tradevo Data is an honest budget tier of research-grade point-in-time data, not the only credible option. Sharadar, Tiingo, and QuantConnect may fit different workflows. See their pricing pages and documentation directly: https://data.nasdaq.com/databases/SF1, https://www.tiingo.com/pricing, and https://www.quantconnect.com/pricing.
When alternatives win
Raw EDGAR wins when you need custom XBRL concepts, filing-level text, bespoke amendment policies, extension-tag research, broader filing coverage, or complete control over lineage. It can also win when maintaining ingestion and normalization is part of your research advantage.
Sharadar, Tiingo, or QuantConnect may win when their coverage, delivery model, integrations, research environment, or supported workflows better match your needs. Verify current features and terms in each provider's documentation and see their pricing pages directly: https://data.nasdaq.com/databases/SF1, https://www.tiingo.com/pricing, and https://www.quantconnect.com/pricing.
Tradevo Data is not a fit if you need non-US or delisted coverage, Parquet delivery, derived TTM values, or concepts outside its supported annual and quarterly sets.
When to build it yourself
Build directly on EDGAR if you can commit to monitoring schema changes, updating mappings, rerunning QA, storing historical revisions, and documenting every point-in-time decision. The source is public; the practical cost is sustained engineering attention.
When a maintained layer is more practical
A normalized service can be useful when your objective is testing research ideas rather than operating XBRL infrastructure. Tradevo Data exposes:
/v1/fundamentals?ticker&as_of[&concept][&period=annual|quarterly]
Annual is the default. Pro provides complete available history, 5,000 requests per day, and bulk access through /v1/download and /v1/snapshot?as_of, each supporting period=annual|quarterly.
Its annual dataset contains 632,466 point-in-time rows across 5,168 US companies, with up to 12 fiscal years and 16 concepts: Revenue, NetIncome, Assets, StockholdersEquity, OperatingCashFlow, EPSDiluted, DilutedShares, GrossProfit, OperatingIncome, PretaxIncome, IncomeTaxExpense, CapitalExpenditures, CashAndCashEquivalents, CurrentAssets, CurrentLiabilities, and NetPPE. Its quarterly dataset contains 1,089,635 rows across 5,382 companies and 7 concepts. Q4 is reported where tagged or derived and labelled where supported; EPS and share counts are not derived for Q4. TTM values are not provided.
Each row includes first_filed, original_value, latest_value, a restatement flag for same-tag changes above 0.5%, including amendments, and qa_status. The dataset labels 37,804 restatements.
Start with the public proof pack: https://github.com/christianpichichero-max/pit-fundamentals. It contains 225 rows covering five companies and their latest three fiscal years, plus the methodology, without signup.
If the schema fits your work, the card-backed seven-day free trial at https://tradevodata.com/?utm_content=blog-sec-edgar-companyfacts-api-tutorial covers 10 companies, a rolling three-year history, and 100 requests per day without bulk. It renews at $29/month unless canceled. Documentation is at https://tradevodata.com/docs?utm_content=blog-sec-edgar-companyfacts-api-tutorial.
Not investment advice.