GuidesSEARCH APIS

Google autocomplete API in JavaScript

Google's suggest endpoint, suggestqueries.google.com/complete/search, answers 200 with a JSON array of 10 suggestions but sends no Access-Control-Allow-Origin header, so fetch() from a page fails. Prefix it with https://proxy.cors.dev/: the free proxy needs no signup, allows GET and HEAD and about 600 requests a minute per IP, and returned the same 258 bytes with CORS headers in our 28 October 2026 test. The endpoint is unofficial and undocumented, so debounce requests, use client=firefox for UTF-8 and keep suggestions optional.

The suggest endpoint and its response

Google's search-box suggestions come from https://suggestqueries.google.com/complete/search, an endpoint Google never documented. On 28 October 2026, GET ...?client=firefox&q=cors answered 200 with text/javascript; charset=UTF-8 and a 258-byte JSON array: element 0 is your query, element 1 holds 10 suggestion strings, the rest is metadata. https://www.google.com/complete/search returned the same 258 bytes.

  • hl: Sets the language. hl=de&q=wetter returned German suggestions, and a Japanese query with hl=ja came back as clean UTF-8.
  • gl: Sets the country. For q=cors, hl=en&gl=us added "corsets near me", while hl=fr&gl=fr led with "corse" and "corsica ferries".
  • client=chrome: Returns 15 suggestions plus google:suggestrelevance, google:suggesttype and google:verbatimrelevance, in ISO-8859-1.
  • ds=yt and output=toolbar: YouTube suggestions, and an XML version of the list.
Google suggest variants, checked October 2026
RequestContent typeBytesWhat you get
client=firefox&q=corstext/javascript, UTF-825810 suggestions
client=chrome&q=corstext/javascript, ISO-8859-173515 suggestions with relevance scores
output=toolbar&q=corstext/xml, ISO-8859-1739CompleteSuggestion elements
ds=yt&client=firefox&q=corstext/javascript, UTF-8304YouTube suggestions
hl=de&client=firefox&q=wettertext/javascript, UTF-8335German suggestions

Why fetch() fails and the proxy fix

Google answers 200 but sends no Access-Control-Allow-Origin header, so the browser hides the body and fetch() rejects with a TypeError. Prefix the URL with https://proxy.cors.dev/: the same request answered 200 with Access-Control-Allow-Origin set to the page origin and the identical 258 bytes. https://proxy.cors.dev/?url=<percent-encoded URL> works too.

Google suggestions through the free proxy
const SUGGEST = 'https://proxy.cors.dev/https://suggestqueries.google.com/complete/search?';

async function googleSuggest(query, language = 'en') {
  const params = new URLSearchParams({ client: 'firefox', hl: language, q: query });
  const response = await fetch(SUGGEST + params);

  const proxyError = response.headers.get('X-Cors-Error');
  if (proxyError) throw new Error(`cors.dev: ${proxyError}`);

  const [, suggestions] = await response.json();
  return suggestions;
}

console.log(await googleSuggest('cors'));
// ["corsica", "corsair", "corset", "corsair cove", ...]

The free proxy needs no signup or key, allows GET and HEAD, passes responses up to 6 MiB and caches nothing (Cache-Control: no-store, no-transform). When it refuses a request, it names the reason in the X-Cors-Error header.

Debounce input, abort stale requests

Fetching on every keystroke sends 16 requests while someone types "javascript fetch". A 250 ms debounce sends one when they pause, and an AbortController cancels the previous request so a slow, stale answer cannot overwrite newer suggestions.

  1. Keep a timer and a controller

    Both live outside the input handler.

  2. Restart the timer on every keystroke

    clearTimeout() drops the pending call, so only a 250 ms pause lets a request through.

  3. Abort, then fetch

    When the timer fires, abort the previous request, create a new controller, pass its signal to fetch() and fill the <datalist>.

Debounced, abortable search box with a datalist
// <input id="search" list="suggestions" autocomplete="off"><datalist id="suggestions"></datalist>
const SUGGEST = 'https://proxy.cors.dev/https://suggestqueries.google.com/complete/search?';
const input = document.querySelector('#search');
const datalist = document.querySelector('#suggestions');
let timer;
let controller;

input.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    controller?.abort();
    const query = input.value.trim();
    if (!query) return datalist.replaceChildren();

    controller = new AbortController();
    try {
      const params = new URLSearchParams({ client: 'firefox', q: query });
      const response = await fetch(SUGGEST + params, { signal: controller.signal });
      const [, suggestions] = await response.json();
      datalist.replaceChildren(...suggestions.map((text) => new Option(text)));
    } catch (error) {
      if (error.name !== 'AbortError') console.error(error);
    }
  }, 250);
});

In our test, four keystrokes 60 ms apart produced one request, which filled the list with 10 options. An aborted request may already have reached the server, so it still counts toward the per-IP limit.

The client=chrome charset trap

client=chrome answers in ISO-8859-1. For münchen, the ü arrived as the single byte fc, and because response.json() always decodes UTF-8, it turned into the replacement character.

Use client=firefox, which returned UTF-8 (c3 bc) without extra parameters, or add ie=UTF-8&oe=UTF-8 to client=chrome, which switched the response to UTF-8.

Other autocomplete endpoints

  • DuckDuckGo (https://duckduckgo.com/ac/?q=cors): No CORS header directly, 200 with CORS through the free proxy. The default shape is [{"phrase":"corsair"},...]; type=list returns Google's [query, [suggestions]] shape.
  • Bing (https://api.bing.com/osjson.aspx?query=cors): Unofficial; Microsoft retired its official Bing Search APIs on 11 August 2025. No CORS header directly, 200 with CORS through the proxy.
  • Brave (https://search.brave.com/api/suggest?q=cors): 200 from our test server, but with Access-Control-Allow-Origin: https://brave.com, so browsers block it on other origins; 200 with CORS through the proxy. The official api.search.brave.com/res/v1/suggest/search needs an X-Subscription-Token header, which the free proxy refuses with 400 header_not_allowed.
  • Serper (POST https://google.serper.dev/autocomplete): Sends CORS headers, with X-API-KEY allowed in the preflight, so call it directly. 2,500 free queries, no card.
  • SerpApi (engine=google_autocomplete): No CORS headers, but the key rides in the URL, so the free proxy returns the answer with CORS headers. A key in your page is public; cors.dev does not hide it.
DuckDuckGo suggestions in Google's format
async function duckSuggest(query) {
  const params = new URLSearchParams({ q: query, type: 'list' });
  const response = await fetch('https://proxy.cors.dev/https://duckduckgo.com/ac/?' + params);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status} ${response.headers.get('X-Cors-Error') ?? ''}`);
  }

  const [, suggestions] = await response.json();
  return suggestions;
}

console.log(await duckSuggest('cors'));
// ["corsica ferries", "corsair", ...]

Autocomplete endpoints from browser JavaScript, checked October 2026
EndpointKeyDirect fetch()Through the free proxyFormat
Google suggestqueriesNoneBlocked, no CORS header200 with CORS[query, [suggestions]]
DuckDuckGo acNoneBlocked, no CORS header200 with CORSPhrase objects, or a list with type=list
Bing osjsonNoneBlocked, no CORS header200 with CORS[query, [suggestions]]
Brave search suggestNoneBlocked, CORS only for brave.com200 with CORS[query, [suggestions]]
Brave Suggest APIX-Subscription-Token headerBlocked, preflight 405400 header_not_allowedJSON object
Serper autocompleteX-API-KEY headerWorks, CORS headers sentNot neededJSON object
SerpApi google_autocompleteapi_key in the URL401, no CORS headerWorks, key visibleJSON object

Rate limits and reliability

Google's 24 July 2015 post "Update on the Autocomplete API" called this "a non-official, non-published API" and said Google would restrict unauthorized access from 10 August 2015. It still answered on 28 October 2026, but with no documentation it can change or stop without notice, so keep suggestions optional and let the search box submit without them.

The free proxy allows about 600 requests a minute per IP, then answers 429 with Retry-After; there is no daily or monthly quota. Each visitor's browser calls the proxy itself, so the limit applies per visitor IP, not per site, and a debounced box stays well under it.

Good questions.

Is there an official Google autocomplete API?

Not for Google's web search suggestions: the suggest endpoint is unofficial, and Google said so in 2015. For search on your own site, the Programmable Search Element has built-in autocomplete, capped with data-autoCompleteMaxCompletions.

Why does fetch() to suggestqueries.google.com fail with a CORS error?

Google answers 200 without an Access-Control-Allow-Origin header, so the browser refuses to hand the body to your code. Prefix the URL with https://proxy.cors.dev/ and the same response arrives with CORS headers.

Why do Google suggestions show garbled characters?

You are probably using client=chrome, which answers in ISO-8859-1, while response.json() decodes UTF-8. Switch to client=firefox or add ie=UTF-8&oe=UTF-8.

How do I get DuckDuckGo suggestions in Google's format?

Add type=list: https://duckduckgo.com/ac/?q=cors&type=list returns ["cors",["corsica ferries",...]], so the same parsing code handles both. Without it you get an array of {"phrase": ...} objects.

How many suggestion requests can a page send through cors.dev?

About 600 a minute per visitor IP on the free tier, with no daily or monthly quota. Past that, the proxy answers 429 with Retry-After, which a 250 ms debounce makes unlikely for one person typing.