AnswerLine Start free

, SERP API · Google · Tutorials

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:

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.

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

  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. Google’s help page says Custom Search searches a subset of the web index and lacks features such as Oneboxes.
  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 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

  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. At the Starter tier, a Google Search task costs $0.00103, and each extra page $0.00071 more. pages: 3 is $0.00103 + $0.00071 x 2 = $0.00245. A query that carries siteSearch or fileType as site: or filetype: costs 5x: $0.01225 for the same three pages. 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; 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; 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.

Try it on your own prompts

100 free requests on any engine, no card. One POST returns the answer, sources and citations as JSON.

Keep reading