Tavily
Tavily searches the live web and pulls pages back as text for agents. Here that is /v1/websearch for the search and /v1/crawl for the page.
Tavily gives an agent two primitives: search the live web, and read a page.
/v1/websearch (6 operations) answers the first and /v1/crawl (1) answers the
second, with /v1/websearch/scrape (1) beside it for callers that already
speak the firecrawl envelope.
Start here
Mint a key, search the live web, then read one of the URLs that came back — Tavily's single search() is two calls here.
# 1. mint a key — sk- belongs on a server, pk- is safe in a browser
curl -sS -X POST https://api.hanzo.ai/v1/account/keys \
-H "Authorization: Bearer $HANZO_SESSION" \
-H 'Content-Type: application/json' \
-d '{"type":"secret"}'
# 2. search — answers {query, number_of_results, results:[{url, title, content, engine}]}
curl -sS -X POST https://api.hanzo.ai/v1/websearch \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"q": "Who is Leo Messi?"}'
# 3. read one of those URLs — step 2 gave snippets, this gives the page
curl -sS -X POST https://api.hanzo.ai/v1/crawl \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"url": "https://en.wikipedia.org/wiki/Lionel_Messi"}'Step 2 returned engine snippets with the engine named on each hit; step 3 turned
one of those URLs into data.markdown. That is the whole shape of the move —
Tavily's one round trip is two, and both calls fail soft, so an empty results
array and a success: false are answers at 200 rather than outages.
Core capabilities
| Capability | What it does | Operations |
|---|---|---|
/v1/websearch | Searches the live web. Keyless public engines run concurrently, hits are merged and deduplicated by normalised URL, capped at 30. | 7 |
/v1/crawl | Fetches one URL from inside the cluster and answers with the page as markdown, plus what the page said about itself. | 1 |
Nouns
| Tavily | Hanzo |
|---|---|
client.search(query) | POST /v1/websearch — q, and language to narrow the locale |
POST https://api.tavily.com/search | POST https://api.hanzo.ai/v1/websearch |
A result's title, url, content | The same three, plus engine — which engine found it |
| A SearXNG-shaped client you already wrote | GET /v1/websearch/search?q= — the /search?format=json envelope, unchanged |
client.extract(urls) | POST /v1/crawl — one URL, answered as markdown |
raw_content | data.markdown |
failed_results | success: false with the reason in error, at 200 |
| A firecrawl-shaped client | POST /v1/websearch/scrape — {success, data: {markdown, metadata}} |
Search takes a validated principal and nothing else. The results are public web pages, identical for every caller, so there is no tenant to scope and nothing that can leak between orgs.
The call
Tavily:
from tavily import TavilyClient
client = TavilyClient(api_key="tvly-YOUR_API_KEY")
response = client.search("Who is Leo Messi?")
for result in response["results"]:
print(result["title"], result["url"])
extracted = client.extract(urls=["https://en.wikipedia.org/wiki/Lionel_Messi"])
print(extracted["results"][0]["raw_content"])Hanzo:
# Search. Answers {query, number_of_results, results:[{url, title, content, engine}]}.
curl -X POST https://api.hanzo.ai/v1/websearch \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"q": "Who is Leo Messi?"}'
# Read one page. Answers {success, data:{url, title, markdown, metadata}, error}.
curl -X POST https://api.hanzo.ai/v1/crawl \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"url": "https://en.wikipedia.org/wiki/Lionel_Messi"}'Both fail soft and both fail soft in a checkable way. An engine that errors,
times out, or is served a bot-challenge page contributes zero results and never
fails the call, so an empty results means nothing was found rather than
something broke — and the array is always present, never null. A page that could
not be fetched answers 200 with success: false, so read success before you
read data, and keep non-2xx for your own mistakes.
What does not carry
A search result's content is the engine's snippet, not the page. Tavily
gives you extracted page text in the search response. Here search returns
snippets and reading the page is a second call to /v1/crawl. An agent loop
that assumed one round trip needs two.
No answer, no context string. include_answer, qna_search and
get_search_context fold search and a model into one call. Search returns
results. Compose the answer yourself with POST /v1/chat/completions over the
results you got.
No crawl and no map. client.crawl() walks a site to a depth and
client.map() returns its structure. POST /v1/crawl fetches exactly one URL —
batching would make the answer a partial-failure envelope every caller then has
to unpack. Walking a site is a loop you write.
No research task. client.research() is an agent: it plans, searches, reads
and writes a cited report, and you poll a request_id for it. There is no
equivalent single call. Plan the loop yourself: search, read the pages you
picked, then compose the report.
No search options. There is no search_depth, topic, max_results,
include_domains or exclude_domains. Engines run concurrently, hits are merged
and deduplicated by normalised URL, and the list is capped at 30. Ranking is
deterministic rather than scored: the first configured engine's hits lead.
/v1/websearch/scrape is not the one to reach for. It is the
service-to-service endpoint and takes the shared service key as its Authorization
Bearer — a validated principal does not substitute for it. Use /v1/crawl,
which takes either.
How is this guide?