Google Custom Search JSON API alternatives: deadlines, options and a migration map
Google has closed the Custom Search JSON API to new customers, and existing customers have until January 1, 2027 to move to something else. If your product calls customsearch/v1 today, you need a replacement for both the data (web results for a query) and the request shape your code was built around.
What Google changed, and when
All dates below come from Google’s own pages, checked 2026-09-17.
| Date | Change | Source |
|---|---|---|
| January 8, 2025 | The Custom Search Site Restricted JSON API endpoints ceased to serve traffic. | Site Restricted JSON API |
| January 20, 2026 | New Programmable Search Engines must use “Sites to search”, covering up to 50 designated domains. | Update sites in your search engine |
| Now | The Custom Search JSON API is closed to new customers. | Custom Search JSON API overview |
| January 1, 2027 | Deadline for existing Custom Search JSON API customers to transition; existing engines set to “Search the entire web” can keep that setting until this date. | Overview, Update sites |
Two details matter for planning. First, the help page notes that once “Search the entire web” is switched off on an existing engine, it cannot be switched back on, so do not toggle it while you evaluate options. Second, for existing customers the overview page still lists the pricing: 100 search queries per day free, then $5 per 1,000 queries, up to 10,000 queries per day. Your bill stays predictable until the deadline; your integration does not survive it.
What the Custom Search JSON API returned
The API returned JSON results from a Programmable Search Engine that you configured, identified by its engine ID (cx) and called with an API key. Per Google’s cse.list reference:
numaccepts integers from 1 to 10: at most ten results per request.startis the index of the first result, sostart=11begins the second page.- The API never returns more than 100 results;
start + numabove 100 is an error. - Filters include
gl(boosts results from a country),hl(interface language),lr(restrict to a language, such aslang_ja),cr(restrict to documents from a country),dateRestrict(d[number],w[number],m[number],y[number]),safe(activeoroff),siteSearchwithsiteSearchFilter,exactTerms,excludeTerms,orTermsandfileType.
The Search response holds queries, searchInformation, spelling, promotions, context and items[], where each item has title, htmlTitle, link, displayLink, snippet, htmlSnippet, formattedUrl, pagemap, mime, fileFormat and a few more.
That is a list of links, not the page a searcher sees. Google’s Programmable Search Engine vs Google.com help page is explicit about the gap: an engine configured to search the entire web is limited to a subset of the Google Web Search corpus, is designed to emphasize results from your own sites, and lacks Google Web Search features such as Oneboxes, real-time results and universal search. If you used Custom Search as a proxy for “what ranks on Google”, your numbers were already an approximation.
What a full SERP response contains
A SERP API reads the Google results page itself. For comparison, this is what a Google Search request to this API (POST /v1/monitor/google) returns under result, per its API reference:
| Field | What it holds |
|---|---|
organicResults[] |
position, title, link, displayedLink, snippet, date, page, sitelinks.inline[] |
peopleAlsoAsk[] |
question, type (LINK, AIOVERVIEW, UNKNOWN), plus title/link/snippet or markdown/sources[] |
relatedSearches[] |
query, link |
ads[] |
position, blockPosition (top, bottom, middle, rhs), type, title, url, domain, description, price |
localResults[] |
local pack entries: title, placeId, rating, reviews, address, phone, hours, links |
shoppingCards[] |
organic product grids: title, price, store, rating |
knowledgeGraph |
title, type, description, website, attributes[], peopleAlsoSearchFor[] |
peopleAreSaying[] |
discussion and forum modules: title, link, date |
aioverview |
with include.aioverview: text, markdown, sources[], citationPills[] |
Modules Google does not show are omitted. For the data model in depth, see typed SERP data and what a SERP contains.
Your options
There is no drop-in replacement that returns the same JSON from the same corpus. Pick by what you used the API for.
1. Google’s suggested path: Agent Search (formerly Vertex AI Search)
Google’s overview page calls Vertex AI Search “a favorable alternative for searching up to 50 domains” and asks customers who need full web search to contact Google. Google Cloud’s documentation now names the product Agent Search (formerly Vertex AI Search) and describes it as search within your public websites and within data stores containing your own data. If your engine searched your own site or a fixed list of domains, this is the closest fit. If you searched the whole web, it is not a like-for-like replacement.
2. Grounding with Google Search (Gemini API)
Grounding with Google Search connects a Gemini model to real-time web content and returns the model’s answer with citations and the search queries it ran. The output is a generated answer, not a ranked list of results, and the documentation points to usage requirements for displaying search suggestions. On the Gemini API pricing page (checked 2026-09-17), Gemini 3.x models list 5,000 free search requests per month shared across those models, then $14 per 1,000 requests; Gemini 2.5 models are billed per grounded prompt. Choose it if you were feeding search results to an LLM anyway. Do not choose it if you need positions, snippets or a stable result list.
3. A SERP API
SERP APIs return the Google results page as JSON. Prices below were read from each vendor’s own page on 2026-09-17; they change, so recheck before you commit.
| Provider | Published price | Notes | Source |
|---|---|---|---|
| SerpApi | Free: 250 searches/month; Starter $25/month for 1,000; Developer $75/month for 5,000; Production $150/month for 15,000 | Free plan throughput listed as 50 per hour | serpapi.com/pricing |
| DataForSEO (Google Organic SERP API) | Standard queue $0.6 per 1K SERPs; Priority $1.2 per 1K; Live $2 per 1K | Page states each SERP contains 10 results; some parameters add cost | dataforseo.com pricing |
| Serper | 2,500 free queries, no credit card | Paid pack prices not verified; check the site | serper.dev |
| This API | 500 free credits per month, no card; a Google Search task costs 3 credits, a synchronous call 5 | Dollar prices on /pricing | /docs/credits |
For a broader field comparison, see best SERP APIs, cheapest SERP API and SerpApi compared.
4. An independent index
Brave’s Search API page (checked 2026-09-17) lists $5 per 1,000 requests with $5 in free credits every month and a capacity of 50 queries per second, and states that the results come from Brave’s own independent index rather than from Google or Bing. That is a valid choice when you need “web results” and not “Google’s results”; rankings will differ from Google’s.
Decision criteria
- Did you search your own sites? Agent Search, or site search you host yourself.
- Did you feed results into an LLM prompt? Grounding with Google Search, or a SERP API if you want to control which results are used.
- Did you measure Google visibility (positions, SERP features, AI Overviews)? A SERP API. Custom Search never reflected the Google results page well for this.
- Do you just need a web index? An independent index such as Brave.
Parameter migration map
For a SERP API migration, most of your request logic carries over. This API’s Google Search request has two shapes:
- Standard shape:
querypluscountry(orgl), with optionalhl,locationoruule,device(desktop,mobile,ios,android) andpages(1 to 10). - URL shape:
url, a complete Google web search URL. Onlyq,gl,hl,uule,num,start,tbsandsafeare applied; other parameters are dropped, and URLs withtbmare rejected.numis read as result depth, rounded up to whole pages of ten, up to ten pages.urlcannot be combined withquery,location,uuleorpages.
| Custom Search parameter | Equivalent here | Notes |
|---|---|---|
q |
query (or q inside url) |
1 to 10,000 characters |
gl |
gl or country |
Here it sets the market the search runs in, not a boost. Must be listed by GET /v1/countries?model=google |
hl |
hl |
Google’s own codes, such as en, de, pt-BR |
num |
pages (standard) or num in url |
Results come in pages of about ten; ask for pages, not results |
start |
start in url |
Custom Search counts from 1 (start=11 is page 2); Google’s web URL counts from 0 (start=10 is page 2) |
dateRestrict |
tbs in url |
d1, w1, m1, y1 correspond to qdr:d, qdr:w, qdr:m, qdr:y |
safe |
safe in url |
active or off |
exactTerms |
"phrase" in the query |
Operator |
excludeTerms |
-term in the query |
Operator |
siteSearch + siteSearchFilter |
site:domain or -site:domain in the query |
Operator |
fileType |
filetype:pdf in the query |
Operator |
orTerms |
OR in the query |
Not documented on Google’s operator help page; test it |
lr, cr |
none | Use country/hl, or filter results yourself |
cx |
none | Results come from Google web search, not a configured engine |
searchType=image |
none | tbm URLs are rejected |
Google does not document tbs values in its help pages; the qdr: values above come from third-party references such as Bright Data’s parameter reference (dated February 26, 2026), which also gives start=10 as page 2. The operator syntax is covered in Google search operators, and every parameter in Google search parameters.
Change one structure instead of mapping it: Custom Search forced a loop of ten-result requests. Here, one request with pages: 5 returns the first five pages with a page number on every organic result. Replace pagination loops with a single request where you can.
Python
Using requests against the HTTP API:
import os
from urllib.parse import urlencode, urlparse
import requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['ANSWERLINE_API_KEY']}"}
DATE_RESTRICT = {"d1": "qdr:d", "w1": "qdr:w", "m1": "qdr:m", "y1": "qdr:y"}
def to_request(cse: dict) -> dict:
"""Translate Custom Search JSON API parameters into a Google Search request body."""
q = cse["q"]
if cse.get("exactTerms"):
q += f' "{cse["exactTerms"]}"'
if cse.get("excludeTerms"):
q += "".join(f" -{term}" for term in cse["excludeTerms"].split())
if cse.get("siteSearch"):
q += f' {"-" if cse.get("siteSearchFilter") == "e" else ""}site:{cse["siteSearch"]}'
if cse.get("fileType"):
q += f" filetype:{cse['fileType']}"
gl = cse.get("gl", "us")
start = int(cse.get("start", 1))
date = cse.get("dateRestrict")
if date and date not in DATE_RESTRICT:
raise ValueError(f"no mapping for dateRestrict={date}")
if start == 1 and not date and "safe" not in cse:
body = {"query": q, "gl": gl}
else:
params = {"q": q, "gl": gl, "num": cse.get("num", 10), "start": start - 1}
if date:
params["tbs"] = DATE_RESTRICT[date]
if "safe" in cse:
params["safe"] = cse["safe"]
body = {"url": "https://www.google.com/search?" + urlencode(params)}
if cse.get("hl"):
body["hl"] = cse["hl"]
return body
def search(cse: dict) -> list[dict]:
"""Return Custom-Search-style items so existing callers keep working."""
r = requests.post(f"{API}/v1/monitor/google", json=to_request(cse), headers=HEADERS, timeout=360)
r.raise_for_status()
return [
{
"title": o["title"],
"link": o["link"],
"displayLink": urlparse(o["link"]).hostname,
"snippet": o.get("snippet", ""),
}
for o in r.json()["result"].get("organicResults", [])
]
print(search({"q": "annual report", "siteSearch": "example.com", "fileType": "pdf", "gl": "us", "hl": "en"}))
TypeScript
const API = "https://api.answerline.dev";
const DATE_RESTRICT: Record<string, string> = { d1: "qdr:d", w1: "qdr:w", m1: "qdr:m", y1: "qdr:y" };
type CseParams = {
q: string; gl?: string; hl?: string; num?: number; start?: number; safe?: "active" | "off";
dateRestrict?: string; exactTerms?: string; excludeTerms?: string;
siteSearch?: string; siteSearchFilter?: "e" | "i"; fileType?: string;
};
function toRequest(cse: CseParams): Record<string, unknown> {
let q = cse.q;
if (cse.exactTerms) q += ` "${cse.exactTerms}"`;
if (cse.excludeTerms) q += cse.excludeTerms.split(/\s+/).map((t) => ` -${t}`).join("");
if (cse.siteSearch) q += ` ${cse.siteSearchFilter === "e" ? "-" : ""}site:${cse.siteSearch}`;
if (cse.fileType) q += ` filetype:${cse.fileType}`;
const gl = cse.gl ?? "us";
const start = cse.start ?? 1;
if (cse.dateRestrict && !DATE_RESTRICT[cse.dateRestrict]) {
throw new Error(`no mapping for dateRestrict=${cse.dateRestrict}`);
}
let body: Record<string, unknown>;
if (start === 1 && !cse.dateRestrict && !cse.safe) {
body = { query: q, gl };
} else {
const params = new URLSearchParams({ q, gl, num: String(cse.num ?? 10), start: String(start - 1) });
if (cse.dateRestrict) params.set("tbs", DATE_RESTRICT[cse.dateRestrict]);
if (cse.safe) params.set("safe", cse.safe);
body = { url: `https://www.google.com/search?${params}` };
}
if (cse.hl) body.hl = cse.hl;
return body;
}
export async function search(cse: CseParams) {
const res = await fetch(`${API}/v1/monitor/google`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ANSWERLINE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(toRequest(cse)),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const { result } = await res.json();
return (result.organicResults ?? []).map((o: { title: string; link: string; snippet?: string }) => ({
title: o.title,
link: o.link,
displayLink: new URL(o.link).hostname,
snippet: o.snippet ?? "",
}));
}
Response mapping
| Custom Search | This API | Notes |
|---|---|---|
items[].title |
organicResults[].title |
|
items[].link |
organicResults[].link |
Google /goto redirects also come as redirectLink |
items[].displayLink, formattedUrl |
organicResults[].displayedLink |
The URL as Google displays it |
items[].snippet |
organicResults[].snippet |
Plain text; no htmlSnippet |
position in items[] + start |
organicResults[].position, page |
Explicit fields |
spelling, pagemap, promotions |
none | |
| (not available) | peopleAlsoAsk, ads, localResults, knowledgeGraph, aioverview, … |
New data to use |
Migration checklist
- Inventory calls. List every call site with its parameters, especially
cxengines restricted to specific sites; those may belong in Agent Search instead. - Decide per use. Site search, LLM grounding, Google visibility and generic web results lead to different products (see the criteria above).
- Translate requests with a function like
to_request, and log any parameter you cannot map instead of dropping it silently. - Collapse pagination. Replace
startloops withpageswhere you want the top N pages. - Run both in parallel for a sample of your own queries before the deadline, and compare what matters to you. Expect differences: Custom Search results came from a subset of the index.
- Budget. A Google Search task costs 3 credits, each extra page 2 more, and a synchronous call adds 2.
pages: 3as an async task is 3 + 2 x 2 = 7 credits. See /docs/credits and cost planning. - Move bulk jobs to async. Batches of up to 500 tasks per call, with webhooks, avoid synchronous timeouts and cost less; see sync, async and webhooks and rank tracking with batches.
- Handle errors. Invalid combinations (for example
urlwithpages, orcountryandglwith different values) return 400 withdetails[]naming the field; see API errors and retries.
Pitfalls
- Off-by-one on
start. Custom Search’sstartis 1-based and Google’s web URLstartis 0-based. - Assuming
glmeans the same thing. In Custom Search it boosts results from a country; in a SERP request it sets the market the search is run in. Results will shift. - Toggling “Search the entire web” off during evaluation. Google’s help page says it cannot be turned back on.
- Waiting for December. Parallel runs, quota planning and customer communication take longer than the code change.
For related reading, see is there an official Google Search API and, if you also used Microsoft’s service, Bing Search API alternatives.
Request fields and response examples are on the Google Search engine page.
Questions
Is the Google Custom Search JSON API shutting down?
Google's overview page states that the Custom Search JSON API is closed to new customers and that existing customers have until January 1, 2027 to transition to an alternative (checked 2026-09-17).
What does Google recommend instead of the Custom Search JSON API?
The same page names Vertex AI Search as an alternative for searching up to 50 domains, and asks customers who need full web search to contact Google. Google Cloud's documentation now calls that product Agent Search (formerly Vertex AI Search).
How many results did the Custom Search JSON API return per request?
The num parameter accepts integers from 1 to 10, and the API never returns more than 100 results for a query, so start plus num above 100 produces an error, according to Google's cse.list reference.
Can I keep my Custom Search parameters when moving to a SERP API?
Most of them map. q, gl and hl carry over directly; exactTerms, excludeTerms, siteSearch and fileType become search operators inside the query; start, dateRestrict and safe move into a Google search URL sent in the url field. lr, cr and cx have no equivalent.
What does a full SERP response include that Custom Search did not?
A Google Search request to this API returns organic results with page numbers and sitelinks, People Also Ask, related searches, ads, local results, shopping cards, the knowledge graph and, on request, the AI Overview. Custom Search results come from a Programmable Search Engine, which Google says lacks features such as Oneboxes and universal search.