Providers

Brave

The Brave Search API, one GET per page, with extra snippets joined into text and a continuation that carries the offset.
IDbrave01 / 13api.search.brave.com
Provider

Brave

  • create("brave")
  • BRAVE_API_KEY
Auth
X-Subscription-Token header
Results
10, up to 20 · paged
Free tier
2k queries a month

Capabilities

  • Searchsearch()
  • Imagenot supported
  • Readnot supported

Options

FiltersstartPublishedDate, endPublishedDate
FieldspublishedDate, favicon, text

Address it

ts
import { create } from "@agntn/web";

const brave = await create("brave"); // BRAVE_API_KEY
const results = await brave.search("typescript runtimes", { maxResults: 10 });
const recent = await brave.search("typescript runtimes", { startPublishedDate: "2026-06-01" });

The key goes in the X-Subscription-Token header. Pass apiKey to create() if you do not want env involved.

What it reads

CallEndpoint
searchGET /res/v1/web/search?q=…&count=…&offset=…&extra_snippets=true&text_decorations=false on api.search.brave.com, plus freshness=YYYY-MM-DDtoYYYY-MM-DD when a date bound is set

count is maxResults, 20 at most. offset comes from the continuation and steps through pages of that size. query.more_results_available in the response decides whether the next token is next or end, so Brave is one of the few engines that actually tells you.

What comes back

snippet is the description with Brave's HTML escapes decoded. favicon comes from meta_url.favicon, and text is the extra_snippets joined with newlines when Brave sends them. publishedDate is Brave's page_age, the page's published or last modified date as Brave read it, so it can be missing. startPublishedDate and endPublishedDate go out as freshness, the only filter Brave has here. No score, and includeDomains and friends come back in ignoredFilters.

Gotchas

  • A page is 20 results max. Ask for more and you get 20 plus a continuation.
  • Brave bolds query terms with <strong> unless you send text_decorations=false, and it escapes description like HTML (&#x27;, &amp;, &quot;). The adapter turns the bolding off and decodes the escapes, so snippet is plain text. title and extra_snippets come as text already.
  • The free plan is a couple of thousand queries a month and one query a second. The second query inside that second gets a 429 with no Retry-After, just X-RateLimit-Reset: 1, 56000 next to X-RateLimit-Remaining: 0, 445, one value per window. The client waits out the window that ran dry and asks again, so a batch of five queries takes about four seconds instead of losing three of them. An empty monthly quota names days, not seconds, and that one is RateLimitError right away.
  • freshness wants YYYY-MM-DDtoYYYY-MM-DD, both days inside the window, the same day twice gives that day's pages. Anything else, a time on the date included, Brave drops without a word and answers unfiltered, so a datetime bound becomes its UTC day here, a missing start becomes 1970-01-01 and a missing end becomes today's UTC date. The open forms 2026-06-01to and to2026-06-01 answered correctly when probed, but Brave does not document them, and a filter that fails silently is not the place to lean on that.
  • Brave only fills extra snippets on a Search plan. A free key still takes extra_snippets=true and leaves text empty.

Where it lives

src/providers/brave.ts. Smallest search adapter with pagination, copy it when you write a new one.