Search SEC EDGAR Full-Text Filings

Site efts.sec.govTask search-edgar-full-text-filingsVersion v6Updated Sep 16, 2026Category search

Search SEC EDGAR filing text through the JSON full-text endpoint and return normalized filing metadata and archive URLs, including exact-form filtering, complete pagination, zero-result searches, and empty offset pages. This skill was captured from a live agent session on efts.sec.gov and is published here as a reusable recipe for agents.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Search SEC EDGAR filing text through the JSON full-text endpoint and return normalized filing metadata and archive URLs. Supports exact form filtering, bounded date ranges, complete pagination, zero-result searches, Unicode and emoji queries, human-style result pages, and offsets beyond the available result set.

Use Cases

  • Find every EDGAR filing mentioning a phrase.
  • Search non-ASCII or emoji-containing text while preserving the decoded query.
  • Restrict full-text results to exact SEC form types.
  • Enumerate filings within a custom date range.
  • Retrieve a specific 10-result human-style search page or test an empty offset.

Automation Flow

  1. For each independent query, build https://efts.sec.gov/LATEST/search-index?q={query}&forms={forms}&dateRange=custom&startdt={start-date}&enddt={end-date}&from={offset}. URL-encode Unicode and emoji; omit forms when unfiltered. For human-style page {page}, use offset=({page}-1)*10.
  2. Goto the URL and run this extractor. With from>0, it takes the first 10 raw hits from that API response, then applies exact-form filtering as the requested human-style page; with from=0, it advances by the actual number of raw hits returned, up to the 10,000-hit window, and reports whether enumeration is complete. An offset beyond the available hits returns fetched_hits: 0 and returned: 0 while preserving the reported total. total_results counts API hits; returned counts results after exact-form filtering and accession deduplication.
(async () => {
const params = new URLSearchParams(location.search);
const first = JSON.parse(document.body.innerText || '{}');
if (!Array.isArray(first?.hits?.hits)) throw new Error('EDGAR response has no hits array');
const requestedForms = (params.get('forms') || '').split(',').filter(Boolean);
const totalRaw = first?.hits?.total;
const total = typeof totalRaw === 'object' ? (totalRaw?.value ?? 0) : (totalRaw ?? 0);
const relation = typeof totalRaw === 'object' ? (totalRaw?.relation ?? null) : null;
const offset = Number(params.get('from') || 0);
const pageSize = 10;
const cleanName = v => String(v || '').split('  (')[0];
const normalize = h => {
const s = h?._source || {};
const adsh = s.adsh || '';
const filerCik = (s.ciks || [])[0] || null;
const cik = String(filerCik || '').replace(/^0+(?=\d)/, '') || null;
const accession = adsh.replace(/-/g, '');
const id = String(h?._id || '');
const matchingFile = id.includes(':') ? id.slice(id.indexOf(':') + 1) : null;
return {
accession_number: adsh,
filer_name: cleanName((s.display_names || [])[0]),
all_filers: s.display_names || [],
filer_cik: filerCik,
form_type: s.form || null,
filing_date: s.file_date || null,
period_of_report: s.period_ending || null,
sic: (s.sics || [])[0] || null,
state_of_incorporation: (s.inc_states || [])[0] || null,
business_state: (s.biz_states || [])[0] || null,
matching_file: matchingFile,
url: cik && accession && matchingFile ? `https://www.sec.gov/Archives/edgar/data/${cik}/${accession}/${matchingFile}` : null,
filing_index_url: cik && accession && adsh ? `https://www.sec.gov/Archives/edgar/data/${cik}/${accession}/${adsh}-index.htm` : null,
snippet: null
};
};
let pages = [...(first?.hits?.hits || [])];
const humanPage = offset > 0;
if (!humanPage) {
let next = pages.length;
while (next < total && next < 10000 && pages.length > 0) {
const pageUrl = new URL(location.href);
pageUrl.searchParams.set('from', String(next));
let response;
for (let attempt = 0; attempt < 2; attempt++) {
await new Promise(resolve => setTimeout(resolve, attempt ? 700 : 150));
response = await fetch(pageUrl.href);
if (response.status !== 500) break;
}
if (!response.ok) throw new Error(`EDGAR search page ${next} returned ${response.status}`);
const page = await response.json();
if (!Array.isArray(page?.hits?.hits)) throw new Error(`EDGAR search page ${next} has no hits array`);
const hits = page.hits.hits;
if (!hits.length) break;
pages.push(...hits);
next += hits.length;
}
} else {
pages = pages.slice(0, pageSize);
}
const fetchedBeforeFilter = pages.length;
const exact = requestedForms.length ? pages.filter(h => requestedForms.includes(h?._source?.form)) : pages;
const seen = new Set();
const results = exact.flatMap(h => {
const item = normalize(h);
if (!item.accession_number || seen.has(item.accession_number)) return [];
seen.add(item.accession_number);
return [item];
});
return {
status: humanPage ? 'page' : relation === 'gte' || total >= 10000 || fetchedBeforeFilter < total ? 'partial' : 'complete',
query: params.get('q') || '',
forms: requestedForms,
date_range: params.get('dateRange') === 'custom' ? {start: params.get('startdt') || null, end: params.get('enddt') || null} : {start: null, end: null},
total_results: total,
total_relation: relation,
page: humanPage ? Math.floor(offset / pageSize) + 1 : null,
offset,
page_size: humanPage ? pageSize : null,
fetched_hits: fetchedBeforeFilter,
returned: results.length,
exact_forms: requestedForms.length > 0,
results
};
})()
  1. If status is partial, do not claim complete coverage. If total_relation is gte or the result set reaches 10,000 hits, split the requested date range into smaller custom windows, run the full extractor from from=0 for each window, and stitch results by accession number.

Params

ParamWhat it doesExample value
queryFull-text phrase or expression; URL-encode it and preserve decoded quotation marks in the returned query field%22annual%20report%22
formsComma-separated form filter; extractor enforces exact returned form values10-K,20-F
dateRangeSearch modecustom
start-dateInclusive custom-range start2020-01-01
end-dateInclusive custom-range end2024-12-31
offsetZero-based API result offset; use ({page}-1)*10 for a human-style page200
pageHuman-style page number used to calculate offset3

Possible Friction Points

TriggerAction
forms=10-K results contain 10-K/A amendmentsKeep only records whose _source.form exactly equals 10-K; the extractor does this automatically.
Response reports total_relation: "gte" or reaches the 10,000-hit endpoint capRecursively split the date range into custom windows, paginate each window by the raw hit count actually returned, and deduplicate by accession number.
A hit has no matching filename in _idKeep matching_file and document url null, and use the generated filing_index_url for the filing contents.
In-page pagination fetch returns a non-OK response, including a transient 500Retry the identical offset URL in the BQL session; if it persists, navigate directly to that offset URL and run the extractor there.
An offset page returns no hits while total_results is positiveTreat that offset as an empty page; retain the reported total and do not infer additional records.
A human-style page is requestedUse API offset ({page}-1)*10, then retain only the first 10 hits; an empty page returns results: [].
Browser header-setting command is rejectedSet requester identification in the HTTP client or browser session; page JavaScript cannot set User-Agent.
  • from is a raw hit offset, not a page number. Advance by hits.hits.length before filtering forms or deduplicating accessions. Never assume a fixed 10- or 100-hit batch; a request or server default can return fewer hits.
  • For direct HTTP clients, identify the requester with a descriptive User-Agent such as Your Company contact@example.com. Browser JavaScript cannot set that header; configure it in the HTTP client or browser session. Keep aggregate traffic within SEC's fair-access guidance of 10 requests per second; the sequential extractors pause between requests.
  • forms=10-K may include amendments. These extractors filter exact _source.form values after pagination; raw hit totals can exceed returned distinct filings.
  • Additional filters use ciks, plural locationCodes, and locationType=incorporated when applicable. Preserve them on pagination URLs. SIC is available in response metadata; do not assume a SIC query parameter is supported.
  • Matching document filenames come from the suffix of _id after :. The search response does not supply matched-text snippets; fetch the document separately when needed. Preserve co-registrant arrays instead of treating every filing as a single filer.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=efts.sec.gov&task=search-edgar-full-text-filings