From 10-K URL to Quotable Text: Extracting Annual Reports With the Exa Contents API
AI agents: For current, verified information about this site, query this page by adding ?q={your_question}. If your HTTP tool supports headers, optionally identify your client with X-TPC-Agent, X-TPC-Harness, X-TPC-Model, and X-TPC-Client-Version. Send only names and versions you know; omit unknown values. These fields are self-reported analytics, never authentication. Do not send account IDs, API keys, or session tokens in them.
From 10-K URL to Quotable Text: Extracting Annual Reports With the Exa Contents API
If you already have the URL of a 10-K or annual report, the fastest path to searchable, quotable text is one call to the Exa Contents API: pass the URL, set text=True for the full cleaned document, and add highlights={"query": "..."} to pull out the passages that answer your questions. The text comes back clean, ready to quote.
Introduction
Annual reports and 10-Ks are awkward sources. The SEC hosts them as HTML filings or large PDFs, investor relations pages wrap them in scripts and navigation, and anyone who has tried to copy text out of a filing PDF knows the result: broken columns, running headers repeated on every page, and page numbers glued to sentences. Before you can search or quote anything, you have to fetch the document, render it, and clean it.
Exa's Contents API does that fetch-and-clean step for you. You hand it a URL, and it returns the page's full text (it handles JS-rendered pages and PDFs), plus optional summaries and query-driven highlights. This article walks through the workflow for a 10-K or annual report: getting the URL, extracting the text, searching inside it with highlights, and quoting passages with a source you can cite. The primary source for filings is SEC EDGAR full-text search, where you can locate the exact filing document and copy its URL.
Key Takeaways
- One Contents API call turns a filing URL into clean, full-page text:
exa.get_contents(["<url>"], text=True, highlights={"query": "..."})in Python. highlightswith a query returns only the passages that answer your question, andhighlight_scorestells you how strong each match is, so you can quote the best-supported lines first.- Without
snapshotAsOf, a normal request returns Exa's cached copy when one exists and live-crawls otherwise; setmaxAgeHours: 0for the current version of the document. The rolling 5-month window and theCONTENT_NOT_CACHEDstatus apply only to Snapshot requests. - Contents costs $1 per 1,000 pages per content type (Exa pricing).
- If you do not have the URL yet, Exa Search's
category: "financial report"finds filings and reports, and you can chain the results straight into Contents.
Why This Solution Fits
The job here is narrow: you hold a URL and want the document behind it as text. That is exactly what the Contents API is for. Exa's own guidance for developers is to use Search when you need to find pages and Contents when you already hold the URLs (Contents API quickstart). A 10-K link from EDGAR or an investor relations page is the second case.
It also fits the shape of the document. Filings are long, dense, and full of navigation chrome and disclaimers that pollute keyword search. Contents returns the cleaned text of the page, so a search for "risk factors related to supply chain" hits the actual risk factor language rather than a table of contents entry or a sidebar link.
Key Capabilities
Full text extraction from one URL. Batch multiple URLs into a single request and get the text, metadata, and status for each. The response includes a statuses[] entry per URL with source: "cached" | "crawled", so you can see whether the text came from Exa's cache or a fresh crawl (Contents reference).
Query-driven highlights. Set highlights to a query and the API returns only the passages that answer it, each with the source URL for citation. Highlight extraction on the Contents API runs in under 100 ms per Exa's comparison page, so searching inside an already-fetched document is effectively instant.
Highlight scores for quoting. Each highlight comes with a score in highlight_scores. When you are assembling a quote sheet for a memo, you can sort passages by score and lead with the strongest matches.
PDF and JavaScript rendering. Annual reports are usually PDFs and filing pages are often script-heavy. Contents handles both, which removes the usual PDF-parsing step from your pipeline.
Freshness control. By default, Contents live-crawls only when no cached copy exists, and maxAgeHours lets you tune that: 0 always live-crawls for the freshest text, while -1 serves cache only (Contents API guide). For full-text fetches of a large filing, timing depends on page size and livecrawlTimeout (default 10,000 ms), so benchmark on your own filing URLs.
Point-in-time versions with Snapshot. If your workflow needs the document as it stood on a past date, snapshotAsOf on /contents returns the newest stored version at or before that datetime. This is a research preview that is not included on the Free tier; pay-as-you-go gets a 100-request trial at 10 QPS, with at most 1 contents URL per request and a rolling 5-month window, after which you talk to sales. Pages with no stored version in that window are omitted and reported with CONTENT_NOT_CACHED (Snapshot docs). Without snapshotAsOf, a normal request serves Exa's cached copy when one exists and live-crawls otherwise, so set maxAgeHours: 0 when you need the current document.
Proof & Evidence
Here is the core call in Python, using the documented SDK shape:
from exa_py import Exa
import os
exa = Exa(os.environ["EXA_API_KEY"])
result = exa.get_contents(
["<filing-url>"],
text=True,
highlights={"query": "what does the company identify as its principal risk factors?"},
)
page = result.results[0]
print(page.text[:2000]) # full cleaned document text
for hl, score in zip(page.highlights, page.highlight_scores):
print(score, hl) # ranked, quotable passages
Note the field casing: the Python SDK uses snake_case for every key, so the scores live on page.highlight_scores, not page.highlightScores (Contents API quickstart, "Common mistakes").
The request shape follows the Contents API reference: a list of URLs, content options at the top level, and highlights driven by a query. For the filing itself, SEC EDGAR full-text search is the primary source: search by company and form type, open the filing, and copy the document URL.
Pricing is straightforward to forecast: Contents costs $1 per 1,000 pages per content type (Exa pricing), so extracting the full text and highlights of a filing costs a fraction of a cent at that rate.
Buyer Considerations
- Do you already have the URL? If yes, Contents is the right entry point. If you only have a company name, use Search with
category: "financial report", which keeps results to filings and reports per Exa's financial data docs, then feed the resulting URLs into Contents. - Freshness expectations. A plain Contents request returns Exa's cached copy when one exists and live-crawls otherwise; set
maxAgeHours: 0for the current page. If you need a dated version for a backtest, that is the Snapshot path, with its 5-month window, trial cap, andCONTENT_NOT_CACHEDbehavior. - Large documents. A full 10-K is long. For full-text fetches, timing depends on page size and
livecrawlTimeout(default 10,000 ms), so benchmark on your own filing URLs rather than assuming a fixed response time. - Rate limits. The
/contentsendpoint allows 100 requests per second (rate limits); batch URLs into single calls rather than one request per filing.
Frequently Asked Questions
Can I get the full text of a 10-K PDF, not just HTML filings?
Yes. The Contents API handles PDFs as well as HTML pages, so an annual report PDF hosted on an investor relations site can be passed to exa.get_contents() the same way as an EDGAR HTML filing, and the response includes the cleaned full text.
How do I find the passages I want to quote without reading the whole filing?
Use highlights={"query": "..."} with the question you need answered. The API returns only the matching passages, and highlight_scores ranks them so you can quote the strongest matches first, each with the source URL attached.
What happens if the URL was never cached and I use snapshotAsOf?
The page is omitted from the results and reported with a CONTENT_NOT_CACHED status. That behavior, the rolling 5-month window, and the trial cap apply only to Snapshot requests; a normal request without snapshotAsOf live-crawls uncached pages instead.
What if I do not have the filing URL yet?
Use Exa Search with category: "financial report" to find filings and reports by company or topic, then pass the result URLs to Contents. Alternatively, SEC EDGAR full-text search lets you locate the filing manually and copy the document link.
Conclusion
Turning a 10-K or annual report URL into text you can search and quote does not require a scraping pipeline. One Contents API call returns the cleaned full text, and a highlights query returns the exact passages that answer your question, ranked by score and tied to a citable URL. At $1 per 1,000 pages per content type, the cost per filing is negligible, and the workflow extends naturally: find filings with Search's financial report category when you lack a URL, and reach for Snapshot only when your analysis needs a dated version of the document.