GuidesFINANCE APIS
EDGAR full-text search API from JavaScript
EDGAR full-text search has no documented API, but SEC's search page calls https://efts.sec.gov/LATEST/search-index, which returns JSON for any query across filings since 2001. On 28 October 2026 that endpoint sent CORS headers only to https://www.sec.gov, so fetch() from your own site is blocked; through https://proxy.cors.dev/ it returned 200 with up to 100 hits per request. The free tier needs no signup and handles GET requests at about 600 a minute per IP.
The endpoint behind the search page
SEC's full-text search page at https://www.sec.gov/edgar/search/ runs on a JSON endpoint: its JavaScript sends GET requests to https://efts.sec.gov/LATEST/search-index with the form fields as query parameters. The index covers the full text of all EDGAR filings submitted electronically since 2001, including attachments such as exhibits.
SEC documents the search page and its query syntax, but not this endpoint as an API, so parameters and fields can change without notice. The documented APIs live on data.sec.gov, covered in the SEC EDGAR API guide.
On 28 October 2026, efts.sec.gov sent Access-Control-Allow-Origin only to https://www.sec.gov, the search page's own origin. Requests from https://example.com and http://localhost:5173 got no CORS header, so the browser blocks the response. Through https://proxy.cors.dev/ the same URLs returned 200 with identical JSON: 39 KB for q="cors"&forms=10-K (66 hits).
Parameters that work
The search page rewrites some of its own URL fields before calling the endpoint, so copying them straight from the page URL does not always work. These are the results of sending each parameter directly:
| Parameter | Example | Effect | Result |
|---|---|---|---|
| q | "material weakness" | Phrase search in all text; words are ANDed | Works |
| q operators | tariffs NEAR(5) china | OR, NOT, -term, suffix *, NEAR(n) | Works |
| forms | 10-K,8-K | Comma list; 10-K also matches 10-K/A | Works |
| startdt, enddt | 2026-10-01 | Filing date range, YYYY-MM-DD | Works |
| ciks | 0000320193 | Filer CIK, 10 digits; 320193 finds nothing | Works |
| entityName | Apple | Matches words in the filer name | Works |
| locationCodes | CA | Business address state | Works |
| from | 100 | Offset, in steps of 100, up to 9,900 | Works |
| page | 2 | The search page turns it into from | Ignored |
| size | 10 | Pages are always 100 hits | Ignored |
| dateRange | 30d | Presets do nothing; send startdt and enddt | Ignored |
| keysTyped | apple | Company suggestions with CIK and ticker | Works |
Search is case-insensitive and words are combined with AND. Quotes match an exact phrase, a capitalised OR needs one of the terms, and NOT or a leading hyphen excludes one; a trailing * matches word stems (not inside phrases), and NEAR(n) finds terms within n words, 10 by default. Counts for 10-K filings filed 1 January to 28 October 2026:
tariffs NEAR(5) china: 698tariffs -china: 1,711tariff*: 4,540"supply chain" OR semiconductors: 4,469
Leave out q and send only forms plus dates to list filings newest first: 391 8-K filings dated 28 October 2026.
Search filings and build document links
The response is Elasticsearch-shaped. hits.total has value and relation, hits.hits holds at most 100 results, and aggregations has key and doc_count buckets in entity_filter, sic_filter, biz_states_filter and form_filter, the facet counts on the search page.
Each hit's _id is the accession number, a colon and the file name, such as 0001493152-26-048997:form8-k.htm. _source carries the metadata:
ciks(10 digits, filer first) anddisplay_names, such as "Nano Dimension Ltd. (NNDM) (CIK 0001643303)"form,root_forms,file_date,period_endinganditems(8-K item numbers such as 4.01 and 9.01)file_type(such as EX-21.1 for an exhibit),file_description,xsl,sics,biz_statesandinc_states
There is no document link in the response, so build it the way the search page does: https://www.sec.gov/Archives/edgar/data/, the first CIK without leading zeros, the accession number without dashes, then the file name. For XML forms such as Form 4, put the xsl value (such as xslF345X06) and a slash before the file name to get the rendered page instead of raw XML. This search for "material weakness" in 8-K filings from 1 to 28 October 2026 found 101 filings:
// Runs in a module script or the DevTools console
const proxy = 'https://proxy.cors.dev/';
async function searchFilings(options) {
const params = new URLSearchParams(Object.entries(options).filter(([, value]) => value !== undefined && value !== ''));
const response = await fetch(proxy + 'https://efts.sec.gov/LATEST/search-index?' + params);
if (!response.ok) throw new Error(`Search failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
const data = await response.json();
if (!data.hits) throw new Error(data.errorMessage ?? 'Unexpected response');
const filings = data.hits.hits.map(({ _id, _source: hit }) => {
const [accession, fileName] = _id.split(':');
const path = hit.xsl && fileName.endsWith('.xml') ? `${hit.xsl}/${fileName}` : fileName;
return {
company: hit.display_names[0],
form: hit.form,
filed: hit.file_date,
url: `https://www.sec.gov/Archives/edgar/data/${Number(hit.ciks[0])}/${accession.replaceAll('-', '')}/${path}`,
};
});
return { total: data.hits.total.value, filings };
}
const result = await searchFilings({ q: '"material weakness"', forms: '8-K', startdt: '2026-10-01', enddt: '2026-10-28' });
console.log(result.total, result.filings.slice(0, 3));Paging past 100 results
Every request returns up to 100 hits, whatever you pass as size, so page with from=0, from=100 and so on up to from=9900. Totals stop at 10,000: a broad query such as tariffs reports value: 10000 with relation: "gte".
Asking for from=10000 still returns HTTP 200, but the body is {"errorType":"ResponseError","errorMessage":"search_phase_execution_exception: ... Result window is too large, from + size must be less than or equal to: [10000] ..."} instead of hits. Check for hits before reading it, and split bigger result sets by date range or form. This loop stops at a short page or at your maximum:
// Runs in a module script or the DevTools console
const proxy = 'https://proxy.cors.dev/';
async function allHits(params, maxResults = 1000) {
const hits = [];
// Pages are fixed at 100 hits; from + 100 may not pass 10,000
for (let from = 0; from < Math.min(maxResults, 10000); from += 100) {
const query = new URLSearchParams({ ...params, from: String(from) });
const response = await fetch(proxy + 'https://efts.sec.gov/LATEST/search-index?' + query);
if (!response.ok) throw new Error(`Search failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
const data = await response.json();
if (!data.hits) throw new Error(data.errorMessage ?? 'Unexpected response');
hits.push(...data.hits.hits);
if (data.hits.hits.length < 100) break;
}
return hits;
}
const hits = await allHits({ q: '"going concern"', forms: '10-K', startdt: '2026-01-01', enddt: '2026-10-28' }, 500);
console.log(hits.length, hits.map((hit) => hit._source.display_names[0]).slice(0, 3));Limits and which cors.dev tier fits
SEC's fair access rules cover every SEC.gov host: no more than 10 requests per second per user, and automated tools should declare a User-Agent with a company name and contact email. A sequential paging loop like the one above stays well under that.
- cors.dev free: prefix the URL with
https://proxy.cors.dev/. No signup, no key,GETandHEADonly, about 600 requests a minute per IP, no daily quota and responses up to 6 MiB; a full page of 100 hits was 55 to 68 KB. Nothing is cached. - cors.dev Pro: $5 a month for 500,000 requests, with a 7-day trial of 1,000 requests and no card. It allows 3,000 requests a minute per account and 100 in flight, and adds an optional GET cache with
X-Cors-Cache: 60(1 to 300 seconds; cache hits still count as requests), which suits a public page where many visitors load the same saved search. Start the trial.
Good questions.
Is there an official API for EDGAR full-text search?
No. SEC documents the search page and its query syntax, and the page itself calls https://efts.sec.gov/LATEST/search-index, which returns JSON. That endpoint is not documented as an API, so it can change without notice.
Why does my CIK filter return zero filings?
The ciks parameter needs the 10-digit CIK with leading zeros: ciks=0000320193 returned 140 hits for "supply chain", ciks=320193 returned none. Document URLs need the reverse, the CIK without leading zeros.
How far back does EDGAR full-text search go?
It covers filings submitted electronically since 2001, including exhibits and other attachments. For older filings, list a company's filings with the submissions API on data.sec.gov, which reaches back to 1994 for Apple.
Can I get more than 100 results from EDGAR full-text search?
Yes, by paging with from in steps of 100, up to from=9900. The endpoint never returns more than 10,000 hits for one query, so split larger searches by date range or form type.
Why do Form 4 links from full-text search open as raw XML?
Form 4 filings are XML documents. When a hit has an xsl value such as xslF345X06, insert it and a slash before the file name in the /Archives/edgar/data/ URL to get the rendered page.
How do I turn a company name into a CIK with the same endpoint?
Send keysTyped=apple instead of q, through the proxy. It returned 10 suggestions out of 230 matches, each with the CIK in _id, "Apple Inc. (AAPL)" in _source.entity and the ticker in _source.tickers.