Exa runs its own semantic index rather than scraping a search engine’s results page, and its API is built around four actions that compose: search finds candidate pages by meaning rather than keyword match, contents pulls clean text (or highlights, or a summary) out of pages you already have, findSimilar expands from one good page to others like it, and answer skips straight to a cited, synthesized answer when that’s what you actually want. NativePort proxies all four under one key. This guide chains them into a small research pipeline: discover, expand, then answer.
What you’ll need
- A NativePort API key. Sign up to get a key and $5 in credits.
- Python 3, standard library only —
urllib.request, nopip install. - Your key exported as an environment variable, never hardcoded:
export NATIVEPORT_API_KEY="np_..."
The four actions
All four sit behind the same key and the same Authorization: Bearer <NATIVEPORT_API_KEY> header — only the path changes, and each request/response body is exactly Exa’s own documented shape, untouched by the gateway.
POST https://api.nativeport.ai/exa/search
POST https://api.nativeport.ai/exa/contents
POST https://api.nativeport.ai/exa/findSimilar
POST https://api.nativeport.ai/exa/answer
import json
import os
import urllib.request
GATEWAY = "https://api.nativeport.ai"
API_KEY = os.environ["NATIVEPORT_API_KEY"]
def exa(action, body):
data = json.dumps(body).encode()
req = urllib.request.Request(f"{GATEWAY}/exa/{action}", data=data, method="POST")
req.add_header("Authorization", f"Bearer {API_KEY}")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
Step 1 — search, with contents bundled
query is the only required field. Exa’s type parameter picks a latency/depth tier — auto (the default, a sensible starting point), instant and fast for latency-sensitive calls, deep and deep-reasoning when a query needs Exa’s own multi-step research pass rather than a single index lookup. On /search specifically, text/highlights/summary have to be nested inside a contents object rather than passed at the top level — that’s how you get page content back in the same round trip as the search itself, instead of a second call:
results = exa("search", {
"query": "how retrieval-augmented generation reduces hallucination",
"numResults": 5,
"contents": {
"text": {"maxCharacters": 1000},
"highlights": {"query": "hallucination reduction"},
},
})["results"]
for r in results:
print(r["title"], r["url"])
Each item in results carries id, url, title, publishedDate, author, and — because contents was requested inline — text and highlights too. costDollars on the top-level response reports what that call actually cost, split by component (search vs. contents).
Step 2 — pull contents standalone, or expand with findSimilar
The same text/highlights/summary fields work as a standalone call against /contents when you already have IDs or URLs and don’t want to re-run a search — the difference from step 1 is that on /contents those fields sit at the top level, not nested:
pages = exa("contents", {
"ids": [results[0]["id"]],
"text": True,
})["results"]
findSimilar takes a single URL and returns other pages Exa’s index considers related by meaning, not by shared keywords — useful for broadening a research pipeline past whatever the original query phrasing happened to surface:
similar = exa("findSimilar", {
"url": results[0]["url"],
"numResults": 5,
"excludeSourceDomain": True,
"contents": {"text": {"maxCharacters": 500}},
})["results"]
excludeSourceDomain keeps the expansion from just returning more pages off the same site as your seed URL — worth leaving on for genuine topic breadth.
Step 3 — answer
When the end goal is a synthesized, cited answer rather than a list of pages to read yourself, /answer skips straight there:
answer = exa("answer", {
"query": "does RAG measurably reduce hallucination rates compared to a model with no retrieval?",
"text": True,
})
print(answer["answer"])
for c in answer["citations"]:
print("-", c["url"])
text: true includes the full cited passages alongside each citation, not just the URL. There’s no model or systemPrompt field on this endpoint — unlike /search, /answer’s only real inputs are the query itself and whether you want streaming (stream: true, server-sent events) or the citation text inline.
Putting it together
A minimal pipeline: search with contents bundled, expand the strongest result with findSimilar, then ask /answer the actual question once you have a sense of what’s out there:
def research(question):
hits = exa("search", {
"query": question,
"numResults": 5,
"contents": {"highlights": {"query": question}},
})["results"]
if hits:
exa("findSimilar", {
"url": hits[0]["url"],
"numResults": 5,
"excludeSourceDomain": True,
}) # broaden coverage; folded into your own ranking/synthesis if you're not using /answer
return exa("answer", {"query": question, "text": True})
result = research("does RAG measurably reduce hallucination rates?")
print(result["answer"])
Whether you stop at search+contents and synthesize with your own model, or let /answer do the synthesis, depends on whether you need control over the sources going into the final answer or just want the answer itself.
Errors you’ll actually hit
402 Payment Required(from NativePort): the NativePort balance is at $0. Every request answers this deterministically, so top up and retry.401 Unauthorized: theAuthorization: Bearer <NATIVEPORT_API_KEY>header is missing, malformed, or doesn’t resolve to an account.403 Forbidden: the key is valid but the account isn’t active.404on any path outsidesearch,contents,findSimilar,answer: not rate-limited, just not a mounted route — Exa’s async/researchAPI in particular isn’t exposed through this proxy.400/422(from Exa): malformed or invalid request parameters — check required fields per action (queryfor search/answer,urlfor findSimilar,ids/urlsfor contents).429(from Exa): Exa’s own documented per-second rate limits (tightest onsearch,findSimilarandanswer; looser oncontents) — back off before retrying.- Exa’s API has no dedicated “out of credits” status distinct from the codes above — a funding problem on Exa’s own account, if it ever happens, would surface as a
401/403-shaped failure rather than a specific code.
Security
Treat NATIVEPORT_API_KEY like any other credential: environment variable or secret store, never a literal string in source, never logged. Exa’s own key never reaches your process — the gateway injects it server-side into every call.
What this costs
Exa’s response carries its own costDollars breakdown, and that’s what gets metered — no NativePort markup on top. Search, contents, findSimilar and answer are priced independently (search scales with result count and depth tier; contents and answer both scale with page count and content type requested), so current per-request rates are worth checking on the Exa provider page rather than assumed from one call. A zero balance answers every request with 402 instead of degrading. Adding credit carries a flat 5.5% fee on top-ups between $10 and $5,000 — never on the calls themselves. Full breakdown: pricing.
Where to go next
- Exa provider page: current pricing and benchmark standing.
- Sourced answers leaderboard: how Exa’s
/answerendpoint scores against the field. - Build a cited web research pipeline: the same discover-then-synthesize shape, built instead from Serper search plus Jina Reader — useful if you want raw SERP results rather than a semantic index.
- Pricing: the full billing model.