brave-search

Brave Search API bindings for Hot: web search from Brave's independent index, with country, language, safesearch, and freshness filters, and site operators in the query.

Installation

Add this to the deps in your hot.hot file:

"hot.dev/brave-search": "0.1.0"

Setup

Context VariableDescription
brave.api.keySubscription token from the Brave Search API dashboard, sent as X-Subscription-Token

The API key can also be passed per call; see Per-call credentials.

Quick start

::bs ::brave::search

out ::bs/search("rust jit compiler", {count: 5, freshness: "pm"})
map(out.web.results, (r) { `${r.title} - ${r.url}` })

// Restrict sites with operators in the query
docs ::bs/search("release notes site:hot.dev NOT site:example.com", {count: 5})

Options are passed through as Brave documents them (snake_case: search_lang, extra_snippets, ...). Unset (null) options are dropped rather than sent.

Functions

FunctionEndpointReturns
search(query), search(query, options)GET /web/search{web: {results: [{title, url, description}]}, ...}

search options:

OptionMeaning
countResults per request, max 20
offsetPage index, max 9
country, search_langAs Brave documents them
safesearch"off", "moderate", or "strict"
freshness"pd", "pw", "pm", "py", or a range like "2026-01-01to2026-06-30"
extra_snippetstrue for more snippets per result

Errors come back as an Err. With no key set, the Err names what to set. A failed call carries ::brave::search::http/HttpError (status, a readable message, headers, body).

Per-call credentials

search takes an optional trailing opts Map, so one run can act for many tenants and parallel flows don't share credentials. Credentials in opts are masked in call records, and nothing from opts is written to ctx.

KeyEffect
api-keyThe subscription token for this call, instead of brave.api.key

opts always comes last. Where a function already takes options, pass {} for those.

tenant {api-key: "tenant-b-token"}
out ::brave::search/search("rust jit compiler", {count: 5}, tenant)

An api-key that is present but blank or null is an error naming the key, never a fallback to brave.api.key. ::brave::search::api/resolve(opts) is the one place this is decided.

Notes and limits

  • Snippets contain markup. Titles and descriptions come back as Brave sends them, with matched terms in <strong> and HTML entities such as &amp;.
  • Site operators are best-effort. Brave documents its search operators as experimental, so site:/NOT site: filtering may not be exact.
  • offset stops at 9 pages of 20.
  • Brave replaced its free plan in 2026 with a monthly credit that needs a card on file. There is no sandbox, so the integration test makes one small live search.

License

Apache-2.0 - see LICENSE