Hanzo AI

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

CapabilityWhat it doesOperations
/v1/websearchSearches the live web. Keyless public engines run concurrently, hits are merged and deduplicated by normalised URL, capped at 30.7
/v1/crawlFetches one URL from inside the cluster and answers with the page as markdown, plus what the page said about itself.1

Nouns

TavilyHanzo
client.search(query)POST /v1/websearchq, and language to narrow the locale
POST https://api.tavily.com/searchPOST https://api.hanzo.ai/v1/websearch
A result's title, url, contentThe same three, plus engine — which engine found it
A SearXNG-shaped client you already wroteGET /v1/websearch/search?q= — the /search?format=json envelope, unchanged
client.extract(urls)POST /v1/crawl — one URL, answered as markdown
raw_contentdata.markdown
failed_resultssuccess: false with the reason in error, at 200
A firecrawl-shaped clientPOST /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?