SEC EDGAR via Cormorant
Cormorant gives you company and filing data from SEC EDGAR — tickers and CIKs, filing histories, annual reports, filing documents, XBRL facts and full-text search — without running your own SEC client. It follows SEC's fair-access rules for you:
- Cormorant declares its own identity. SEC requires every client to send
a
User-Agentnaming the organisation and a contact address. Cormorant sends the operator's identity on every SEC request. Do not send your own User-Agent: for SEC hosts auser_agentoraccept_languageinPOST /v1/fetchis ignored (and does not need to be on your project's allowlist). - One shared rate limit. SEC allows at most 10 requests per second per
IP address. All Cormorant clients share one limit (8 requests/s by
default) per egress IP; a request waits up to 10 s for a slot, then answers
error: "rate_limited"withretry_after_s. - Shared backoff. When SEC throttles (HTTP 403, 429 or 503), every SEC
request of that egress pauses — 10 s, doubling up to 10 minutes — and
answers
error: "sec_unavailable"withretry_after_swithout contacting SEC. Theseccomponent ofGET /v1/statusshowsbackoff_activeandthrottle_wait_p95_ms. - Direct only. SEC is never reached through the browser, archived copies, paid proxies or alternative sources. robots.txt is honoured and nothing is crawled: each call reads exactly the SEC documents it needs.
- Cached. Filings never change, so filing documents and indexes are
cached for a year;
company_tickers.json, submissions and XBRL facts for 12 hours (then revalidated with conditional requests); full-text search for an hour. Cached answers are fast and do not count towards SEC's limit.
SEC requests are metered as their own usage kind sec (categories sec
and sec_cached in GET /v1/usage); they cost Cormorant
nothing upstream. Your project's rate limit still applies, and a key's
fetch quota counts them.
All examples use $CORMORANT for the base URL and $KEY for your key.
Resolve a company#
curl -s "$CORMORANT/v1/sec/companies/resolve?ticker=TSN" -H "Authorization: Bearer $KEY"
{"cik": 100493, "cik_padded": "0000100493", "title": "TYSON FOODS, INC.",
"tickers": ["TSN"], "exchanges": ["NYSE"], "ambiguous": false, "candidates": [...],
"error": null, "cached": true, "source_url": "https://www.sec.gov/files/company_tickers.json"}
Also ?cik=100493 (or 0000100493) and ?name=…. BRK.B and BRK-B are
the same ticker. A name that matches several companies answers
ambiguous: true with the matches in candidates (and cik: null); pick
one and continue with its CIK.
List filings#
curl -s "$CORMORANT/v1/sec/companies/100493/submissions?forms=10-K,10-K/A&since=2015-01-01&limit=20" \
-H "Authorization: Bearer $KEY"
filings are newest first, each with accession, form, filing_date,
report_date, primary_document, primary_document_url, index_url and
is_amendment (plus SEC's other columns). Cormorant also reads the pages of
older filings that SEC lists under files — only those that can contain
filings after since (history=false skips them). filings_total counts
the matches before limit; the raw SEC JSON is at body_url.
The latest annual report#
curl -s "$CORMORANT/v1/sec/companies/1094517/filings/latest" -H "Authorization: Bearer $KEY"
By default the newest 10-K, 20-F (foreign private issuers) or 40-F
(Canadian issuers). Amendments are included only when you ask for them:
?forms=10-K,10-K/A returns a newer 10-K/A with is_amendment: true.
The documents of a filing#
curl -s "$CORMORANT/v1/sec/filings/100493/0000100493-25-000070" -H "Authorization: Bearer $KEY"
Accession numbers work with or without dashes. documents lists every file
with type (10-K, EX-21, EX-97, EX-101.SCH, …), description,
size and url.
Fetch a document#
curl -s "$CORMORANT/v1/sec/documents/fetch" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"cik": 100493, "accession": "0000100493-25-000070", "document": "tsn-20250927.htm",
"include_html": false}'
The answer is exactly a POST /v1/fetch result — text,
pdf_text_pages for PDFs, content_kind, sha256 and body_url for the
exact bytes — so you can keep your artifact handling. Only documents of the
given filing can be fetched. You can also pass an SEC URL to
POST /v1/fetch or a batch job directly; the same SEC rules apply.
import httpx
client = httpx.Client(base_url=CORMORANT, headers={"Authorization": f"Bearer {KEY}"}, timeout=180)
cik = client.get("/v1/sec/companies/resolve", params={"ticker": "ZTS"}).json()["cik"]
latest = client.get(f"/v1/sec/companies/{cik}/filings/latest").json()
if latest["error"] is None:
doc = client.post(
"/v1/sec/documents/fetch",
json={
"cik": cik,
"accession": latest["accession"],
"document": latest["primary_document"],
"include_html": False,
},
).json()
print(latest["form"], latest["report_date"], len(doc["text"]), doc["body_url"])
XBRL facts and full-text search#
curl -s "$CORMORANT/v1/sec/companies/100493/facts" -H "Authorization: Bearer $KEY"
curl -s "$CORMORANT/v1/sec/search?q=%22avian%20influenza%22&forms=10-K&dateRange=5y" \
-H "Authorization: Bearer $KEY"
facts summarises the company's XBRL facts (concepts per taxonomy); the
complete JSON — often several megabytes — is at body_url. search is
EDGAR full-text search (filings since 2001): total and up to 100 hits
per page (from for the next page), each with accession, ciks, form,
the matching document and its url. Dates: dateRange = all, 10y,
5y, 1y, 30d, or startdt/enddt.
Errors#
Every SEC answer is HTTP 200; check error:
error |
What to do |
|---|---|
not_found |
SEC has no such company, filing or document. Do not retry. |
invalid_identifier |
Fix the CIK, accession number, document name, form type or date (message says which). |
rate_limited |
Retry after retry_after_s. Batches retry automatically. |
sec_unavailable |
SEC is throttling or failing: retry after retry_after_s, not earlier — the answer would be the same. |
sec_not_configured |
SEC access is not enabled on this deployment; ask the operator. |
Spread large jobs over time and prefer batch jobs: SEC's limit is shared by everyone, and cached answers are free and instant.
Company and filing data from SEC EDGAR. Cormorant sends its own declared
identity to SEC ("<Org name> <contact email>", set by the operator) and
keeps all clients together under SEC's fair-access limit: one cluster-wide
rate limit per egress IP (default 8 requests/s, never more than 10), queued
for up to 10 s, and a shared backoff when SEC throttles (403/429/503). Only
the direct tier is used — never the browser, archived copies, paid proxies or
alternative sources. Do not send your own User-Agent for SEC: a
user_agent/accept_language in POST /v1/fetch is ignored for SEC hosts.
The same policy applies to POST /v1/fetch and batch items for URLs on
www.sec.gov, data.sec.gov and efts.sec.gov.
All endpoints need an API key, count towards the project's rate limit and
are metered as kind sec (no provider cost). Answers are HTTP 200 also when
they failed; check error:
error |
Meaning |
|---|---|
not_found |
SEC has no such company, filing or document (also: no filing of the requested forms) |
invalid_identifier |
a CIK, accession number, document name, form type, date or query is malformed (message says which); nothing was requested from SEC |
rate_limited |
no slot in the shared SEC rate limit within 10 s; retry after retry_after_s |
sec_unavailable |
SEC throttles (all SEC requests back off) or failed; retry after retry_after_s |
sec_not_configured |
SEC is not enabled on this deployment |
timeout, too_large, robots_disallowed, … |
the fetch errors |
Identifiers: CIKs as a number or 10 digits (100493, 0000100493);
accession numbers with or without dashes (0000100493-25-000070,
000010049325000070); form types comma-separated (10-K,20-F,40-F,10-K/A).
Only the SEC URL patterns behind each endpoint can be requested.
Every answer has error, message, retry_after_s, cached (true when
all SEC documents came from Cormorant's cache), retrieved_at (oldest
document), source_url (the main SEC document), body_url and sha256
(its exact bytes, e.g. the raw submissions JSON) and request_id.
Caching. Filing documents and indexes (/Archives/edgar/data/{cik}/{accession}/…)
are immutable and cached for 365 days (revalidated only when you ask with
cache_ttl_s); company_tickers.json, submissions and XBRL facts for 12 h,
then revalidated with conditional requests; full-text search for 1 h.
GET /v1/sec/companies/resolve?ticker=TSN#
Also ?cik=100493 or ?name=tyson foods (exactly one). From
company_tickers.json and company_tickers_exchange.json:
{"cik": 100493, "cik_padded": "0000100493", "title": "TYSON FOODS, INC.",
"tickers": ["TSN"], "exchanges": ["NYSE"], "ambiguous": false,
"candidates": [{"cik": 100493, "cik_padded": "0000100493", "title": "TYSON FOODS, INC.", "tickers": ["TSN"], "exchanges": ["NYSE"]}],
"error": null, "cached": true, "source_url": "https://www.sec.gov/files/company_tickers.json", "…": "…"}
BRK.B and BRK-B are the same ticker. A name matches exact titles first,
then titles starting with or containing it; when several companies match,
ambiguous is true, the company fields are null and candidates lists
up to 10. A CIK without a listed ticker is resolved from its submissions.
GET /v1/sec/companies/{cik}/submissions#
Query: forms, since, until (filing dates, YYYY-MM-DD), limit
(default 100, max 10,000), history (default true: also read the pages of
older filings SEC lists under files, only those that can hold filings
after since). Company fields cik, cik_padded, name, title,
tickers, exchanges, entity_type, sic, sic_description, category,
fiscal_year_end, state_of_incorporation, ein, lei, website,
phone, business_address, former_names; filings (newest first, at most
limit), filings_total, truncated, and files (name, filing_count,
filing_from, filing_to, url, fetched, body_url). Each filing:
| Field | From SEC's column |
|---|---|
accession |
accessionNumber |
form, filing_date, report_date, acceptance_datetime |
form, filingDate, reportDate, acceptanceDateTime |
act, file_number, film_number, items, core_type, size |
act, fileNumber, filmNumber, items, core_type, size |
is_xbrl, is_inline_xbrl |
isXBRL, isInlineXBRL (as booleans) |
primary_document, primary_document_description |
primaryDocument, primaryDocDescription |
primary_document_url, index_url |
built: the document and the -index.htm page (null URL when the primary document is in a sub-folder, e.g. ownership forms) |
is_amendment |
built: the form ends in /A |
GET /v1/sec/companies/{cik}/filings/latest?forms=10-K,20-F,40-F#
The newest filing (by filing date) of forms — default 10-K,20-F,40-F
(annual reports; amendments only when you list them, e.g. 10-K,10-K/A):
the filing fields above at the top level (accession, form,
filing_date, report_date, primary_document, primary_document_url,
index_url, is_amendment, …) plus cik, cik_padded, name, tickers,
exchanges. Older filing pages are searched when the recent list has none;
none at all: not_found.
GET /v1/sec/filings/{cik}/{accession}#
The filing index: form, filing_date, report_date, accepted,
company, is_amendment, index_url, folder_url, accession,
accession_nodash, index_source (index_htm, or index_json when only the
folder listing has the documents) and documents: sequence,
description, document (file name), type (10-K, EX-21, EX-101.SCH,
…), size, url. Sub-folders are not listed.
POST /v1/sec/documents/fetch#
{"cik": 100493, "accession": "0000100493-25-000070", "document": "tsn-20250927.htm",
"max_bytes": 52428800}
Fetches one document of a filing (HTML/iXBRL, PDF, exhibit, XML) and answers
exactly like POST /v1/fetch (text, pdf_text_pages, content_kind,
body_url, sha256, body_kind: origin, …). Optional: include_html
(default true), accept_pdf, cache_ttl_s, timeout_s, project_ref.
Invalid identifiers answer error: invalid_identifier in that shape.
GET /v1/sec/companies/{cik}/facts#
A summary of data.sec.gov/api/xbrl/companyfacts/CIK##########.json:
cik, cik_padded, name, taxonomies (concepts per taxonomy),
concepts, bytes; the complete JSON is at body_url.
GET /v1/sec/search?q=…&forms=…&dateRange=…#
EDGAR full-text search (filings since 2001). Query: q (EDGAR syntax),
forms, dateRange (all, 10y, 5y, 1y, 30d, or custom with
startdt/enddt), from (offset, pages of 100). Answer: query, total,
total_relation, hits (accession, ciks, display_names, form,
root_forms, file_type, file_description, file_date, period_ending,
document, url, index_url, score).