SEC EDGAR API Guide
SEC EDGAR API Guide: XBRL CompanyFacts, Rate Limits & Python Examples
The SEC's EDGAR system exposes a set of free, public APIs for programmatic access to financial data. The most powerful of these is the XBRL CompanyFacts endpoint, which returns every structured fact a company has ever filed — revenues, total assets, shares outstanding, and hundreds more — in a single JSON response.
This guide covers the EDGAR XBRL API endpoints, the 10 requests-per-second rate limit, the mandatory User-Agent requirement, and practical Python examples for fetching and parsing CompanyFacts data. Whether you're building a financial model, a data pipeline, or training an LLM on SEC data, these are the constraints and patterns you need to know. Platforms like DealCharts build on these same EDGAR data sources to provide structured, citable charts for structured finance deals.
EDGAR API Quick Reference
Before diving into details, here's a quick-reference table for the primary EDGAR data endpoints and access rules:
| Resource | URL Pattern | Returns |
|---|---|---|
| CompanyFacts | data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json | All XBRL facts for a company |
| CompanyConcept | data.sec.gov/api/xbrl/companyconcept/CIK{cik}/{taxonomy}/{tag}.json | Single concept time series |
| Submissions | data.sec.gov/submissions/CIK{cik}.json | Filing history and metadata |
| Company Tickers | sec.gov/files/company_tickers.json | Ticker-to-CIK lookup table |
| Rate Limit | 10 requests/second per IP | — |
| User-Agent | Required: CompanyName admin@email.com | — |
All endpoints are free and require no API key. The only access requirements are a compliant User-Agent header and staying within the rate limit.
CompanyFacts Deep Dive
The CompanyFacts endpoint is the single most useful EDGAR API for bulk financial data. A single request returns every XBRL-tagged fact across all filings for a given CIK.
Example: Apple Inc. (CIK 0000320193)
The URL for Apple's CompanyFacts is:
https://data.sec.gov/api/xbrl/companyfacts/CIK0000320193.json
The response is a JSON object with this top-level structure:
{"cik": 320193,"entityName": "Apple Inc.","facts": {"dei": { ... },"us-gaap": {"Revenues": {"label": "Revenues","description": "Amount of revenue recognized...","units": {"USD": [{"end": "2023-09-30","val": 383285000000,"accn": "0000320193-23-000106","fy": 2023,"fp": "FY","form": "10-K","filed": "2023-11-03"}]}},"Assets": { ... },"StockholdersEquity": { ... }}}}
Key things to notice:
- facts contains two namespaces: dei (document and entity info) and us-gaap (financial data)
- Each concept (e.g., Revenues) has a units object — usually USD for monetary values or shares for share counts
- Each data point includes the filing accession number (accn), fiscal year (fy), period (fp), and the form type
- Values (val) are in raw units — Apple's FY2023 revenue is 383,285,000,000 (not thousands or millions)
CompanyConcept: Fetching a Single Metric
If you only need one concept (e.g., revenue over time), the CompanyConcept endpoint is more efficient:
https://data.sec.gov/api/xbrl/companyconcept/CIK0000320193/us-gaap/Revenues.json
This returns the same data structure as the Revenues section of CompanyFacts, without the overhead of every other concept.
Example: Amazon.com (CIK 0001018724)
Amazon is one of the most-searched CIKs on EDGAR. The CompanyFacts URL is:
https://data.sec.gov/api/xbrl/companyfacts/CIK0001018724.json
The response follows the same structure as Apple above. To pull Amazon's annual revenue:
amazon = get_company_facts("1018724")revenues = extract_annual_values(amazon, "Revenues")for r in revenues:print(f"FY{r['fy']}: ${r['val']:,.0f}")
Note the CIK zero-padding:
becomes1018724
in the URL. TheCIK0001018724
function below handles this automatically. You can also fetch a single concept directly atget_company_facts
.data.sec.gov/api/xbrl/companyconcept/CIK0001018724/us-gaap/Revenues.json
Python Examples
Setting Up Compliant Headers
Every request to EDGAR must include a User-Agent header identifying your application and providing a contact email. Requests without this header receive a 403 Forbidden response.
import requestsimport timeHEADERS = {"User-Agent": "MyCompany Analytics admin@mycompany.com"}SEC_BASE = "https://data.sec.gov"
Fetching CompanyFacts
def get_company_facts(cik: str) -> dict:"""Fetch all XBRL facts for a company by CIK (zero-padded to 10 digits)."""cik_padded = cik.zfill(10)url = f"{SEC_BASE}/api/xbrl/companyfacts/CIK{cik_padded}.json"resp = requests.get(url, headers=HEADERS, timeout=30)resp.raise_for_status()return resp.json()# Apple's CIKfacts = get_company_facts("320193")print(facts["entityName"]) # "Apple Inc."
Extracting a Specific Concept
Once you have the CompanyFacts response, extracting a time series for a specific concept is straightforward:
def extract_annual_values(facts: dict, concept: str, taxonomy: str = "us-gaap") -> list:"""Extract annual (10-K) values for a concept from CompanyFacts data."""try:concept_data = facts["facts"][taxonomy][concept]except KeyError:return []# Get USD values, fall back to first available unitunits = concept_data.get("units", {})values = units.get("USD", units.get("shares", []))# Filter to annual filings onlyannual = [v for v in values if v.get("form") == "10-K" and v.get("fp") == "FY"]# Sort by fiscal yearannual.sort(key=lambda x: x.get("fy", 0))return annualrevenues = extract_annual_values(facts, "Revenues")for r in revenues:print(f"FY{r['fy']}: ${r['val']:,.0f}")
Rate-Limited Batch Fetching
When fetching data for multiple companies, you must stay under the 10 requests-per-second limit:
def batch_fetch_facts(ciks: list) -> dict:"""Fetch CompanyFacts for multiple CIKs with rate limiting."""results = {}for cik in ciks:try:results[cik] = get_company_facts(cik)print(f"Fetched {results[cik]['entityName']}")except requests.exceptions.HTTPError as e:print(f"Error fetching CIK {cik}: {e}")results[cik] = None# Stay under 10 req/s — 0.12s buffer is safer than exact 0.1stime.sleep(0.12)return results# Example: fetch facts for a few companiescompany_ciks = ["320193", "789019", "1018724"] # Apple, Microsoft, Amazonall_facts = batch_fetch_facts(company_ciks)
CIK Lookup from Ticker
The SEC publishes a ticker-to-CIK mapping file that you can use to convert ticker symbols to CIK numbers:
def get_cik_from_ticker(ticker: str) -> str:"""Look up a CIK number from a ticker symbol."""url = "https://www.sec.gov/files/company_tickers.json"resp = requests.get(url, headers=HEADERS, timeout=30)resp.raise_for_status()tickers = resp.json()ticker_upper = ticker.upper()for entry in tickers.values():if entry["ticker"].upper() == ticker_upper:return str(entry["cik_str"])raise ValueError(f"Ticker '{ticker}' not found in SEC data")# Exampleapple_cik = get_cik_from_ticker("AAPL")print(f"AAPL CIK: {apple_cik}") # "320193"
Note that CIK numbers must be zero-padded to 10 digits when used in API URLs. The get_company_facts function above handles this automatically with zfill(10).
For a deeper dive on mapping CUSIPs to CIKs — especially important in structured finance where a single issuer may have hundreds of securities — see our CUSIP to CIK Mapping Guide.
Common Errors and How to Fix Them
403 Forbidden — Missing or Invalid User-Agent
The most common error developers encounter. EDGAR blocks requests that don't include a User-Agent header identifying the caller.
Fix: Set the User-Agent header to a string containing your organization name and a contact email:
headers = {"User-Agent": "YourCompany analytics@yourcompany.com"}
429 Too Many Requests — Rate Limit Exceeded
You've exceeded the 10 requests-per-second limit. The SEC will temporarily block your IP.
Fix: Add a minimum delay of 0.1 seconds between requests. Use 0.12 seconds for safety. For burst-heavy workloads, implement exponential backoff:
def fetch_with_backoff(url, retries=3):for attempt in range(retries):resp = requests.get(url, headers=HEADERS, timeout=30)if resp.status_code == 429:wait = 2 ** attemptprint(f"Rate limited. Waiting {wait}s...")time.sleep(wait)continueresp.raise_for_status()return resp.json()raise Exception(f"Failed after {retries} retries")
For a detailed walkthrough of building resilient EDGAR scrapers, see our guide on EDGAR Scraping Rate Limits Explained.
Connection Reset — Sustained Rate Limit Violation
If you sustain a high request rate, EDGAR may reset the TCP connection entirely rather than returning a 429. This manifests as a ConnectionResetError or requests.exceptions.ConnectionError.
Fix: Back off immediately and reduce your request rate. Wait at least 10 seconds before retrying.
Empty or Unexpected Response — CIK Zero-Padding
EDGAR is strict about CIK formatting in URLs. The CIK must be zero-padded to exactly 10 digits.
| URL | Result |
|---|---|
| CIK0000320193.json | Works (Apple) |
| CIK320193.json | May fail or return unexpected data |
| CIK0001018724.json | Works (Amazon) |
Fix: Always use str(cik).zfill(10) when constructing API URLs.
Related EDGAR Resources
- EDGAR Scraping Rate Limits Explained — Detailed guide to building compliant data pipelines within the SEC's 10 req/s limit
- CUSIP to CIK Mapping Guide — Programmatic mapping between security identifiers and SEC filing entities
- EDGAR 10-D API for ABS — Accessing asset-backed securities surveillance filings via EDGAR
- How to Cite SEC Filings — Citation formats for EDGAR filings in research and models
- Programmatic CIK-CUSIP Mapping with Python — Python workflows for entity resolution across SEC identifiers
For an example of how EDGAR CompanyFacts data connects to real deals, explore a structured finance deal like BBCMS 2023-C21 — where filing data, tranches, and collateral are linked with provenance.