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.

Brave Search API requests from browser JavaScript, checked October 2026
RequestStatusCORS headersWhat your code sees
OPTIONS preflight405NoneCORS error, the search is never sent
GET with an invalid token422, SUBSCRIPTION_TOKEN_INVALIDNoneCORS error
GET without a token422, header requiredNoneCORS error
Token in the query string422, header requiredNoneCORS error
News and suggest endpoints, invalid token422, SUBSCRIPTION_TOKEN_INVALIDNoneCORS error
Free proxy with X-Subscription-Token400, header_not_allowedYesReadable proxy error
Free proxy without a token422, header requiredYesReadable 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.

  1. Sign up

    Create an account at cors.dev/signup with Google sign-in.

  2. Create a Connection

    List the exact origins that may use it, such as https://app.example.com and http://localhost:5173, and enable api.search.brave.com as a target host.

  3. Send both headers

    Put the Connection's publishable key in X-Cors-Key next to X-Subscription-Token on every request.

Brave web search through cors.dev Pro
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.

Brave Search API plans, checked October 2026
PlanPriceFree creditCapacity
Search$5 per 1,000 requests$5 every month, about 1,000 requests50 queries per second
Answers$4 per 1,000 requests plus $5 per million input and output tokens$5 every month2 queries per second
EnterpriseCustom pricingCustom termsCustom 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 answered 204 with Access-Control-Allow-Origin: * and allows X-API-KEY, an invalid key got a 403 with the same header, and you get 2,500 free queries with no card.
  • Tavily: api.tavily.com answered with Access-Control-Allow-Origin: *. Its keyless mode, the header X-Tavily-Access-Mode: keyless on /search and /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.