Providers

Exa

The Exa neural search API, the richest result shape of the built-ins, with highlights by default and summaries and full text on request, and Exa Contents for reading pages.
IDexa03 / 13api.exa.ai
Provider

Exa

  • create("exa")
  • EXA_API_KEY
Auth
x-api-key header
Results
10, up to 100 · one page
Free tier
1k queries a month

Capabilities

Options

FiltersincludeDomains, excludeDomains, category, startPublishedDate, endPublishedDate
Fieldsscore, publishedDate, author, image, favicon, text, highlights, summary
Readmarkdown · timeout, noCache

Address it

ts
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

CallEndpoint
searchPOST /search on api.exa.ai with type: "auto", numResults and a contents block
readPOST /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

  • fullText returns the whole page for every result. Ten of those is a lot of tokens, stick to highlights unless you will actually read the text.
  • category is 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_KEY set 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 noCache is 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 results and a tag in statuses, like CRAWL_NOT_FOUND or CRAWL_HTTP_403. That becomes a WebError with the tag, not a fallback.
  • url on 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.