Brave
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
- Filters
startPublishedDate, endPublishedDate - Fields
publishedDate, favicon, text
Address it
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
| Call | Endpoint |
|---|---|
search | GET /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 sendtext_decorations=false, and it escapesdescriptionlike HTML (',&,"). The adapter turns the bolding off and decodes the escapes, sosnippetis plain text.titleandextra_snippetscome 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, justX-RateLimit-Reset: 1, 56000next toX-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 isRateLimitErrorright away. freshnesswantsYYYY-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 becomes1970-01-01and a missing end becomes today's UTC date. The open forms2026-06-01toandto2026-06-01answered 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=trueand leavestextempty.
Where it lives
src/providers/brave.ts. Smallest search adapter with pagination, copy it when you write a new one.