Create websearch
Searches the live web and answers with ranked results.
POST /v1/websearch
| Address | https://api.hanzo.ai/v1/websearch |
| Method | POST |
| Operation | search_web |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Searches the live web and answers with ranked results.
This is the fleet's path to what is happening RIGHT NOW — today's weather, an
outage, a release that postdates any model's training. q is the query and
language narrows it to a locale. The answer is {query, number_of_results, results:[{url, title, content, engine}]}, where content is the ENGINE's
snippet and not the page: read a page with POST /v1/crawl.
It is served in-process by a Go meta-search over keyless public engines — never a third-party search API and never a search key. The enabled engines run concurrently and their hits are merged, deduplicated by normalised URL (host and path, trailing slash and fragment dropped, query kept, so distinct queries stay distinct results) and capped at 30. Ranking is deterministic rather than scored: the first configured engine's hits lead.
It fails SOFT on the engines. One that errors, times out or is served a
bot-challenge page contributes zero results and never fails the call, so an
empty results is a real answer — nothing was found — and not an outage. The
array is always present, never null.
Two refusals in the order they have to be asked, both in the PREAMBLE. A typed op is also an MCP tool, a call-plane operation, a graph field and a CLI command, and every one of those invokes it with no route and therefore no middleware — so what admits a caller here is asked where every caller reaches it rather than in a middleware only one of them passes through.
A VALIDATED PRINCIPAL IS REQUIRED, and there is no tenant beyond that: the results are public web pages, identical for every caller, so nothing here is scoped and nothing here can leak across orgs.
THEN THE ANTI-FORGERY TOKEN, immediately before the money, because that is what it is about. This search is the SAME bought meta-search the compat endpoint runs — the engines cost, and account.Shared/meter.go bills the caller's ledger for the answer — so a page the caller never visited must not be able to spend for them by sending their browser here with a cookie they already hold. Nothing leaks; the answer is unreadable cross-origin. What moves is money.
It is account's control, the one every operation in this estate asks, and it is a no-op the moment a caller PRESENTS a credential (Bearer, gateway, API key) — which is every service and console caller here — so it costs a CLI, an agent and an API client nothing. Only the ambient-cookie path is asked for the echoed token. The raw /v1/websearch/search route asks the same control on its group (see Mount), so the two addresses of one search are admitted alike.
Request
2 fields, body application/json (required).
| Field | In | Type | Required | Description |
|---|---|---|---|---|
language | body | string | — | Language narrows the engines to a locale, BCP-47-ish ("en", "ja", "de"). |
q | body | string | — | Q is the query. |
Response
| Status | Body | Meaning |
|---|---|---|
200 | webSearchResults | ok |
200 body — 11 fields.
| Field | In | Type | Always | Description |
|---|---|---|---|---|
engines | body | webEngine[] | — | Engines is one entry per engine asked, in the order they were asked. |
engines[].name | body | string | — | Name is the engine, matching the engine stamped on each result. |
engines[].outcome | body | string | — | Outcome is "answered", "blind" or "failed" — see outcome.go. |
engines[].results | body | integer | — | Results is how many hits this engine contributed, before the merge deduplicated them against the others. |
number_of_results | body | integer | — | NumberOfResults is len(results) — what this answer carries, never an estimate of what the web holds. |
query | body | string | — | Query is the query that ran, echoed back. |
results | body | webResult[] | — | Results are the merged hits, deduplicated by normalised URL and capped at 30. |
results[].content | body | string | — | Content is the ENGINE's snippet — the few lines shown under the title, not the page's text. |
results[].engine | body | string | — | Engine names the backend that found this hit, so one engine's view of a query can be told from another's. |
results[].title | body | string | — | Title is the page's title. |
results[].url | body | string | — | URL is the page's address, as the engine reported it. |
Failure carries the platform error shape — see Errors.
Examples
hanzo websearch createimport { Configuration, WebsearchApi } from 'hanzoai';
const api = new WebsearchApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.searchWeb({ language: "<language>", q: "<q>" });from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import WebsearchApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = WebsearchApi(client).search_web(language="<language>", q="<q>")cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.WebsearchAPI.SearchWeb(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, websearch_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = websearch_api::search_web(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.WebsearchApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new WebsearchApi(client).searchWeb();curl -X POST https://api.hanzo.ai/v1/websearch \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"language": "<language>",
"q": "<q>"
}'Tool websearch, op search_web — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "websearch",
"arguments": {
"op": "search_web",
"input": {
"language": "<language>",
"q": "<q>"
}
}
}
}'How is this guide?