Google Custom Search JSON API alternatives and migration
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
No option 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 | A Google Search task costs $1.03 per 1,000 on every tier; prepaid balance, top up from $1 | 100 free requests for new accounts, no card. Tier 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. Google’s help page says Custom Search searches a subset of the web index and lacks features such as Oneboxes.
- 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 optionalpages(1 to 10),hl,locationoruule, anddevice. - URL shape:
url, a complete Google web search URL that may carry onlyq,gl,num,hlanduule. A URL with any other parameter (start,tbs,safe,tbmand so on) is refused with a 400.numis read as result depth, rounded up to whole pages of ten, up to ten pages.urlcannot be combined withqueryorpages.
| 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 |
Without it, the interface language is the country’s own |
num |
pages (standard) or num in url |
Results come in pages of about ten; ask for pages, not results |
start |
pages up to the offset |
Custom Search counts from 1 (start=11 is page 2); request pages through that one and keep results by their page field |
dateRestrict |
after: in the query |
d1, w1, m1, y1 become the date one day, week, month or year back; Google’s relative time filters through url are coming |
safe |
coming soon | SafeSearch filtering through url is coming |
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, or filter results yourself |
cx |
none | Results come from Google web search, not a configured engine |
searchType=image |
none | tbm URLs are rejected |
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 datetime import date, timedelta
from urllib.parse import urlparse
import requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['ANSWERLINE_API_KEY']}"}
DATE_RESTRICT = {"d1": 1, "w1": 7, "m1": 30, "y1": 365}
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']}"
restrict = cse.get("dateRestrict")
if restrict:
if restrict not in DATE_RESTRICT:
raise ValueError(f"no mapping for dateRestrict={restrict}")
q += f" after:{date.today() - timedelta(days=DATE_RESTRICT[restrict])}"
last = int(cse.get("start", 1)) + int(cse.get("num", 10)) - 1
return {"query": q, "gl": cse.get("gl", "us"), "pages": min(10, -(-last // 10))}
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()
first_page = (int(cse.get("start", 1)) - 1) // 10 + 1
return [
{
"title": o["title"],
"link": o["link"],
"displayLink": urlparse(o["link"]).hostname,
"snippet": o.get("snippet", ""),
}
for o in r.json()["result"].get("organicResults", [])
if o["page"] >= first_page
]
print(search({"q": "annual report", "siteSearch": "example.com", "fileType": "pdf", "gl": "us"}))
TypeScript
const API = "https://api.answerline.dev";
const DATE_RESTRICT: Record<string, number> = { d1: 1, w1: 7, m1: 30, y1: 365 };
type CseParams = {
q: string; gl?: string; num?: number; start?: number;
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}`;
if (cse.dateRestrict) {
const days = DATE_RESTRICT[cse.dateRestrict];
if (!days) throw new Error(`no mapping for dateRestrict=${cse.dateRestrict}`);
q += ` after:${new Date(Date.now() - days * 86_400_000).toISOString().slice(0, 10)}`;
}
const last = (cse.start ?? 1) + (cse.num ?? 10) - 1;
return { query: q, gl: cse.gl ?? "us", pages: Math.min(10, Math.ceil(last / 10)) };
}
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();
const firstPage = Math.floor(((cse.start ?? 1) - 1) / 10) + 1;
type Organic = { title: string; link: string; snippet?: string; page: number };
return (result.organicResults ?? []).filter((o: Organic) => o.page >= firstPage).map((o: Organic) => ({
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 in the Search response schema) | 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. At the Starter tier, a Google Search task costs $0.00103, and each extra page $0.00071 more.
pages: 3is $0.00103 + $0.00071 x 2 = $0.00245. A query that carriessiteSearchorfileTypeassite:orfiletype:costs 5x: $0.01225 for the same three pages. See /docs/credits and cost planning. - Move bulk jobs to async. Batches of up to 500 tasks per call, with webhooks, avoid synchronous timeouts; 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:start=11is the first result of page 2, not page 1. - 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; dateRestrict becomes an after: operator; start becomes pages deep enough to reach the offset; safe is coming. 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.