KYB checks, CRM enrichment and filing monitors all start with the same question: given a company name or ticker, who exactly is this legal entity, and what has it filed? Two free, official sources answer most of it:
- GLEIF (the Legal Entity Identifier registry): legal name, jurisdiction, addresses, status and parent companies, worldwide.
- SEC EDGAR: CIK, industry code, every filing, and XBRL financials for US filers.
Neither needs an API key. SEC only asks for a User-Agent with your contact and fewer than 10 requests per second.
DIY: one company in about 30 lines
# pip install requests
import requests
UA = {"User-Agent": "Your Name your.email@example.com"} # SEC requires a real contact
def gleif(name):
r = requests.get("https://api.gleif.org/api/v1/lei-records",
params={"filter[entity.legalName]": name, "page[size]": 1}, timeout=20)
rec = r.json()["data"][0]
e = rec["attributes"]["entity"]
return {"lei": rec["id"], "legalName": e["legalName"]["name"],
"jurisdiction": e["jurisdiction"], "status": e["status"],
"hqCity": e["headquartersAddress"]["city"]}
def cik_for(ticker):
data = requests.get("https://www.sec.gov/files/company_tickers.json", headers=UA, timeout=20).json()
for row in data.values():
if row["ticker"] == ticker.upper():
return f'{row["cik_str"]:010d}'
def sec(cik, forms=("10-K", "10-Q", "8-K"), n=3):
sub = requests.get(f"https://data.sec.gov/submissions/CIK{cik}.json", headers=UA, timeout=20).json()
rec = sub["filings"]["recent"]
filings = [{"form": f, "filedAt": d} for f, d in zip(rec["form"], rec["filingDate"]) if f in forms][:n]
facts = requests.get(f"https://data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json", headers=UA, timeout=20).json()
ni = [x for x in facts["facts"]["us-gaap"]["NetIncomeLoss"]["units"]["USD"] if x.get("form") == "10-K" and x.get("fp") == "FY"]
latest = max(ni, key=lambda x: x["end"])
return {"name": sub["name"], "sic": sub["sicDescription"], "filings": filings,
"netIncome": (latest["fy"], latest["end"], latest["val"])}
print(gleif("Apple Inc."))
cik = cik_for("AAPL")
print(cik, sec(cik))
Output (28/10/2026):
{'lei': 'HWUPKR0MPOU8FGXBT394', 'legalName': 'Apple Inc.', 'jurisdiction': 'US-CA', 'status': 'ACTIVE', 'hqCity': 'Cupertino'}
0000320193 {'name': 'Apple Inc.', 'sic': 'Electronic Computers', 'filings': [{'form': '10-Q', 'filedAt': '2026-07-31'}, {'form': '8-K', 'filedAt': '2026-07-30'}, {'form': '10-Q', 'filedAt': '2026-05-01'}], 'netIncome': (2025, '2025-09-27', 112010000000)}
Three things that bite once you go beyond one company:
-
fyin XBRL facts is the fiscal year of the filing, not of the number. A 10-K repeats the two previous years with the samefy, so pick the row with the latestenddate (as above), not the first row. -
Concept names change. Revenue is
Revenuesfor some filers andRevenueFromContractWithCustomerExcludingAssessedTaxfor others. You need a fallback list per metric. -
GLEIF stores the legal name in the local script.
filter[entity.legalName]=Toyota Motor Corporationreturns Toyota's US subsidiaries, not the parent, because the parent's legal name isトヨタ自動車株式会社. For non-US companies, search by LEI when you can.
Many companies: the actor
I built the Company Lookup actor to do this for a list. You can mix names, tickers, LEIs and CIKs. It returns one record per input with GLEIF data, recent filings, and the latest annual financials from XBRL with concept fallbacks. Anything that failed goes into an errors list, and the rest of the record is still returned.
# pip install apify-client
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("bachiembmt/company-lookup-gleif-sec-edgar").call(run_input={
"companies": ["AAPL", "5493006W3QUS5LMH6R84", "Microsoft Corporation"],
"includeFilings": True,
"filingForms": ["10-K", "8-K"],
"maxFilings": 3,
"includeFinancials": True,
"includeParents": True,
"secUserAgent": "Your Name your.email@example.com",
})
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item["query"], item.get("lei"), item.get("legalName"), item.get("cik"),
(item.get("financials") or {}).get("revenue"), item["errors"])
Output of that run:
AAPL HWUPKR0MPOU8FGXBT394 Apple Inc. 0000320193 {'value': 416161000000, 'fy': 2025, 'unit': 'USD', 'end': '2025-09-27', 'concept': 'RevenueFromContractWithCustomerExcludingAssessedTax'} []
5493006W3QUS5LMH6R84 5493006W3QUS5LMH6R84 トヨタ自動車株式会社 None None []
Microsoft Corporation INR2EJN1ERAN0W5ZP974 MICROSOFT CORPORATION 0000789019 {'value': 331839000000, 'fy': 2026, 'unit': 'USD', 'end': '2026-06-30', 'concept': 'RevenueFromContractWithCustomerExcludingAssessedTax'} []
Toyota by LEI resolves in GLEIF but has no CIK. That is expected: it files with the SEC as a foreign private issuer under a different name, and the actor only links GLEIF to SEC on an exact legal-name match, so it does not guess. apify-client 3.x returns a Run object from .call(), so use run.default_dataset_id. On 2.x it is run["defaultDatasetId"].
Pricing is $4 per 1,000 companies, with no subscription. Set secUserAgent to your own contact, as SEC policy requires.
When to use which
- A handful of US tickers in a notebook: use the DIY script.
- A list of counterparties for KYB, CRM enrichment, or a scheduled filing watch: use the actor, or keep extending the script with name normalisation, concept fallbacks and rate limiting.
Disclosure: I built and maintain the actor. GLEIF data is CC0; SEC EDGAR data is public domain.
Top comments (0)