# 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](https://www.sec.gov/os/accessing-edgar-data) 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`](/docs/monitoring.md) 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`](/docs/usage.md)); 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

```sh
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

```sh
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

```sh
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

```sh
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

```sh
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`](/docs/fetching.md) 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"])
```

## XBRL facts and full-text search

```sh
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`.

```sh
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.