CormorantDocs
Menu

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-Agent naming 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 a user_agent or accept_language in POST /v1/fetch is 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" with retry_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" with retry_after_s without contacting SEC. The sec component of GET /v1/status shows backoff_active and throttle_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
curl -s "$CORMORANT/v1/sec/companies/resolve?ticker=TSN" -H "Authorization: Bearer $KEY"
JSON
{"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
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
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
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
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.

Python
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"])
curl
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:

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#

JSON
{"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).

View as Markdown