GuidesSEARCH APIS
Brave Search API from JavaScript
Brave Search API answers the browser's preflight with 405 and sends no Access-Control-Allow-Origin, so fetch() from a page fails before the search goes out. The token must travel in the X-Subscription-Token header, which the free proxy refuses with 400 header_not_allowed. cors.dev Pro forwards it for $5 a month for 500,000 requests, with a 7-day trial of 1,000 requests and no card; Brave's Search plan costs $5 per 1,000 requests and includes $5 of free credit every month.
Why the browser blocks Brave Search API calls
Brave's API reference requires the x-subscription-token header on every request to https://api.search.brave.com/res. Web search is GET /v1/web/search?q=..., with q capped at 600 characters and 75 words, count at 20, and results in web.results, each with title, url and description.
A custom header like X-Subscription-Token makes the browser send an OPTIONS preflight first. On 28 October 2026, with Origin: https://example.com set the way a page sets it, Brave answered that preflight with 405 and no CORS headers. The browser stops there, never sends the search, and fetch() rejects with a CORS error.
Brave's real responses carry no Access-Control-Allow-Origin either. The token is accepted only as a header: &token=... in the URL got the same 422 "header required" error as no token at all.
| Request | Status | CORS headers | What your code sees |
|---|---|---|---|
| OPTIONS preflight | 405 | None | CORS error, the search is never sent |
| GET with an invalid token | 422, SUBSCRIPTION_TOKEN_INVALID | None | CORS error |
| GET without a token | 422, header required | None | CORS error |
| Token in the query string | 422, header required | None | CORS error |
| News and suggest endpoints, invalid token | 422, SUBSCRIPTION_TOKEN_INVALID | None | CORS error |
| Free proxy with X-Subscription-Token | 400, header_not_allowed | Yes | Readable proxy error |
| Free proxy without a token | 422, header required | Yes | Readable Brave error |
What the free proxy does with X-Subscription-Token
The free proxy at https://proxy.cors.dev/<target-url> needs no signup, allows GET and HEAD, and handles about 600 requests a minute per IP. It forwards only Accept, Accept-Language, If-None-Match, If-Modified-Since and Range, and it refuses credential headers.
The proxy answers the browser's preflight for X-Subscription-Token itself with 204 and CORS headers, so the real request goes out. It then returns 400 with X-Cors-Error: header_not_allowed and the message "X-Subscription-Token is only accepted with a Pro key; free requests forward Accept, Accept-Language, If-None-Match, If-Modified-Since and Range. The 7-day trial includes 1,000 requests, no card." Your code can read that reason instead of an opaque network error.
Without the token, the request reaches Brave and its 422 comes back with CORS headers. The path works, but there is no free way to authenticate, because Brave reads the token only from the header.
Forward the token with cors.dev Pro
cors.dev Pro forwards X-Subscription-Token. It costs $5 a month for 500,000 requests, and the 7-day trial includes 1,000 requests with no card.
- Sign up
Create an account at cors.dev/signup with Google sign-in.
- Create a Connection
List the exact origins that may use it, such as
https://app.example.comandhttp://localhost:5173, and enableapi.search.brave.comas a target host. - Send both headers
Put the Connection's publishable key in
X-Cors-Keynext toX-Subscription-Tokenon every request.
const PROXY = 'https://proxy.cors.dev/';
const CORS_KEY = 'YOUR_CONNECTION_KEY';
async function braveSearch(query, braveToken) {
const params = new URLSearchParams({ q: query, count: '10' });
const response = await fetch(PROXY + 'https://api.search.brave.com/res/v1/web/search?' + params, {
headers: {
Accept: 'application/json',
'X-Cors-Key': CORS_KEY,
'X-Subscription-Token': braveToken,
},
credentials: 'omit',
});
const proxyError = response.headers.get('X-Cors-Error');
if (proxyError) throw new Error(`cors.dev: ${proxyError}`);
if (!response.ok) throw new Error(`Brave ${response.status}: ${await response.text()}`);
const data = await response.json();
return data.web?.results ?? [];
}
const braveToken = document.querySelector('#brave-token').value;
const results = await braveSearch('cors proxy', braveToken);
for (const { title, url, description } of results) {
console.log(title, url, description);
}With a wrong or unknown X-Cors-Key, the proxy answers 401 with X-Cors-Error: invalid_key. Pro also supports POST, PUT, PATCH and DELETE with request bodies up to 1 MiB, responses up to 6 MiB and a 30-second deadline. Pro allows 3,000 requests a minute per account and 100 in flight; the trial allows 600 a minute and 10 in flight.
Who should hold the subscription token
The X-Cors-Key is a publishable identifier, not a secret: it works only from the origins you registered. Your Brave token is different. In browser code every visitor can read it in the network tab, cors.dev does not hide it, and a leaked token spends the credit on your account.
- Bring-your-own-token apps: Each user pastes their own Brave token and spends their own credit.
- Internal tools: Your team uses your token from origins you control.
- Prototypes: A short-lived demo where exposure of the token is acceptable.
A public product that runs on your own token belongs on a backend that holds the token and calls Brave for your users.
Brave Search API pricing and limits
We read Brave's API page on 28 October 2026. Brave sells prepaid credits, not subscriptions, and asks for a credit card "to prevent abuse". The page headline promises "up to $20 in free calls every month"; the plan cards list $5 of monthly credit each for Search and Answers.
| Plan | Price | Free credit | Capacity |
|---|---|---|---|
| Search | $5 per 1,000 requests | $5 every month, about 1,000 requests | 50 queries per second |
| Answers | $4 per 1,000 requests plus $5 per million input and output tokens | $5 every month | 2 queries per second |
| Enterprise | Custom pricing | Custom terms | Custom capacity |
Alternatives that work from the browser
Brave's own page notes that Microsoft retired the Bing Search APIs and Google closed its Custom Search API to new customers. Two search APIs send CORS headers, so you call them directly with no proxy:
- Serper: Google results as JSON from
google.serper.dev. Its preflight answered204withAccess-Control-Allow-Origin: *and allowsX-API-KEY, an invalid key got a403with the same header, and you get 2,500 free queries with no card. - Tavily:
api.tavily.comanswered withAccess-Control-Allow-Origin: *. Its keyless mode, the headerX-Tavily-Access-Mode: keylesson/searchand/extract, works without an account, and the free plan has 1,000 credits a month.
Good questions.
Can I pass the Brave Search token in the URL instead of a header?
No. On 28 October 2026, &token=... on /res/v1/web/search got a 422 validation error saying the x-subscription-token header is missing. Brave reads the token only from the X-Subscription-Token header.
Do Brave's news and suggest endpoints send CORS headers?
No. /res/v1/news/search and /res/v1/suggest/search with an invalid token answered 422 SUBSCRIPTION_TOKEN_INVALID without Access-Control-Allow-Origin. They need the same header, so they hit the same preflight block as web search.
Why does proxy.cors.dev answer 400 header_not_allowed for Brave?
The free proxy forwards only Accept, Accept-Language, If-None-Match, If-Modified-Since and Range, and refuses credential headers such as X-Subscription-Token with 400 and X-Cors-Error: header_not_allowed. Add an X-Cors-Key from a 7-day trial or Pro Connection to forward the token.
Is it safe to put my X-Cors-Key in frontend code?
Yes. The X-Cors-Key is a publishable identifier that works only from the origins listed on your Connection, such as https://app.example.com or http://localhost:5173. Your Brave X-Subscription-Token stays visible in the network tab to anyone using the page.
Can I cache Brave Search responses through cors.dev Pro?
Only when Brave's response allows it. On a GET, send X-Cors-Cache: 60, or any value from 1 to 300 seconds; Pro stores the reply only if its status and cache headers permit, and X-Cors-Cache-Status and X-Cors-Cache-Reason report what happened. Cache hits still count as requests.