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, the residential network 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), ?name=… and ?isin=…. 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.

Companies listed outside the US#

SEC EDGAR knows companies registered with the SEC, under their US tickers. A Swiss, Danish or Japanese home-market ticker (NESN, NOVO-B, 7203) is not an SEC identifier:

  • Foreign private issuers that file with the SEC (20-F, 40-F, 6-K) are found by the ticker of their US listing or ADR: Novo Nordisk is NVO, Toyota TM, Canadian Natural CNQ. latest returns their 20-F / 40-F by default. Use the ADR ticker, or give the ISIN: ?isin=DK0062498333 finds the issuer (company register, GLEIF) and the SEC filer with its name (NVO).
  • Companies that do not file with the SEC (Nestlé: its ADR NSRGY trades over the counter without SEC registration) answer error: "not_sec_filer" when Cormorant knows the company: a listing on a non-US exchange (NESN.SW or XSWX:NESN), a home-market ticker or ISIN the company register knows. hint says what is known: company (id, name, country, listings, lei, isins), listing (the home listing) and sec_candidates — SEC filers with the company's name; when there is one, the company files through it: retry with that ticker.
  • Otherwise the answer is error: "unknown_identifier": neither SEC nor Cormorant knows the ticker, CIK, name or ISIN.
JSON
{"error": "not_sec_filer",
 "message": "XCSE:NOVO-B is not registered with the SEC (Novo Nordisk A/S); the company files with the SEC as NVO: use that ticker",
 "hint": {"listing": "XCSE:NOVO-B",
          "company": {"id": "cmp_…", "name": "Novo Nordisk A/S", "country": "DK", "listings": ["XCSE:NOVO-B"], "lei": "…", "isins": ["DK0062498333"]},
          "sec_candidates": [{"cik": 353278, "title": "NOVO NORDISK A S", "tickers": ["NVO"], …}]}}

Batch items (POST /v1/batch/sec) answer the same codes and hint, and resolve, submissions, latest and facts items take isin instead of cik or ticker.

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
unknown_identifier Neither SEC nor Cormorant knows this ticker, CIK, name or ISIN. For a company listed outside the US use its ADR ticker or ISIN. Do not retry.
not_sec_filer The company is known but not registered with the SEC under this identifier; hint.sec_candidates names its SEC filing (e.g. the ADR) when there is one. Do not retry as is.
not_found SEC has no such filing or document, or no filing of the requested forms. 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 instant.

Many companies: POST /v1/batch/sec#

For thousands of companies, send one batch job instead of thousands of calls: resolve, latest filing and the documents themselves, mixed in one job. Tickers work instead of CIKs for submissions, latest and facts.

curl
curl -s -X POST https://api.cormorant.ch/v1/batch/sec \
  -H "Authorization: Bearer $CORMORANT_API_KEY" -H "Content-Type: application/json" \
  -d '{"items": [{"op": "latest", "ticker": "TSN"}, {"op": "latest", "ticker": "ZTS"}],
       "idempotency_key": "annual-reports-2026-10"}'
# then: GET /v1/jobs/{job_id}?include=items

A typical 10,000-company run is two jobs: latest per ticker, then document for each primary_document you need. SEC allows 10 requests per second per IP for everyone together, so plan on roughly 10 operations per second at best (an operation reads one or more SEC documents; cached ones do not count against SEC's limit): a 10,000-item latest job takes about 20–40 minutes when nothing is cached. Items wait for the rate limit in the queue, never fail because of it; rate_limited and sec_unavailable are retried automatically.

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, the residential network 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
unknown_identifier neither SEC nor Cormorant knows this ticker, CIK, name or ISIN (a home-market ticker of a non-US company: use its ADR ticker or the ISIN)
not_sec_filer the company is known (a non-US listing such as NESN.SW / XSWX:NESN, the company register, GLEIF for an ISIN) but not registered with the SEC under this identifier; hint: company, listing, sec_candidates (SEC filers with its name, e.g. the ADR)
not_found SEC has no such 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).

Batch: POST /v1/batch/sec#

Up to 50,000 SEC operations as one job of kind sec:

JSON
{
  "items": [
    {"op": "resolve", "ticker": "TSN"},
    {"op": "latest", "ticker": "TSN", "forms": "10-K"},
    {"op": "latest", "cik": 100493},
    {"op": "document", "cik": 100493, "accession": "0000100493-25-000070", "document": "tsn-20250927.htm"}
  ],
  "idempotency_key": "chipiron-run-2026-10-20-sec-0001",
  "callback_url": "https://chipiron.example/hooks/cormorant"
}

op is resolve (ticker, cik or name), submissions (cik or ticker, forms, since, until, limit, history), latest (cik or ticker, forms), facts (cik or ticker), filing (cik, accession) or document (cik, accession, document, max_bytes, accept_pdf, include_html — default false in a batch); project_ref on any item. Identifiers are checked when the job is created (422 invalid_item naming the item). Item results have the shape of the single endpoint (plus op); document results that of POST /v1/sec/documents/fetch. Items share the cluster-wide SEC rate limit with every other SEC request (at most 10 requests/s per egress IP) and are admitted a few at a time: a full limiter or a SEC backoff puts an item back in the queue instead of failing it. Each item is metered once as kind sec (a ticker resolved inside an item is not extra) and counts as fetch for key limits.

View as Markdown