AnswerLineStart free

, SERP API · Google · Tutorials

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:

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.

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

  1. Did you search your own sites? Agent Search, or site search you host yourself.
  2. 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.
  3. Did you measure Google visibility (positions, SERP features, AI Overviews)? A SERP API. Custom Search never reflected the Google results page well for this.
  4. 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:

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

  1. Inventory calls. List every call site with its parameters, especially cx engines restricted to specific sites; those may belong in Agent Search instead.
  2. Decide per use. Site search, LLM grounding, Google visibility and generic web results lead to different products (see the criteria above).
  3. Translate requests with a function like to_request, and log any parameter you cannot map instead of dropping it silently.
  4. Collapse pagination. Replace start loops with pages where you want the top N pages.
  5. 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.
  6. Budget. A Google Search task costs 3 credits, each extra page 2 more, and a synchronous call adds 2. pages: 3 as an async task is 3 + 2 x 2 = 7 credits. See /docs/credits and cost planning.
  7. 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.
  8. Handle errors. Invalid combinations (for example url with pages, or country and gl with different values) return 400 with details[] naming the field; see API errors and retries.

Pitfalls

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.

Try it on your own prompts

500 free credits a month, no card. One POST returns the answer, sources and citations as JSON.

Keep reading