EDGAR full-text search has a JSON endpoint: https://efts.sec.gov/LATEST/search-index?q=.... Pass a phrase in q, filter with forms, ciks (10-digit) and startdt/enddt, and page with from in steps of 100. No API key is needed, but you must send a User-Agent naming you. The SEC doesn't document this endpoint as an API; it's the backend of its Full-Text Search page.
This page documents what each parameter actually does. Every response from the endpoint includes a query object, the Elasticsearch query it ran, so the table below comes from those echoes plus live tests on 2026-10-14. It is not from an official spec, because there isn't one.
What full-text search covers
From the SEC's Full-Text Search FAQ: it searches "the full text of all EDGAR filings submitted electronically since 2001", and "the full text of a filing includes all data in the filing itself as well as all attachments (such as exhibits)". Natural-language queries aren't supported.
Each result is one document, not one filing. A 10-K and its exhibits are separate hits that share an accession number. In one page of 100 hits I found 99 distinct filings.
Parameters
| Parameter | What it does (from the echoed query) | Example |
|---|---|---|
q | Searches document text. A quoted phrase is a phrase match. Words without quotes are each required. | q="supply chain disruption" |
forms | Filters on root_forms, comma-separated. Upper case only: 8-k matched 0 documents where 8-K matched 831. 10-K also returns 10-K/A. | forms=10-K,8-K |
ciks | Filters on ciks. Must be zero-padded to 10 digits: 320193 matched 0, 0000320193 matched 12. | ciks=0000320193 |
startdt, enddt | Range on file_date, format yyyy-mm-dd. Works with or without dateRange=custom. | startdt=2025-01-01&enddt=2025-12-31 |
dateRange | Ignored by the endpoint. 30d, 1y, 5y and all produced no date filter; the website turns them into startdt/enddt itself. | |
from | Offset of the first hit. from=9900 worked; from=10000 returned no hits. | from=100 |
sort | desc or asc sorts by file_date. Without it, results are ranked by relevance. | sort=desc |
locationCodes | Filters on business-address state (biz_states). | locationCodes=CA |
locationType=incorporated | Makes locationCodes filter on state of incorporation (inc_states) instead. | locationType=incorporated&locationCodes=DE |
entityName | Adds a loose word match on company names. Apple Inc. became separate matches for Apple and Inc., giving 43 results against 12 for Apple's CIK. Prefer ciks. | |
hits, size, page | Ignored. The page size is always 100. |
Query syntax in q
These all ran on 2026-10-14. The syntax matches the FAQ.
| You type | It becomes |
|---|---|
"supply chain disruption" | Exact phrase |
software hardware | Both words required (implied AND) |
tariff OR tariffs -china | Either word, and not "china" (- or NOT excludes) |
tariff NEAR(5) china | Both words within 5 words of each other, in any order (default 10) |
gas* | Prefix wildcard (only at the end of a word) |
An empty q with no other filter returns nothing. But forms plus dates with no q works: forms=8-K&startdt=2026-10-13&enddt=2026-10-13 returned the day's 252 8-K documents.
The response
{ "took": 293, "hits": { "total": {"value": 331, "relation": "eq"}, "hits": [{ "_id": "0001640334-25-001300:acro_10k.htm", "_source": { "ciks": ["0001622996"], "display_names": ["ACRO BIOMEDICAL CO., LTD. (CIK 0001622996)"], "form": "10-K", "root_forms": ["10-K"], "file_type": "10-K", "file_date": "2025-07-24", "period_ending": "2024-12-31", "adsh": "0001640334-25-001300", "sequence": 1, "sics": ["7900"], "biz_states": ["IN"], "inc_states": ["NV"], "biz_locations": ["Fishers, IN"], "file_num": ["000-55643"], "items": [] } }] }, "aggregations": {"entity_filter": {}, "sic_filter": {}, "biz_states_filter": {}, "form_filter": {}}, "query": {} } That's a trimmed real hit from the query below. The fields you'll use most:
-
hits.total.valuewithrelation. Whenrelationis"gte", the value is capped at 10,000 and the true count is higher. -
_idisaccession:filename. Together with the first CIK, it gives the document URL (seedoc_urlbelow). -
adshis the accession number, the same for every document in a filing. -
itemslists 8-K item numbers when the document belongs to an 8-K. -
aggregationsgives the top 30 companies, SIC codes, states and forms for the whole result set. That's useful for counts without paging.
Python: search, page and link to documents
import time import requests EFTS = "https://efts.sec.gov/LATEST/search-index" HEADERS = {"User-Agent": "Data Tooly research contact@example.com"} # use your own name + email def search(q, forms=None, ciks=None, start=None, end=None, max_hits=300): """Yield full-text search hits, 100 per page, following `from`.""" params = {"q": q} if forms: params["forms"] = ",".join(forms) # upper case: "8-K", not "8-k" if ciks: params["ciks"] = ",".join(f"{int(c):010d}" for c in ciks) # must be 10 digits if start or end: params.update(dateRange="custom", startdt=start, enddt=end) # yyyy-mm-dd offset = 0 while offset < max_hits: r = requests.get(EFTS, params={**params, "from": offset}, headers=HEADERS, timeout=60) r.raise_for_status() hits = r.json()["hits"]["hits"] if not hits: return yield from hits offset += len(hits) # the page size is fixed at 100 time.sleep(0.2) # stay well under 10 requests/second def doc_url(hit): """Build the sec.gov URL of the matching document.""" adsh, filename = hit["_id"].split(":", 1) cik = int(hit["_source"]["ciks"][0]) return f"https://www.sec.gov/Archives/edgar/data/{cik}/{adsh.replace('-', '')}/{filename}" first = requests.get(EFTS, headers=HEADERS, timeout=60, params={ "q": '"supply chain disruption"', "forms": "10-K", "dateRange": "custom", "startdt": "2025-01-01", "enddt": "2025-12-31"}).json() print("total:", first["hits"]["total"]) print("forms:", [(b["key"], b["doc_count"]) for b in first["aggregations"]["form_filter"]["buckets"]]) print("top states:", [(b["key"], b["doc_count"]) for b in first["aggregations"]["biz_states_filter"]["buckets"][:3]]) hits = list(search('"supply chain disruption"', forms=["10-K"], start="2025-01-01", end="2025-12-31")) print(len(hits), "hits,", len({h["_source"]["adsh"] for h in hits}), "distinct filings") h = hits[0]["_source"] print(h["display_names"][0], "|", h["form"], h["file_date"], "| period", h["period_ending"]) print(doc_url(hits[0])) apple = list(search("tariff", ciks=[320193])) print("Apple filings mentioning tariff:", len(apple), sorted({x["_source"]["form"] for x in apple})) Output on 2026-10-14:
total: {'value': 331, 'relation': 'eq'} forms: [('10-K', 331)] top states: [('CA', 68), ('NY', 28), ('TX', 26)] 300 hits, 299 distinct filings ACRO BIOMEDICAL CO., LTD. (CIK 0001622996) | 10-K 2025-07-24 | period 2024-12-31 https://www.sec.gov/Archives/edgar/data/1622996/000164033425001300/acro_10k.htm Apple filings mentioning tariff: 12 ['10-K', '10-K405', '10-Q', '8-K', 'DEF 14A', 'PX14A6G', 'SC TO-I'] The document URL returned HTTP 200. The filing's index is at the same folder plus {accession}-index.htm, or index.json for a machine-readable listing.
The same first query with curl:
curl -s -A "Data Tooly research contact@example.com" \ "https://efts.sec.gov/LATEST/search-index?q=%22supply+chain+disruption%22&forms=10-K&dateRange=custom&startdt=2025-01-01&enddt=2025-12-31" Traps that return wrong data without an error
None of these return an error. You just get wrong or empty results:
- Unpadded CIK → 0 results.
ciks=320193returns an empty result with HTTP 200. Always pad:f"{cik:010d}". - Lowercase form → 0 results.
forms=8-kmatched nothing. -
dateRange=30ddoes nothing. Sendstartdtandenddtyourself. - A badly formatted date is ignored.
startdt=2026/01/01returned unfiltered results. Useyyyy-mm-dd. - The 10,000-hit ceiling.
fromcan't go past 9,900. To collect more, split the date range into smaller windows, for example month by month, so each window stays under 10,000. - Hits are documents. Count filings with distinct
adsh, not withlen(hits).
Rate limit and User-Agent
The endpoint follows SEC.gov's fair-access rules: at most 10 requests per second, and a declared User-Agent. With no User-Agent, or with curl's or python-requests' default, efts.sec.gov answered HTTP 403. The SEC rate limit and User-Agent guide covers the exact format and the block behavior.
Related guides for this API
- SEC EDGAR API in Python: User-Agent, rate limits, XBRL, the overview.
- Submissions API and ticker to CIK: get the 10-digit CIK that
ciks=needs. - Company facts, company concept and frames: the numbers inside the filings you found.
- SEC rate limits and User-Agent rules.
Do it without code
The free SEC EDGAR query builder assembles a full-text search configuration, with tickers, forms and dates, and shows the output shape. It's a query builder, not a live search: browsers can't send the User-Agent the SEC requires, so the query runs server-side.
At scale
The SEC EDGAR Scraper actor on Apify has a full_text_search mode (searchQuery, searchCiks, forms, dateFrom, dateTo) that handles the User-Agent, pacing and paging, and returns clean rows as JSON, CSV or Excel. It also has a latest_feed mode for new filings. It's free to start, then pay-as-you-go.
Disclosure: I build the datatooly query builder and the Apify actor. The endpoint works the same without them.
Sources: SEC Full-Text Search FAQ, SEC: Accessing EDGAR Data, and live responses from efts.sec.gov on 2026-10-14, including each response's echoed query object.