Exa API Reference (2026)
Source: docs.exa.ai → redirects to exa.ai/docs
LLM Discovery: exa.ai/docs/llms.txt (150+ pages)
OpenAPI Specs:
- Search API: exa-openapi-spec.yaml
- Websets API: exa-websets-spec.yaml
Python SDK: pip install exa-py (module: exa_py)
Last Verified: 2026-01-13
Verified Against: Official docs via llms.txt, Exa OpenAPI specs, and live API probes where noted
Overview
Exa is "a search engine made for AIs" - optimized for RAG, agentic workflows, and structured data extraction. Unlike traditional search engines (optimized for human clicks), Exa is optimized for machine consumption.
Core Capabilities:
- Search: Neural/embeddings-based web search with 4 modes (auto, neural, fast, deep)
- Contents: Clean, parsed text from URLs with live crawling
- Find Similar: Discover semantically related pages
- Answer: LLM-generated answers with citations
- Research: Async deep research with structured output (agentic)
Base URL & Authentication
Base URL: https://api.exa.ai
Authentication:
x-api-key: YOUR_EXA_API_KEY
Or use Bearer token:
Authorization: Bearer YOUR_EXA_API_KEY
Search Endpoint
POST /search
Intelligently find webpages using embeddings-based search.
Search Types
| Type | Description |
|---|---|
auto |
Default. Intelligently selects the best method |
neural |
Embeddings-based semantic search |
fast |
Streamlined search optimized for speed |
deep |
Multi-pass with query expansion for highest quality |
When to use deep: Complex research queries where quality matters more than speed. The endpoint agentically searches, processes, and searches again until it finds the highest quality information.
Request Body
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
string | Required | The search query |
type |
enum | "auto" |
auto, neural, fast, deep |
additionalQueries |
string[] | - | Extra query variations (deep search only) |
numResults |
integer | 10 | Max 100 results |
includeDomains |
string[] | - | Filter to specific domains |
excludeDomains |
string[] | - | Exclude domains |
startPublishedDate |
ISO 8601 | - | Filter by publish date |
endPublishedDate |
ISO 8601 | - | Filter by publish date |
startCrawlDate |
ISO 8601 | - | Filter by crawl date |
endCrawlDate |
ISO 8601 | - | Filter by crawl date |
includeText |
string[] | - | Must contain (1 string, 5 words max) |
excludeText |
string[] | - | Must not contain (1 string, 5 words max; checks first 1,000 words) |
category |
enum | - | See category table below |
userLocation |
string | - | Two-letter ISO country code |
moderation |
boolean | false | Moderate results for safety (SDK) |
flags |
string[] | - | Experimental flags (SDK) |
context |
boolean/object | - | Return combined context string for RAG |
contents |
object | - | Control text/highlights/summary retrieval |
Categories
| Category | Description |
|---|---|
news |
News articles |
research paper |
Academic papers |
pdf |
PDF documents |
github |
GitHub repositories |
tweet |
Twitter/X posts |
personal site |
Personal websites/blogs |
financial report |
Financial documents |
company |
Company profiles |
people |
People profiles |
Notes: The official
exa-pySDK exposesmoderationandflags, but they are not currently documented inexa-openapi-spec.yamlv1.2.0. Prefer treating them as optional/experimental until confirmed in vendor docs.
Response
{
"requestId": "string",
"results": [
{
"id": "string",
"title": "string",
"url": "string",
"publishedDate": "string|null",
"author": "string|null",
"score": 0.0,
"image": "string|null",
"favicon": "string|null",
"text": "string",
"summary": "string",
"highlights": ["string"],
"highlightScores": [0.0]
}
],
"context": "string (combined content for RAG, if requested)",
"searchType": "string",
"costDollars": {
"total": 0.005,
"breakDown": [
{
"search": 0.005,
"contents": 0.0,
"breakdown": {
"neuralSearch": 0.005,
"deepSearch": 0.0,
"contentText": 0.0,
"contentHighlight": 0.0,
"contentSummary": 0.0
}
}
],
"perRequestPrices": {
"neuralSearch_1_25_results": 0.005,
"neuralSearch_26_100_results": 0.025,
"deepSearch_1_25_results": 0.015,
"deepSearch_26_100_results": 0.075
},
"perPagePrices": {
"contentText": 0.001,
"contentHighlight": 0.001,
"contentSummary": 0.001
}
}
}
Note:
searchTypeindicates which search method was used. Fortype="auto", this shows the actual method selected (e.g., "neural" or "deep").Contents note: You may see
text: trueused at the top level in examples; the official SDKs send content options undercontents(e.g.contents: {"text": true}), which supports advanced options likehighlights,summary,subpages, andextras.
Code Examples
cURL:
curl -X POST 'https://api.exa.ai/search' \
-H 'x-api-key: YOUR_EXA_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"query": "Latest research in LLMs", "text": true}'
Python:
from exa_py import Exa
exa = Exa('YOUR_EXA_API_KEY')
results = exa.search("Latest research in LLMs", num_results=10, contents={"text": True})
for result in results.results:
print(f"{result.title}: {result.url}")
JavaScript:
import Exa from 'exa-js';
const exa = new Exa('YOUR_EXA_API_KEY');
const results = await exa.searchAndContents('Latest research in LLMs', { text: true });
Contents Endpoint
POST /contents
Obtain clean, parsed content from URLs with automatic live crawling fallback.
Livecrawl Options
| Option | Description |
|---|---|
never |
Disable live crawling (use cache only) |
fallback |
Livecrawl only when cache is empty |
preferred |
Recommended. Try livecrawl first, fall back to cache if crawling fails |
always |
Always live-crawl (not recommended without consulting Exa) |
auto |
Let Exa choose behavior (SDK) |
Request Body
| Parameter | Type | Description |
|---|---|---|
urls |
string[] | Required. URLs to crawl |
ids |
string[] | Deprecated. Use urls instead |
text |
boolean/object | Full page text. Object: {maxCharacters, includeHtmlTags} |
highlights |
object | Extract snippets: {query, numSentences, highlightsPerUrl} |
summary |
object | LLM summaries: {query, schema} for structured output |
metadata |
boolean/object | Request metadata (SDK) |
livecrawl |
enum | never, fallback, preferred, always, auto |
livecrawlTimeout |
integer | Milliseconds (default 10000) |
filterEmptyResults |
boolean | Drop empty results (SDK) |
subpages |
integer | Number of subpages to crawl |
subpageTarget |
string/string[] | Keywords for subpage filtering |
extras |
object | Additional data: {links: N, imageLinks: N} |
flags |
string[] | Experimental flags (SDK) |
context |
boolean/object | Return combined context string for RAG |
Response
{
"requestId": "string",
"results": [
{
"id": "string",
"title": "string",
"url": "string",
"publishedDate": "string|null",
"author": "string|null",
"score": 0.0,
"image": "string|null",
"favicon": "string|null",
"text": "string",
"highlights": ["string"],
"highlightScores": [0.0],
"summary": "string",
"subpages": [],
"extras": {"links": [], "imageLinks": []}
}
],
"statuses": [
{
"id": "string",
"status": "success|error",
"error": {
"tag": "CRAWL_NOT_FOUND|CRAWL_TIMEOUT|CRAWL_LIVECRAWL_TIMEOUT|SOURCE_NOT_AVAILABLE|CRAWL_UNKNOWN_ERROR",
"httpStatusCode": 404
}
}
],
"context": "string (combined content for RAG, if requested)",
"costDollars": {
"total": 0.001,
"perPagePrices": {
"contentText": 0.001,
"contentHighlight": 0.001,
"contentSummary": 0.001
}
}
}
Code Examples
Python:
from exa_py import Exa
exa = Exa('YOUR_EXA_API_KEY')
results = exa.get_contents(
urls=["https://arxiv.org/abs/2307.06435"],
text=True
)
Find Similar Endpoint
POST /findSimilar
Find semantically related pages to a given URL. Supports the same filtering options as Search.
Request Body
| Parameter | Type | Description |
|---|---|---|
url |
string | Required. Source URL to find similar pages for |
numResults |
integer | Max 100, default 10 |
includeDomains |
string[] | Limit to domains |
excludeDomains |
string[] | Exclude domains |
startPublishedDate |
ISO 8601 | Filter by publish date |
endPublishedDate |
ISO 8601 | Filter by publish date |
startCrawlDate |
ISO 8601 | Filter by crawl date |
endCrawlDate |
ISO 8601 | Filter by crawl date |
includeText |
string[] | Must contain (1 string, 5 words max) |
excludeText |
string[] | Must not contain (1 string, 5 words max; checks first 1,000 words) |
excludeSourceDomain |
boolean | Exclude results from the source URL's domain (SDK) |
category |
enum | A data category to focus on (SDK) |
flags |
string[] | Experimental flags (SDK) |
context |
boolean/object | Return combined context string for RAG |
contents |
object | Configure text/highlights/summary retrieval |
Response
{
"requestId": "string",
"results": [
{
"id": "string",
"title": "string",
"url": "string",
"publishedDate": "string|null",
"author": "string|null",
"score": 0.0,
"image": "string|null",
"favicon": "string|null",
"text": "string",
"summary": "string",
"highlights": ["string"],
"highlightScores": [0.0]
}
],
"context": "string (combined content for RAG, if requested)",
"costDollars": {
"total": 0.005
}
}
Code Examples
Python:
from exa_py import Exa
exa = Exa('YOUR_EXA_API_KEY')
results = exa.find_similar(
"https://arxiv.org/abs/2307.06435",
num_results=10,
contents={"text": True},
)
Answer Endpoint
POST /answer
Generate LLM-powered answers with citations from web search.
Request Body
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
string | Required | The question to answer |
stream |
boolean | false | Server-sent events stream |
text |
boolean | false | Include full text in citations |
Response
{
"answer": "string - generated answer",
"citations": [
{
"id": "string",
"url": "string",
"title": "string",
"author": "string|null",
"publishedDate": "string|null",
"image": "string|null",
"favicon": "string|null",
"text": "string"
}
],
"costDollars": {
"total": 0.005,
"breakDown": [
{
"search": 0.005,
"contents": 0.0,
"breakdown": {
"neuralSearch": 0.005,
"deepSearch": 0.0,
"contentText": 0.0,
"contentHighlight": 0.0,
"contentSummary": 0.0
}
}
]
}
}
Code Examples
Python:
from exa_py import Exa
exa = Exa('YOUR_EXA_API_KEY')
result = exa.answer(
"What is the latest valuation of SpaceX?",
text=True
)
print(result.answer)
for citation in result.citations:
print(f" - {citation.title}: {citation.url}")
Research Endpoints
- GET
/research/v1(list) - POST
/research/v1(create) - GET
/research/v1/{researchId}(get / stream)
Async deep research with structured output support.
List Parameters (GET /research/v1)
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor |
string | - | Cursor for pagination (use nextCursor from prior response) |
limit |
int | 10 | Number of results (OpenAPI: 1–50) |
List Response (200)
{
"data": [],
"hasMore": false,
"nextCursor": null
}
Note: The list endpoint wraps items under
data(notitems) and usesnextCursor(notcursor).
Create Request Body
| Parameter | Type | Description |
|---|---|---|
instructions |
string | Required. Research guidelines (max 4096 chars) |
model |
enum | exa-research-fast, exa-research, exa-research-pro (official docs list all three; the Exa OpenAPI spec repo currently omits exa-research-fast, but the API accepts it — verified via live /research/v1 create on 2026-01-13) |
outputSchema |
object | JSON Schema for structured output |
Get Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
stream |
boolean | false | Stream Server-Sent Events (SSE) |
events |
boolean | false | Include events in non-streaming responses |
OpenAPI note: The raw
exa-openapi-spec.yamlcurrently marksstream/eventsas required query params and types them asstring; observed API usage (and SDKs) treat them as optional booleans.
Response (create: 201)
{
"researchId": "string",
"status": "pending|running|completed|canceled|failed",
"createdAt": 1234567890123,
"model": "exa-research",
"instructions": "string"
}
When completed:
{
"researchId": "string",
"status": "completed",
"output": {
"content": "string - research results",
"parsed": {}
},
"costDollars": {
"total": 0.10,
"numSearches": 6,
"numPages": 20,
"reasoningTokens": 1000
}
}
Note: When you provide
outputSchema, the response may include a structuredoutput.parsedin addition tooutput.content.
Code Examples
Python:
from exa_py import Exa
exa = Exa('YOUR_EXA_API_KEY')
# Create research task
research = exa.research.create(
instructions="Summarize latest AI safety research",
model="exa-research"
)
# Poll for results
result = exa.research.poll_until_finished(research.research_id)
Python SDK Reference
Installation
pip install exa-py
# or
uv add exa-py
Quick Start
from exa_py import Exa
# Initialize client
exa = Exa('YOUR_EXA_API_KEY')
# Search (returns text contents by default)
results = exa.search("prediction markets research", num_results=10)
# Access results
for r in results.results:
print(f"Title: {r.title}")
print(f"URL: {r.url}")
print(f"Text: {r.text[:200]}...")
print()
Available Methods
| Method | Description |
|---|---|
search(query, **kwargs) |
Search (API returns content fields only when requested via text/contents) |
search_and_contents(query, **kwargs) |
Search + contents convenience method (used in OpenAPI examples v1.2.0) |
get_contents(urls, **kwargs) |
Get contents from URLs |
find_similar(url, **kwargs) |
Find similar pages |
find_similar_and_contents(url, **kwargs) |
Find similar + contents convenience method (used in OpenAPI examples v1.2.0) |
answer(query, **kwargs) |
Get LLM answer with citations |
stream_answer(query, **kwargs) |
Streaming answer (yields chunks) |
research.create(instructions=..., **kwargs) |
Start async research |
research.get(research_id, **kwargs) |
Get research results |
research.list(cursor=..., limit=...) |
List research tasks |
research.poll_until_finished(research_id, **kwargs) |
Poll for completion |
Note: Structured summaries using
summary={"schema": {...}}return JSON strings that require parsing.
Pricing (as of 2026)
See Exa Pricing for current free credits and plan limits.
Search Pricing (per 1,000 requests)
| Search Type | 1-25 results | 26-100 results |
|---|---|---|
| Fast/Auto/Neural | $5 | $25 |
| Deep | $15 | $75 |
Contents Pricing (per 1,000 pages)
| Content Type | Cost |
|---|---|
| Text | $1 |
| Highlights | $1 |
| Summary | $1 |
Answer Pricing
The Answer endpoint returns costDollars in the response. Use costDollars.total as the source of truth.
Research API Pricing (Variable)
Research API uses consumption-based billing. You're only charged for tasks that complete successfully.
| Component | exa-research | exa-research-pro |
|---|---|---|
| Agent searches (per 1k) | $5 | $5 |
| Agent page reads (per 1k pages*) | $5 | $10 |
| Reasoning tokens (per 1M) | $5 | $5 |
*Page = 1,000 tokens of webpage content
Typical task cost: $0.10-$0.50 depending on complexity (20-40 second completion)
Rate Limits
| Endpoint | Limit |
|---|---|
/search |
5 QPS |
/contents |
50 QPS |
/answer |
5 QPS |
/findSimilar |
5 QPS (assumed, not documented) |
/research/v1 |
15 concurrent tasks |
Note: QPS = Queries Per Second. Research API uses concurrent task limits for long-running operations. Contact hello@exa.ai for Enterprise rate limit increases.
Error Codes
HTTP Status Errors
| Code | Meaning | Action |
|---|---|---|
| 400 | Invalid request parameters, malformed JSON | Validate request format |
| 401 | Missing or invalid API key | Verify credentials |
| 403 | Valid key but insufficient permissions or rate exceeded | Check account/throttle |
| 404 | Resource not found | Confirm resource exists |
| 409 | Resource already exists (Websets) | Use different identifier |
| 429 | Rate limit exceeded | Implement exponential backoff |
| 500 | Server error | Retry after delay |
| 502 | Upstream server issue | Retry after delay |
| 503 | Service temporarily down | Wait and retry |
Contents Endpoint Status Tags
Errors in /contents appear in the statuses field:
| Tag | HTTP Code | Meaning |
|---|---|---|
CRAWL_NOT_FOUND |
404 | URL content unavailable |
CRAWL_TIMEOUT |
408 | Fetch operation timed out |
CRAWL_LIVECRAWL_TIMEOUT |
408 | Live crawl timed out |
SOURCE_NOT_AVAILABLE |
403 | Access denied or paywalled |
CRAWL_UNKNOWN_ERROR |
500+ | Other crawling failures |
Tool Use with Claude (Manual)
For custom tool definitions (or when you need fine-grained control):
import anthropic
from exa_py import Exa
exa = Exa('YOUR_EXA_API_KEY')
client = anthropic.Anthropic()
# Define Exa as a tool
tools = [
{
"name": "web_search",
"description": "Search the web for current information using Exa",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
}
},
"required": ["query"]
}
}
]
# In your tool execution loop
def execute_tool(tool_name, tool_input):
if tool_name == "web_search":
results = exa.search(
tool_input["query"],
num_results=5,
contents={"text": True},
)
return "\n\n".join([
f"**{r.title}**\n{r.url}\n{r.text[:500]}"
for r in results.results
])
Integration Tips for Kalshi Research
Event Resolution
When checking if a market resolved, use category="news" and date filters:
# Find resolution sources for a market expiring this week
results = exa.search(
"Federal Reserve interest rate decision January 2026",
type="deep", # Quality matters for resolution
category="news",
start_published_date="2026-01-06T00:00:00Z",
include_domains=["federalreserve.gov", "reuters.com", "bloomberg.com"],
contents={"text": True},
)
Thesis Research
For creating trading theses, use the Research API with structured output:
research = exa.research.create(
instructions="""
Analyze the likelihood of [EVENT] occurring by [DATE].
Research:
1. Historical precedent
2. Current indicators
3. Expert opinions
4. Contrarian arguments
Provide a probability estimate with confidence interval.
""",
model="exa-research",
output_schema={
"type": "object",
"properties": {
"probability": {"type": "number"},
"confidence_low": {"type": "number"},
"confidence_high": {"type": "number"},
"bull_case": {"type": "string"},
"bear_case": {"type": "string"},
"key_sources": {"type": "array", "items": {"type": "string"}}
}
}
)
News Monitoring
For sentiment analysis and news tracking:
# Find recent news about tracked markets
results = exa.search(
"Bitcoin ETF SEC approval 2026",
type="auto",
category="news",
start_published_date="2026-01-01T00:00:00Z",
num_results=20,
contents={
"text": True,
"highlights": {"num_sentences": 3, "highlights_per_url": 2},
},
)
Domain Filtering for Resolution Sources
When a Kalshi market specifies a "Resolution Source", use include_domains:
# Market resolves based on BLS data
results = exa.search(
"unemployment rate January 2026",
include_domains=["bls.gov"],
contents={
"text": True,
"livecrawl": "preferred", # Get fresh data
},
)
Websets API (Overview)
The Websets API enables programmatic web data discovery and processing at scale. This is a separate API with its own OpenAPI spec.
OpenAPI Spec: exa-websets-spec.yaml
Key Concepts
| Concept | Description |
|---|---|
| Websets | Collections of web data organized around specific searches or imports |
| Items | Individual results within a Webset |
| Searches | Query operations that can be reused or modified |
| Enrichments | AI-generated additional data columns added to results |
| Imports | User-uploaded datasets for deduplication or enrichment |
| Monitors | Scheduled operations to keep Websets updated |
| Webhooks | Integration hooks for external systems |
Use Cases
- Data Collection: Gather web content at scale using natural language criteria
- Data Enhancement: Add structured information via AI-powered enrichment
- Continuous Monitoring: Keep Websets updated on scheduled intervals
- CRM Integration: Connect results with external systems via webhooks
Note: Websets API is documented separately from the core Search/Contents APIs. See the Websets documentation for full details.
See Also
- Kalshi API Reference - Kalshi prediction market API
- Architecture - How our codebase integrates external APIs
- Exa Pricing - Current pricing details