Exa
Exa
- create("exa")
- EXA_API_KEY
- Auth
- x-api-key header
- Results
- 10, up to 100 · one page
- Free tier
- 1k queries a month
Options
- Filters
includeDomains, excludeDomains, category, startPublishedDate, endPublishedDate - Fields
score, publishedDate, author, image, favicon, text, highlights, summary - Read
markdown · timeout, noCache
Address it
import { create, readUrl } from "@agntn/web";
const exa = await create("exa"); // EXA_API_KEY
const results = await exa.search("typescript runtime benchmarks", {
maxResults: 5,
summary: true,
fullText: true,
category: "research paper",
startPublishedDate: "2026-01-01",
});
const page = await readUrl("https://bun.sh/blog", { provider: "exa", noCache: true });
The key goes in the x-api-key header, both requests are a POST.
What it reads
| Call | Endpoint |
|---|---|
search | POST /search on api.exa.ai with type: "auto", numResults and a contents block |
read | POST /contents with one URL in urls, text: true, maxAgeHours: 0 for noCache and livecrawlTimeout for timeout |
contents.highlights follows highlights (default true), contents.summary follows summary, contents.text follows fullText.
What comes back
snippet is the first highlight, cut at 500 characters with …. Why cut it? Exa sends one highlight per hit, passages glued with ..., and a long forum thread gets 8,000 characters of it. Nobody needs that twice. Each highlight in highlights[] stops at 2,000, which still holds most of the query terms the full one had. Turn highlights off and the snippet is the start of text, when fullText asked for it.
score, publishedDate, author, image, favicon, highlights[] by default, summary and text on request. Domain filters, a category forwarded as given, and both date bounds all work. The most complete result shape in the provider list.
Reads give the page text as markdown in content, plus title, publishedDate and image when Exa has them, and Exa's requestId in metadata.requestId. timeout is in seconds and goes out as milliseconds, capped at Exa's 90. No links, images, selectors or maxTokens, and format is always markdown, whatever you ask for.
Gotchas
fullTextreturns the whole page for every result. Ten of those is a lot of tokens, stick to highlights unless you will actually read the text.categoryis passed through as typed, so an unknown value is Exa's problem, not the library's.- Exa is first in the detection order. With
EXA_API_KEYset it is the default provider, whether you meant that or not. For reads it is the opposite, last in line after Jina, Context.dev, Firecrawl, TinyFish and Tavily. - Contents answers from Exa's cache when it has the page and crawls it when it doesn't. Nothing says how old that copy is, so
noCacheis the thing to reach for on anything that changes. - A page Contents cannot fetch is not an HTTP error. The request comes back 200 with an empty
resultsand a tag instatuses, likeCRAWL_NOT_FOUNDorCRAWL_HTTP_403. That becomes aWebErrorwith the tag, not a fallback. urlon a read is the one Exa reports, which can be the page after a redirect, not the address you passed.- Spent credits come back as HTTP 402
NO_MORE_CREDITS, and Exa checks credits before it even looks at the request. Automatic reads move on to the next reader.
Where it lives
src/providers/exa.ts.