CorpWatch (parsing SEC EDGAR Form 10-K Exhibit 21)
US-listed-issuer subsidiary structure - ~606K entities, ~1.22M edges (2003→)
Source
Feed (what to expect)
Periodic free bulk tar.gz (no auth); live-fetched, re-pull each refresh.
Quirks
Reconciliation
Schema
Feeds (axes)
Record kinds
Identifier schemes
Phase 2 desk landscape, written 2026-05-06. Reference document for the SEC EDGAR Exhibit 21 fetcher built against the v1 schema + ADR 010 lineage substrate. Cross-references:
docs/adr/010-lineage-records-share-canonical-pipeline.md,docs/LINEAGE.md,docs/DATASOURCES.md,docs/phases/02-multi-jurisdictional-and-lineage.md.
Live-data observations (verified 2026-05-07 against the Sept-2025 dump on disk at
tmp/sources/corpwatch.org/corpwatch_api_tables_csv/). Numbers here supersede earlier 2024-era estimates from the CorpWatch README and the OpenSanctions metadata - the actual dump is 2.3-2.9× larger across the board.
- The canonical bulk URL has moved. The original
http://api.corpwatch.org/documentation/db_dump/corpwatch_api_tables_csv.tar.gzreturns 404. The Internet Archive mirror is the live source:https://archive.org/download/corpwatch_api_data_dumps/corpwatch_api_tables_csv.tar.gz
1,598,109,530 B (~1.5 GB compressed; ~7.6 GB uncompressed across the CSV set). OpenSanctions'
us_corpwatchmetadata confirms this as the official canonical source. Download from archive.org is slow (sustained ~hundreds of KB/s); the FF fetcher supports a--bulk-source <path>flag that points at a pre-extracted directory so dev cycles don't re-download.
Vintage anchored on
meta.csv:update_date. The on-disk dump reportsupdate_date: 2025-09-13inmeta.csv, withprevious_update: 1755327600(= 2025-08-16) andbegin_date: 2003-1-1. Vintage =2025-09-13for the FF fetcher. (The archive.org "Last-Modified" header is later, but the canonical content date is whatmeta.csvrecords.)Coverage is far larger than the README suggests. Verified row counts on disk:
Table Rows Bytes What it is companies.csv1,421,198 90 MB one row per cw_id; 749K filer entities (have CIK) + 672K cw_id-only stubscompany_relations.csv3,517,388 184 MB distilled (parent_cw_id, child_cw_id, year) edge table relationships.csv4,821,343 755 MB per-filing-quarter raw observations; carries percent,parse_method,hierarchy,cikcompany_locations.csv2,622,964 349 MB address rows with country_code,subdiv_code,most_recentflagfilers.csv7,625,247 2.6 GB per-(filer_id, filing_id) row; carries incorp_country_code+incorp_subdiv_codecompany_names.csv2,435,331 250 MB name variants - not ingested in A.5 filings.csv22,731,743 3.2 GB every SEC filing, most with has_sec21=0- not ingestedmeta.csv5 100 B dump metadata (update_date, begin_date, …) Vulcan Materials Co (cw_id=1896, cik=103973), MP Materials, Rio Tinto, Newmont, Freeport-McMoRan, SanDisk Corp (cw_id=11775) are all canonically present with full subsidiary trees.
relation_typeis 100% NULL incompany_relations.csv- every one of the 3.5M rows. The CorpWatch schema reserves the column to distinguish edge kinds (e.g. ownership-percent vs parent-control), but the September-2025 dump does not populate it. A.5 emitskind="controls"for every distilled edge. Stake percentages live onrelationships.csv.percent(the per-filing-quarter raw observations), aggregating parent_cw_id × cw_id × year → max-percent is the path for the deferred A.5.6owns_shareenrichment.Direction convention (verified by empirical join). A sample row in
company_relations.csvhasrelation_id=17127, source_cw_id=11775, target_cw_id=72185pointing torelationships.relationship_id=8whereparent_cw_id=11775, cw_id=72185. So:source_cw_id= parent (filer side);target_cw_id= subsidiary (the entity listed in the parent's Exhibit 21). FF's lineageRelationshipdirection convention is the opposite (from_entity= subsidiary,to_entity= parent), so the fetcher swaps source/target on emit:from_entity = "<scheme>:<target_cw_id>",to_entity = "<scheme>:<source_cw_id>". This is the easiest error to get backwards - fixture-test it explicitly.Jurisdiction has a clear precedence.
filers.csvcarriesincorp_country_code+incorp_subdiv_codefor filer entities (the gold-standard "state of incorporation" signal: 4.35M US, 287K KY, 67K CA, 31K GB, 30K VG, 22K LU, 17K BM - classic offshore-corporate distribution).company_locations.csvrows wheremost_recent=1provide the fallback for cw_id-only stubs that don't appear infilers.csv. DefaultZZwhen neither resolves.
This is the reference for the v1 SEC EDGAR (via CorpWatch) fetcher:
lineage-side only (no facility-side records emitted). Per ADR 010,
each companies.csv row → one Entity; each company_relations.csv
row → one ownership_relationship with kind="controls" (the
relation_type=NULL reality means we don't conditionally emit
owns_share in A.5; that becomes the deferred A.5.6 enrichment via
relationships.csv.percent). The fetcher writes exclusively to the
resolved tier's entity/source_id=sec_edgar_corpwatch/ and
ownership_relationship/source_id=sec_edgar_corpwatch/ partitions.
Publisher. CorpWatch - a US accountability non-profit. CorpWatch parses Exhibit 21 attachments from public companies' 10-K filings on SEC EDGAR and republishes the structured parent/subsidiary edges as a bulk dump. The underlying SEC EDGAR filings are public-domain federal data (17 U.S.C. § 105); CorpWatch's parser code is MIT-licensed.
Why via CorpWatch and not directly from SEC. SEC publishes the
underlying 10-K filings but does not publish a pre-parsed Exhibit
21 dataset. Each filing's Exhibit 21 is a free-form HTML or text
attachment - extracting the structured parent/subsidiary list
requires regex parsing of heterogeneous formats. CorpWatch has been
doing this work since 2003; doing it ourselves would be reinventing
the wheel for marginal fidelity gain. See the four-source group in
LINEAGE.md for context - CorpWatch direct is the
default; the OpenSanctions republish is a sibling path under
CC-BY-NC; Wharton's WRDS Company Subsidiary Data is the aspirational
higher-fidelity alternative pending institutional access.
License posture.
Provenance.fetched(source_id="sec_edgar_corpwatch", ...) on every
emitted Entity + Relationship - the source-id prefix names both the
upstream-of-upstream (SEC EDGAR) and the parser provider.Coverage. US-listed registrants that file 10-Ks containing an Exhibit 21:
One tarball:
corpwatch_api_tables_csv.tar.gz - 1.5 GB compressed, expands
to ~7.6 GB of CSV. Sourced from
https://archive.org/download/corpwatch_api_data_dumps/.Inside the tarball, 18 CSVs. The four we ingest in A.5 are bolded; the rest are either consulted opportunistically or ignored.
| File | Rows | Used by A.5? | Purpose |
|---|---|---|---|
companies.csv | 1.42 M | emit | one row per cw_id; the entity universe |
company_relations.csv | 3.52 M | emit | distilled (parent_cw_id, child_cw_id, year) edges |
filers.csv | 7.63 M | lookup | incorp_country_code / incorp_subdiv_code for filer entities (jurisdiction) |
company_locations.csv | 2.62 M | lookup | fallback jurisdiction for non-filer entities (rows where most_recent=1) |
meta.csv | 5 | metadata | update_date → vintage |
relationships.csv | 4.82 M | deferred | per-filing-quarter raw observations with percent - A.5.6 enrichment |
company_names.csv | 2.44 M | not ingested | name variants - Phase 3 cross-source identity work |
company_info.csv | 5.20 M | not ingested | per-(cw_id, year) snapshot - superseded by the lookups above |
cw_id_lookup.csv | 2.33 M | not ingested | alternate-form lookup; covered by filers + locations |
cik_name_lookup.csv | 1.00 M | not ingested | name variations per CIK; consumed by Phase 3 entity matching |
company_filings.csv | 208 K | not ingested | per-(cw_id, filing_id) join with sec_21_url; provenance back-link |
filings_lookup.csv | 3.80 M | not ingested | (cw_id, filing_id, company_is_filer) |
filings.csv | 22.73 M | not ingested | every SEC filing; most have has_sec21=0 |
sic_codes.csv + 4 reference | 1.8 K | not ingested | SIC + UN country/subdivision references - covered by FF's NAICS/ISIC/ISO-3166 |
For each admitted row the parser emits:
From companies.csv → one Entity per row (1.42M total):
entity_id = "sec_cik:<cik>" when cik != 0 (~749K entities; the
filer side); entity_id = "corpwatch_cw_id:<cw_id>" otherwise
(~672K cw_id-only stubs from Exhibit 21 disclosures). CIK-anchored
entities get confidence=MEDIUM; cw_id-only stubs get
confidence=LOW (subsidiaries inferred only from a parent's
EX-21, no independent SEC filing).canonical_name = company_name (CorpWatch's standardised name,
upper-case for filers, mixed-case for relationship-derived stubs).jurisdiction - resolved via the precedence in the _jurisdiction
helper:
filers.incorp_country_code /
filers.incorp_subdiv_code (preferring the most-recent filing
per cw_id).company_locations row with
most_recent=1 for the cw_id, using country_code /
subdiv_code.ZZ when neither resolves.lei - none; CorpWatch doesn't emit LEIs (GLEIF L2 will join LEIs
in via a separate fetcher in A.6).entity_type = "corporation" default; "natural_person" is not
used (CorpWatch only carries companies, not directors).provenance - Provenance.fetched(source_id="sec_edgar_corpwatch", source_url="https://archive.org/download/corpwatch_api_data_dumps/corpwatch_api_tables_csv.tar.gz", fetch_date=...).From company_relations.csv → one lineage Relationship per row
(3.52M total):
source_cw_id is the parent;
target_cw_id is the subsidiary (verified empirically via the
relationships.csv join - see live-data observation #5 above).
FF's convention is the opposite, so:
from_entity = "<scheme>:<target_cw_id-resolved>" (the subsidiary)
to_entity = "<scheme>:<source_cw_id-resolved>" (the parent)
Each cw_id resolves to its CIK-anchored or cw_id-stub entity_id via
the companies.csv lookup built during Entity emission.kind = "controls" for every row in this dump. The
company_relations.csv relation_type column is 100% NULL, so
there is no signal to discriminate owns_share. Stake-percent
enrichment via relationships.csv.percent lands in the deferred
A.5.6 follow-up.stake_percent = None (deferred to A.5.6).valid_from = date(year, 1, 1) from the year column on the
relation row. Single-year resolution - CorpWatch doesn't anchor
edges to filing dates with finer granularity.valid_to - left null. CorpWatch doesn't track when subsidiaries
stop being subsidiaries; absence in a later filing is the
signal, but A.5 doesn't materialise that.confidence = MEDIUM - the edge comes from a SEC-filed Exhibit 21
disclosure (anchored), with a small-but-real regex-parser fidelity
caveat (see §"Quirks" below). The combine-with-LOW-stub-Entity →
the resulting traversal hops through these edges still report
weakest-link composite tiers honestly.relationship_id = mint_relationship_id() - fresh ULID; CorpWatch
relation_id is preserved as a join key only via provenance, not
as the FF-side identifier.provenance - Provenance.fetched(source_id="sec_edgar_corpwatch", source_url=..., fetch_date=...). Per-edge link back to a specific
SEC filing accession is reachable via
(parent_cw_id, cw_id, year) → relationships.csv.filing_id → filings.filename; deferred to A.5.6 when we'd want to surface that.Not emitted in A.5:
relationships.csv per-filing-quarter raw observations - collapsed
into company_relations.csv already; A.5.6 will revisit when we
pull stake percentages.company_locations.csv rows beyond the jurisdiction lookup - the
per-cw_id address detail isn't part of the lineage record shape.company_names.csv - variant names; consumed by Phase 3
cross-source entity identity work.filings.csv / company_filings.csv / filings_lookup.csv -
provenance back-link reference only; not emitted as records.cw_id for them.
Cross-source matching against MSHA / MRDS owner names happens via
name + jurisdiction match (Phase 3 entity-identity work) - or via
name + parent-CIK as a stronger anchor.NOTE: Some groups of companies may form circular loops due to peculiarities of their filings (Same companies listed as parents and children), or because multiple CIKs refer to the same filing. The walk_ownership traversal already handles this -
cycleterminus.
The expected wins from adding CorpWatch to the lineage substrate:
msha_legal_entity_id:0071891 ("Vulcan Materials Company") gets
matched (Phase 3) to CorpWatch's sec_cik:0001396009 for the
same company - promoting the entity from confidence=LOW (stub)
to confidence=MEDIUM (CIK-anchored).