# Tavily

URL: https://docs.hanzo.ai/docs/guides/migrate/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.

```bash
# 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:

```python
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:

```bash
# 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.
