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=wetterreturned German suggestions, and a Japanese query withhl=jacame back as clean UTF-8. - gl: Sets the country. For
q=cors,hl=en&gl=usadded "corsets near me", whilehl=fr&gl=frled with "corse" and "corsica ferries". - client=chrome: Returns 15 suggestions plus
google:suggestrelevance,google:suggesttypeandgoogle:verbatimrelevance, in ISO-8859-1. - ds=yt and output=toolbar: YouTube suggestions, and an XML version of the list.
| Request | Content type | Bytes | What you get |
|---|---|---|---|
| client=firefox&q=cors | text/javascript, UTF-8 | 258 | 10 suggestions |
| client=chrome&q=cors | text/javascript, ISO-8859-1 | 735 | 15 suggestions with relevance scores |
| output=toolbar&q=cors | text/xml, ISO-8859-1 | 739 | CompleteSuggestion elements |
| ds=yt&client=firefox&q=cors | text/javascript, UTF-8 | 304 | YouTube suggestions |
| hl=de&client=firefox&q=wetter | text/javascript, UTF-8 | 335 | German 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.
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.
- Keep a timer and a controller
Both live outside the
inputhandler. - Restart the timer on every keystroke
clearTimeout()drops the pending call, so only a 250 ms pause lets a request through. - Abort, then fetch
When the timer fires, abort the previous request, create a new controller, pass its
signaltofetch()and fill the<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,200with CORS through the free proxy. The default shape is[{"phrase":"corsair"},...];type=listreturns 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,200with CORS through the proxy. - Brave (
https://search.brave.com/api/suggest?q=cors):200from our test server, but withAccess-Control-Allow-Origin: https://brave.com, so browsers block it on other origins;200with CORS through the proxy. The officialapi.search.brave.com/res/v1/suggest/searchneeds anX-Subscription-Tokenheader, which the free proxy refuses with400 header_not_allowed. - Serper (
POST https://google.serper.dev/autocomplete): Sends CORS headers, withX-API-KEYallowed 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.
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", ...]| Endpoint | Key | Direct fetch() | Through the free proxy | Format |
|---|---|---|---|---|
| Google suggestqueries | None | Blocked, no CORS header | 200 with CORS | [query, [suggestions]] |
| DuckDuckGo ac | None | Blocked, no CORS header | 200 with CORS | Phrase objects, or a list with type=list |
| Bing osjson | None | Blocked, no CORS header | 200 with CORS | [query, [suggestions]] |
| Brave search suggest | None | Blocked, CORS only for brave.com | 200 with CORS | [query, [suggestions]] |
| Brave Suggest API | X-Subscription-Token header | Blocked, preflight 405 | 400 header_not_allowed | JSON object |
| Serper autocomplete | X-API-KEY header | Works, CORS headers sent | Not needed | JSON object |
| SerpApi google_autocomplete | api_key in the URL | 401, no CORS header | Works, key visible | JSON 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.