If you have ever wanted to find every SEC filing that mentions a phrase, a person, or a legal term, EDGAR has a full text search index that covers every filing since 2001. It is a plain JSON API, no key and no login, and most people never touch it directly.
The endpoint
GET https://efts.sec.gov/LATEST/search-index?q=%22material+weakness%22&forms=10-K&startdt=2026-01-01&enddt=2026-06-30
Parameters:
-
qis the query. Wrap a phrase in double quotes for an exact match. -
formsis an optional comma list of form types, e.g.8-K,10-K. -
startdtandenddtscope the filing date (withdateRange=custom). -
ciksrestricts to one or more companies by CIK number. -
fromis the paging offset.
EDGAR asks for a descriptive User-Agent with contact info. Send one and you are fine:
const res = await fetch(url, {
headers: {
Accept: 'application/json',
'User-Agent': 'YourApp contact@example.com',
},
});
The response shape
Results come back Elasticsearch style under hits.hits. Each hit has an _id and a _source:
{
"_id": "0001493152-26-015257:form10-ka.htm",
"_source": {
"display_names": ["Where Food Comes From, Inc. (WFCF) (CIK 0001360565)"],
"ciks": ["0001360565"],
"form": "10-K/A",
"file_date": "2026-04-06",
"adsh": "0001493152-26-015257"
}
}
Two useful tricks:
The display_names string packs the company, ticker, and CIK together. A small regex pulls them apart:
const m = name.match(/^(.*?)\s*\(([^)]+)\)\s*\(CIK\s*\d+\)\s*$/);
const companyName = m ? m[1].trim() : name;
const ticker = m ? m[2].trim() : null;
The _id is accession:filename, and adsh is the accession number. Combine them into a direct link to the document:
const filename = id.slice(id.indexOf(':') + 1);
const nodash = adsh.replace(/-/g, '');
const cik = String(Number(ciks[0])); // strip leading zeros
const url = `https://www.sec.gov/Archives/edgar/data/${cik}/${nodash}/${filename}`;
Paging
Each request returns up to 100 hits. Advance from in steps of 100 until you have what you need. EDGAR caps deep paging at a 10,000 result window per query, so read the reported hits.total.value and stop there.
let from = 0;
while (from < 10000) {
const data = await getJson(buildUrl(from));
const hits = data.hits.hits;
if (hits.length === 0) break;
// ...process hits...
if (hits.length < 100) break;
from += 100;
}
Why it is handy
Full text search across every filer is a different job from watching one company or one form. You can catch risk language like "going concern" or "material weakness" wherever it lands, follow an executive across filers, or track a product mention over time. And because it is a light JSON GET, a run costs almost nothing.
If you want this packaged instead of maintained by hand, I run it as a keyless pay per use actor on Apify. Give it a query and it returns one JSON row per filing with a direct link. The first rows of every run are free so you can check the output.
Top comments (0)