---
title: "Is There a Google Search API? Options After Custom Search"
slug: google-search-api
date: 2026-10-18T11:14:41+00:00
modified: 2026-10-18T11:14:43+00:00
permalink: https://brightdata.com/blog/web-data/google-search-api
type: blog
---

[ Blog ](https://brightdata.com/blog "Blog") / [Web Data](https://brightdata.com/blog/web-data)







 [Web Data](https://brightdata.com/blog/web-data)

# Is There a Google Search API? Options After Custom Search

Learn how to migrate from Google’s Custom Search JSON API to Bright Data’s SERP API, with a drop-in cse.list() adapter and parameter map.

 6 min read





 [ ](https://brightdata.com/blog/authors/satyam-tripathi)

 [Satyam Tripathi

Technical Writer

 ](https://brightdata.com/blog/authors/satyam-tripathi)





 ![Is There a Google Search API](https://media.brightdata.com/2026/10/Is-There-a-Google-Search-API_.png)





Google closed its Custom Search JSON API to new customers, with existing customers’ access ending January 1, 2027. So is there a Google search API you can still sign up for? Not for full-web results, because Google’s public options search chosen sites or return Gemini answers based on Google Search. Bright Data’s SERP API returns full-web Google results as JSON.

Custom Search JSON API returns a 403 to new projects. Direct HTTP requests to google.com failed silently in testing, with status 200 and 0 results.

This guide compares Google’s options, explains why direct requests fail, and shows how to migrate.

## TL;DR

- **Google sells no self-serve full-web search API to new developers.** Custom Search JSON API’s end date is January 1, 2027.
- **Grounding with Google Search returns Gemini answers, not result lists.** Its terms forbid caching or analyzing the results, or indexing their links.
- **Direct HTTP requests to Google Search got no results in testing.** Browsers on the same connection then got a CAPTCHA.
- **Bright Data’s SERP API returns Google’s results page as JSON.** 5K requests a month are free, then $1.50 per 1,000 successful requests. A drop-in adapter converts `cse.list()` calls.

## Which Custom Search API replacement fits your code

Find what your code needs:

If your code needsUseGoogle search results for the full web[Bright Data SERP API](#migrate)Search limited to a fixed list of sites[Bright Data SERP API](#step-3) with `site:` filters, or [Agent Search, from Google](#google-options)To keep existing `cse.list()` parameters[The adapter in step 3](#step-3)Google Images results[The adapter in step 3](#step-3) with `searchType="image"`An MCP (Model Context Protocol), LangChain, or n8n search tool[Bright Data Web MCP, a LangChain tool, or the n8n node](#agent-tools)A Gemini answer shown to the user who asked[Grounding with Google Search](#google-options)A fix for an error message[The troubleshooting table](#troubleshooting)## What happened to the Google Custom Search JSON API

Google announced the shutdown on January 20, 2026. Its [Custom Search JSON API overview](https://developers.google.com/custom-search/v1/overview) marks the API “closed to new customers” and gives existing customers until January 1, 2027.

New projects already get a 403 from Custom Search JSON API, even before the 2027 deadline. This is the response, shortened to its main fields.

```none
{
  "error": {
    "code": 403,
    "message": "This project does not have the access to Custom Search JSON API.",
    "status": "PERMISSION_DENIED"
  }
}
```

On Google’s developer forum, one developer reports this 403 on an existing project after a billing change.

The table puts Google’s documented limits next to the [Bright Data SERP API](/products/serp-api):

Custom Search JSON APIBright Data SERP APIFree usage100 queries a day, about 3K a month5K requests a month, no credit cardPaid usage$5 per 1,000 queries, up to 10K a day[$1.50 per 1,000 successful requests](/pricing/serp), unlimited concurrencyResults per requestUp to 10A full Google results pageResults per queryUp to 100Paginate with `start`, or get positions 1 to 100 in one callLocation`gl` boosts results from a countryResults as seen by a user in the country you setWhat the API returnsResults from a Programmable Search Engine, which searches a subset of Google’s indexGoogle’s results page, including ads and People Also Ask when Google shows them. One extra parameter adds the AI OverviewThe migration is easier than it looks, because Custom Search results already differed from google.com results. Google’s [help page](https://support.google.com/programmable-search/answer/70392?hl=en) says a full-web engine searches only “a subset of the total Google Web Search corpus” and excludes real-time results. So you don’t need to reproduce old results exactly. Check the new results against google.com, and use saved Custom Search results to find bugs in your migration.

## Google’s own Custom Search API alternatives

Google directs Custom Search users to Agent Search and a full-web interest form. Grounding with Google Search, a Gemini API feature, also appears when you search for a replacement:

Product and scopeWhat it returnsList priceAgent Search (also called Vertex AI Search), for up to 50 domainsResults and answers from those sites[$4 per 1,000 website search queries](https://cloud.google.com/generative-ai-app-builder/pricing)Grounding with Google Search (Gemini API), for Google SearchA Gemini answer with links[$14 per 1,000 search requests](https://ai.google.dev/gemini-api/docs/pricing)Full web search solution, for Google’s full indexNot documented publiclyInterest form onlyIf your engine searched a fixed list of sites, you have 2 options. The Bright Data SERP API accepts the same list as `site:` filters, and Agent Search supports up to 50 domains.

Grounding is the closest product to web search, but its terms limit what you can build. The [Gemini API terms](https://ai.google.dev/gemini-api/terms) require you to show grounded results to the user who asked. They forbid you to “cache, frame, syndicate, resell, analyze, train on, or otherwise learn from” those results or index their links.

Microsoft also retired its Bing Search APIs in August 2025. Our guides to [Bing Search API alternatives](/blog/web-data/best-bing-search-apis) and the [best SERP APIs](/blog/web-data/best-serp-apis) compare the other providers.

None of Google’s self-serve products gives new customers full-web search results. The Bright Data SERP API does.

## Why direct requests to Google Search fail

With Custom Search closing, the obvious fallback is to request the Google results page directly. Google Search has required JavaScript since January 2025. In testing from 1 home broadband connection, 10 requests from HTTP clients and then several real browsers got no search results:

The HTTP clients fail in the way that’s hardest to detect. The 10 requests came from 5 common HTTP client configurations, including `curl` and Python `httpx`. All 10 got status 200 and no results, so a scraper that checks only status codes logs a success and stores nothing. The page contained only a script that has to run in a JavaScript engine before any results appear.

Check every response for results, not just the status code.

Every browser tested, including headless Chromium and Chrome, ran the JavaScript but got Google’s `/sorry/` CAPTCHA page on its first query. They ran after those 10 HTTP requests, and the block applied to the whole connection for 40 minutes or more.

At production volume, you would have to run Google’s JavaScript for every search and maintain parsers for changing markup. Bright Data’s SERP API handles the browsers and parsers for you and returns parsed results.

## How to migrate Custom Search JSON API code to Bright Data’s SERP API

You send a normal Google search URL, and the response is that page as JSON. Steps 1 to 3 are the core migration.

### Step 1: Set up and send a first request

You need a free [Bright Data account](/cp/start), an API key, and a SERP API zone. A zone is a named set of settings that your requests use. If your security team reviews new vendors first, send the team the [Bright Data Trust Center](/trustcenter) link. That page lists SOC 2 and SOC 3 reports, ISO 27701 certificates, and GDPR documentation.

In the control panel, a zone is called an API. Open **Web Access**, select **Create API**, choose **SERP API**, and select **Continue**:

Give the API a name, make sure the data format is **Raw HTML**, and select **Add API**. The JSON comes from a parameter in each request, not from this setting. The API name you chose is your zone name, which you set in `BRIGHTDATA_SERP_ZONE` below:

The API key is in the **Quick start** bar at the bottom of the **Home** page:

Create a virtual environment, set the key and the zone name, and install the only Python dependency. Replace both values with your own key and zone name:

```none
python3 -m venv .venv && source .venv/bin/activate
export BRIGHTDATA_API_KEY="your-api-key"
export BRIGHTDATA_SERP_ZONE="serp_api1"
python -m pip install "httpx>=0.27,<1"
```

On Windows PowerShell, use this block instead:

```none
python -m venv .venv; .venv\Scripts\Activate.ps1
$env:BRIGHTDATA_API_KEY = "your-api-key"
$env:BRIGHTDATA_SERP_ZONE = "serp_api1"
python -m pip install "httpx>=0.27,<1"
```

With uv, skip the virtual environment and start each script with `uv run --with "httpx>=0.27,<1" python`, followed by the script name.

A single POST to Bright Data’s `/request` endpoint fetches a Google URL through your zone and returns one results page. This curl command sends that request and pretty-prints the response:

```none
curl https://api.brightdata.com/request \
  -H "Authorization: Bearer $BRIGHTDATA_API_KEY" \
  -H "Content-Type: application/json" \
  --data @- <<EOF | python -m json.tool
{
  "zone": "$BRIGHTDATA_SERP_ZONE",
  "url": "https://www.google.com/search?q=is+there+a+google+search+api&gl=us&hl=en&brd_json=1",
  "format": "raw"
}
EOF
```

The `brd_json=1` parameter converts the page to parsed JSON, and `"format": "raw"` returns that JSON unchanged. The response below is shortened:

```none
{
  "general": {
    "search_engine": "google",
    "query": "is there a google search api",
    "language": "en",
    "country_code": "US"
  },
  "organic": [
    {
      "link": "https://developers.google.com/custom-search/v1/overview",
      "title": "Custom Search JSON API | Google for Developers",
      "description": "The Custom Search JSON API lets you develop websites and applications to retrieve and display search results from Programmable Search Engine ...",
      "rank": 1
    }
  ],
  "pagination": {
    "current_page": 1,
    "next_page": 2,
    "next_page_start": 10
  }
}
```

The same response also contained 8 related searches and 4 People Also Ask questions. The Custom Search response has no fields for either.

### Step 2: Map Custom Search parameters to Google parameters

Bright Data’s [Google query parameter reference](https://docs.brightdata.com/products/serp-api/query-parameters/google) and its list of [Google search URL parameters and operators](/blog/web-data/google-search-url-parameters) explain Google’s parameters in detail. Response fields are in `items[]` in Custom Search and in `organic[]` in the SERP API response:

Custom Search JSON APIGoogle, through the SERP API`q``q``num` (maximum 10)Removed by Google. The adapter requests more pages`start` (counts from 1)`start` (counts from 0, in steps of 10)`gl` (boosts results from a country)`gl` (runs the search as a user in that country)`hl` (interface language)`hl``siteSearch=a.com``site:a.com` added to `q``siteSearchFilter=e``-site:a.com` added to `q``exactTerms=a b``"a b"` added to `q``excludeTerms=a``-"a"` added to `q``fileType=pdf``filetype:pdf` added to `q``dateRestrict=d7``tbs=qdr:d7`, and the same for `w`, `m`, and `y``safe=active``safe=active``searchType=image``udm=2` (Google Images)`title`, `link``title`, `link``snippet``description``displayLink`Host name of `link``queries.nextPage`Computed by the adapterOn Google, 3 parameters work differently:

**Google counts offsets from 0.** Custom Search treats `start=11` as the top of page 2, and Google treats `start=10` as page 2. Send `start` in steps of 10 and discard the results before your start position in code, as the adapter does.

**Google removed `num`.** The SERP API docs say Google deprecated `num` on September 11, 2025. Each Google page contains about 10 results, and the adapter requests as many pages as `num` needs. For more results per query, Bright Data can return the [top 100 Google results](https://docs.brightdata.com/products/serp-api/get-top-100-google-results), positions 1 to 100, in one call.

**On Google, `gl` sets the search country.** The query “cloud hosting pricing” returned UK, German, and Indian top results that shared no URLs with the US top results. Set `gl` on every request to the country your users search from.

### Step 3: Add a drop-in adapter for cse.list()

The adapter sends `cse.list()` calls to the Bright Data SERP API and runs on Python 3.9 and later. It accepts the Custom Search parameters in the table and keeps the `title`, `link`, `snippet`, and `displayLink` fields in `items`. It ignores `cx` with a warning, because Google search has no engine ID, and it adds `attempts` and `timeout` settings.

At each call site, the change looks like this:

```none
# Before, with the Google API client
res = service.cse().list(q=query, cx=CX, num=10).execute()

# After, with the same core items fields, no engine ID, and gl and hl set
from cse_to_serp import cse_list

res = cse_list(q=query, num=10, gl="us", hl="en")
```

Check 3 more things at each call site:

- If your engine searched a fixed list of sites, put them in the query as `(site:a.com OR site:b.com)`. A query restricted to 2 documentation sites this way returned 10 results, all from those 2 hosts.
- In a web request handler, pass `attempts=1` and a `timeout` that fits the handler’s time budget.
- Where you caught `HttpError`, catch `(httpx.HTTPError, ValueError, RuntimeError)` instead.

Save the adapter as `cse_to_serp.py`, or clone the [companion repo on GitHub](https://github.com/triposat/google-custom-search-api-alternative), which also contains the other scripts below and offline tests. Here is the full file:

```none
"""Replace Google Custom Search JSON API calls with the Bright Data SERP API.

Same cse.list() parameters and core items fields.
"""

import math
import os
import time
import warnings
from urllib.parse import urlencode, urlparse

import httpx

API_URL = "https://api.brightdata.com/request"
API_KEY = os.environ.get("BRIGHTDATA_API_KEY")
if not API_KEY:
    raise RuntimeError("Set the BRIGHTDATA_API_KEY environment variable "
                       "to your Bright Data API key (step 1).")
ZONE = os.environ.get("BRIGHTDATA_SERP_ZONE", "serp_api1")
RETRY_STATUSES = {429, 500, 502, 503, 504}

def fetch_serp(params: dict, attempts: int = 4, timeout: float = 60) -> dict:
    """Fetch one Google results page as parsed JSON."""
    url = "https://www.google.com/search?" + urlencode(params)
    body = {"zone": ZONE, "url": url, "format": "raw"}
    headers = {"Authorization": f"Bearer {API_KEY}"}
    for attempt in range(attempts):
        try:
            resp = httpx.post(API_URL, json=body, headers=headers,
                              timeout=timeout)
        except httpx.TransportError:  # timeout or dropped connection
            if attempt == attempts - 1:
                raise
            time.sleep(16 + attempt * 10)
            continue
        error = (resp.headers.get("x-brd-error")  # API-level errors
                 or resp.headers.get("x-brd-err-msg"))  # proxy-level errors
        if resp.status_code == 200 and not error:
            page = resp.json()
            if "general" not in page:  # not a parsed results page
                raise ValueError("Unexpected response: " + resp.text[:200])
            return page
        if not error and resp.status_code not in RETRY_STATUSES:
            break  # wrong key, zone, or URL, so retrying will not help
        if attempt < attempts - 1:
            # Back off before resending the same query.
            time.sleep(16 + attempt * 10)
    resp.raise_for_status()
    code = (resp.headers.get("x-brd-error-code")  # maps to the error catalog
            or resp.headers.get("x-brd-err-code"))
    raise RuntimeError(f"SERP API error {code}: {error or resp.status_code}")

def cse_list(q, num=10, start=1, gl=None, hl=None, siteSearch=None,
             siteSearchFilter="i", exactTerms=None, excludeTerms=None,
             fileType=None, dateRestrict=None, safe="off", searchType=None,
             attempts=4, timeout=60, **extra):
    """Accept cse.list() parameters, return a dict in cse.list() format."""
    if extra.pop("cx", None):  # no engine ID, so every call searches Google
        warnings.warn("cx is ignored. Add (site:a.com OR site:b.com) to q "
                      "if your engine searched a fixed list of sites")
    if extra:  # fail loudly instead of ignoring a filter
        raise TypeError(f"No mapping for: {sorted(extra)}")
    if exactTerms:  # Custom Search filters become Google search operators
        q = f'{q} "{exactTerms}"'
    if excludeTerms:
        q = f'{q} -"{excludeTerms}"'
    if fileType:
        q = f"{q} filetype:{fileType}"
    if siteSearch:
        exclude = "-" if siteSearchFilter == "e" else ""
        q = f"{q} {exclude}site:{siteSearch}"
    params = {"q": q, "brd_json": 1}  # brd_json=1 returns parsed JSON
    if gl:
        params["gl"] = gl
    if hl:
        params["hl"] = hl
    if safe == "active":
        params["safe"] = "active"
    if dateRestrict:  # "d7" -> qdr:d7 (past 7 days), "m6" -> qdr:m6
        params["tbs"] = "qdr:" + dateRestrict
    if searchType == "image":
        return image_list(params, num, start, attempts, timeout)
    if searchType:
        raise ValueError(f"No mapping for: searchType={searchType}")

    # Custom Search counts from 1 (start=11 is page 2), Google from 0
    # (start=10 is page 2). Track each result's Google position.
    first = start - 1
    offset = first // 10 * 10  # the Google page that contains `start`
    items, next_start = [], None
    # Fetch the pages for positions first to first + num - 1, 1 request each
    for _ in range(math.ceil((num + first - offset) / 10)):
        params["start"] = offset
        page = fetch_serp(params, attempts, timeout)
        for i, result in enumerate(page.get("organic", [])):
            position = offset + i
            if position < first or not result.get("link"):
                continue
            if len(items) == num:
                next_start = position + 1  # convert to 1-based
                break
            items.append({
                "title": result.get("title", ""),
                "link": result["link"],
                "snippet": result.get("description", ""),
                "displayLink": urlparse(result["link"]).netloc,
            })
        if next_start:
            break
        offset = (page.get("pagination") or {}).get("next_page_start")
        if not offset:  # Google has no further pages for this query
            break
    else:
        next_start = offset + 1  # continue at the next Google page

    response = {"items": items, "queries": {}}
    if next_start:
        response["queries"]["nextPage"] = [{"startIndex": next_start}]
    return response

def image_list(params, num, start, attempts, timeout):
    """Google Images returns the whole image grid in one request."""
    params["udm"] = 2
    page = fetch_serp(params, attempts, timeout)
    images = [i for i in page.get("images") or [] if i.get("original_image")]
    items = [{
        "title": image.get("image_alt", ""),
        "link": image["original_image"],
        "snippet": image.get("image_alt", ""),
        "displayLink": urlparse(image["original_image"]).netloc,
    } for image in images[start - 1:start - 1 + num]]
    response = {"items": items, "queries": {}}
    if start - 1 + num < len(images):
        response["queries"]["nextPage"] = [{"startIndex": start + num}]
    return response

if __name__ == "__main__":
    res = cse_list("httpx timeout configuration", num=10,
                   gl="us", hl="en")
    for i, item in enumerate(res["items"], start=1):
        print(i, item["displayLink"], "|", item["title"][:60])
```

Running `python cse_to_serp.py` printed these as its first 5 results. Results come from Google’s live page, so yours can differ.

```none
1 www.python-httpx.org | Timeouts
2 github.com | Proposal: Add a Global Timeout Setting in httpx #3356
3 www.python-httpx.org | QuickStart
4 stackoverflow.com | python - Httpx requests timing out when it shouldn't
5 oneuptime.com | How to Use httpx for Async HTTP Requests
```

With `num=10`, each `nextPage` call fetches a single Google page, so it costs 1 request.

With `searchType="image"`, 1 request for “ergonomic office chair” returned 100 images, while Custom Search returned at most 10 per request. Each adapter call fetches the full image grid and returns `num` images from it.

### Replace the search tool in MCP agents, LangChain, and n8n

Agents that connect to tools over MCP need no adapter. The [Bright Data Web MCP](/ai/mcp-server/google) server exposes Google search as its `search_engine` tool, and the hosted server is one URL with a [free tier](/blog/ai/web-mcp-free-tier). In Claude Code, you add it with one command:

```none
claude mcp add --transport http brightdata https://mcp.brightdata.com/mcp
```

MCP clients that support OAuth, such as Claude Code and Cursor, sign in through your browser, with no API key in a config file. For CI jobs and scripts, use `https://mcp.brightdata.com/mcp?token=YOUR_API_KEY` instead. Add `&tools=search_engine,scrape_as_markdown` to that URL to give the agent search plus a tool that reads each result page as Markdown.

Coding agents can also use Bright Data’s CLI. Install it with `npm install -g @brightdata/cli`, then run `brightdata login` to sign in through your browser. The agent gets a `brightdata search` command.

For code that already calls LangChain, the `GoogleSearchAPIWrapper.results()` method returns dicts with `title`, `link`, and `snippet`. The adapter’s items have the same keys. Install LangChain’s core package, which needs Python 3.10 or later, with `python -m pip install "langchain-core>=1.0,<2"`. Save this file as `langchain_tool.py` next to the [`cse_to_serp.py` adapter](https://github.com/triposat/google-custom-search-api-alternative/blob/main/cse_to_serp.py), and pass `google_search` in your agent’s tool list.

```none
"""LangChain tool that replaces GoogleSearchAPIWrapper.results()."""

from langchain_core.tools import ToolException, tool

from cse_to_serp import cse_list

@tool
def google_search(query: str) -> list[dict]:
    """Search Google and return the top results with title, link, snippet."""
    try:  # 2 attempts and a 20 s timeout limit each search
        return cse_list(q=query, num=10, gl="us", hl="en",
                        attempts=2, timeout=20)["items"]
    except Exception as exc:
        raise ToolException(f"Google search failed: {exc}") from exc

google_search.handle_tool_error = True  # the agent gets the error as text
```

For a full agent built this way, see [Using LangChain and Bright Data for Web Search](/blog/ai/langchain-serp-scraping).

No-code workflows that call Custom Search get the same 403, and the n8n community forum has a thread about that exact error. Bright Data’s official [n8n node](https://github.com/brightdata/n8n-nodes-brightdata) installs as the `@brightdata/n8n-nodes-brightdata` community node. It has a Web Search operation that sends the query to Google through the zone you select, such as your SERP API zone. Replace the workflow’s Custom Search request with that operation.

### Run searches at production volume

Batch jobs, and agents that send many searches in one task, need to run several queries at the same time. The companion repo’s [`serp_batch.py` module](https://github.com/triposat/google-custom-search-api-alternative/blob/main/serp_batch.py) runs an async version of the adapter’s request and retry loop in parallel. Separately, the SERP API itself offers an asynchronous mode for batches of 1K queries or more.

The file’s main block sends the same 10 queries twice, and the second run is served from its in-memory cache.

```none
{'queries': 20, 'cache_hits': 10, 'empty': 0, 'failed': 0}
```

Its `search_many()` function has 3 settings and a set of counters.

- **`concurrency`** sets how many requests run at once. The SERP API has no concurrency limit, so set it to the load your own system can handle.
- **`deadline`** limits the total time for one query, so keep it short for an agent tool and longer for a batch job.
- **`ttl`** sets how long a cached page stays valid.
- **Counters** show cache hits, empty results, and failures, so a wrong `site:` filter or `gl` value appears early as empty results.

A call returns a dict that maps each query to its parsed page, or to `None` if the query failed.

```none
import asyncio

from serp_batch import search_many

pages = asyncio.run(search_many(["redis cache ttl", "nginx rate limiting"],
                                concurrency=20, gl="us", hl="en"))
```

### Get the AI Overview and People Also Ask questions

Agents, RAG pipelines, and SEO teams can need more than a list of 10 links. Google’s page for “what is retrieval augmented generation” shows an AI Overview and People Also Ask questions, which Custom Search never returned.

One extra parameter requests the AI Overview, and the same response contains any People Also Ask questions. Save this script next to the `cse_to_serp.py` adapter from step 3.

```none
"""Read the AI Overview and People Also Ask questions from one Google query."""

from cse_to_serp import fetch_serp

page = fetch_serp({
    "q": "what is retrieval augmented generation",
    "gl": "us",
    "hl": "en",
    "brd_json": 1,
    "brd_ai_overview": 2,  # requests the AI Overview
})

overview = page.get("ai_overview") or {}
print("AI Overview text blocks:", len(overview.get("texts", [])))
print("AI Overview sources:", len(overview.get("references", [])))
questions = dict.fromkeys(  # keeps each question once, in order
    item["question"] for item in page.get("people_also_ask", [])
    if item.get("question")
)
for question in list(questions)[:2]:
    print("People also ask:", question)
```

The script printed this output. Google’s page is live, so your questions can differ from the screenshot.

```none
AI Overview text blocks: 3
AI Overview sources: 8
People also ask: Does ChatGPT use Retrieval-Augmented Generation?
People also ask: What is RAG in LLM with example?
```

Each `references` entry contains the cited page’s `href` and `title`, so SEO teams can track which sites the AI Overview cites. Bright Data’s [Google AI Overview API](/products/serp-api/google-search/ai-overview) page explains the parameter in detail. For Google’s AI Mode, the [Google Scraper API](https://docs.brightdata.com/products/scrapers/google/introduction) returns AI Mode Search results as JSON.

A RAG pipeline that reads the result pages can fetch them through the same `/request` endpoint. Use a [Web Unlocker](/products/web-unlocker) zone and the same API key.

## Migration checklist before January 1, 2027

Run these steps while your Custom Search key still works, because item 2 needs it.

1. Find every call site. Search your code for `customsearch/v1`, `cse().list`, and wrappers such as `GoogleSearchAPIWrapper`. In a large codebase, a coding agent can run that search and make the step 3 change at each call site.
2. Save the Custom Search results for a sample of real production queries, so you keep a baseline after the key stops working. If the key has already stopped working, save google.com results for the same `gl` and `hl` instead.
3. Start on the free tier, then budget 1 SERP API request per results page.
4. Let the adapter convert 1-based `start` values to Google’s 0-based values, or convert them yourself and request whole pages.
5. Set `gl` and `hl` on every request.
6. Compare old and new results as sets of URLs, not by exact rank. For agents, also rerun your task evals and score the final answers. Expect some differences, because the new results come from Google’s full results page, not Custom Search’s subset of the index.
7. Switch behind a feature flag, run both paths in parallel for a few days, then delete the Google key.

For item 6, the [`compare_baseline.py` helper](https://github.com/triposat/google-custom-search-api-alternative/blob/main/compare_baseline.py) scores each query from 0 to 1. The score is the share of the saved top 10 URLs that still appear in the new top 10.

First, run each query twice through the SERP API and score the 2 runs against each other. Google’s ranking can change between calls, so a score just under 1.0 can be normal. In testing, 2 runs of a query with the same `gl` scored 1.0, and a US run against a UK run scored 0.0. Then score every saved query, review the lowest scores by hand, and check `gl` and `site:` filters for any score near 0.

## Troubleshoot migration errors

These are first-run setup issues or Google behavior.

SymptomCauseFixGoogle returns 403 on Custom Search callsAPI closed to new customersA new key won’t help. Migrate to the Bright Data SERP API`RuntimeError: Set the BRIGHTDATA_API_KEY environment variable…`Variable not set in this terminalSet `BRIGHTDATA_API_KEY` as in step 1401 “Invalid token”Wrong Bright Data API keyCopy the key from the Quick start bar on Home400 `zone "..." not found`Wrong zone nameUse the name you gave the API in step 1`UserWarning: cx is ignored…`Google search has no engine IDRemove `cx`. If the engine searched a fixed list of sites, add `(site:a.com OR site:b.com)` to `q``TypeError: No mapping for: [...]`A parameter not in the step 2 tableAdd the Google equivalent to `q` or to the adapter’s `params`, or remove the parameter`KeyError` on `pagemap` or `searchInformation`The adapter returns 4 core fieldsRemove the code that reads the field, or map it from the SERP API response`items` has fewer than `num` resultsGoogle’s page contained fewer organic resultsFollow `queries.nextPage``items` is emptyNo match, or a wrong `site:` filter or `gl` valueTry the query on google.comResults differ from Custom SearchCustom Search searched a subset of Google’s indexCompare URL sets. Run each query twice first to see normal variationA `RuntimeError: SERP API error…` message includes the `x-brd-error-code` or `x-brd-err-code` value. Find the code in the [SERP API error catalog](https://docs.brightdata.com/products/serp-api/debugging).

## Final thoughts

Google’s full-web search product isn’t public, and Custom Search JSON API’s end date is January 1, 2027. Google’s public options search a fixed list of domains or return Gemini answers, and the Gemini terms forbid caching or analyzing the results. Direct HTTP requests got no results in testing, so the Bright Data SERP API is the practical replacement when your code needs the results page.

The adapter in step 3 handles Google’s 0-based `start` and removed `num`, so your code mainly needs `gl` on every call. Run your saved queries through the [Bright Data SERP API](/products/serp-api/google-search) free tier, with no credit card, and compare them with your Custom Search baseline or with google.com.

## FAQ

### Is the Google Custom Search API discontinued?

Yes, in 2 stages. Google closed Custom Search JSON API to new customers in January 2026, with existing customers’ access ending January 1, 2027. Google suggests Agent Search for up to 50 domains. The Bright Data SERP API returns full-web Google results as JSON.

### Does Google have a search API?

Not a full-web search API that new developers can sign up for. Google offers Agent Search for up to 50 domains and Grounding with Google Search, which returns Gemini answers with links. Google’s full-web search product isn’t public. For Google results as JSON, use Bright Data’s Google Search API, part of its SERP API.

### Is the Google search API free?

Not for new developers who need full-web results. Custom Search JSON API gives existing customers 100 free queries a day, then charges $5 per 1,000, until January 1, 2027. Bright Data’s SERP API returns Google results as JSON, with 5K free requests a month.

### What can replace the Google Custom Search API?

The Bright Data SERP API replaces Custom Search JSON API directly, because it returns Google’s full results page as JSON. Google’s own options return AI answers or search a fixed list of domains. An open-source, drop-in Python adapter converts your `cse.list()` calls to Google parameters.

### How can I scrape Google search results?

Not with an HTTP client alone. Google Search requires JavaScript, so in testing, HTTP clients got a 200 page with no results. Browsers on the same connection then got a CAPTCHA page. The Bright Data SERP API returns Google’s results page as parsed JSON, with no browsers or parsers for you to maintain.

### Why does the Custom Search API return 403?

Google closed Custom Search JSON API to new customers in January 2026, so a new key or project won’t fix the 403 error. On Google’s developer forum, one developer also reports an existing project losing access after a billing change. Move the calls to the Bright Data SERP API, which returns Google results as JSON.



Contact usStart for free

No credit card required











 [ ](https://www.linkedin.com/in/triposat/)

Satyam Tripathi

 Technical Writer



  5 years experience



Satyam Tripathi helps SaaS and data startups turn complex tech into actionable content, boosting developer adoption and user understanding.



Expertise

  Python   Developer Education   Technical Writing



 [ View all articles ](https://brightdata.com/blog/authors/satyam-tripathi)











 Table of Contents







Dedicated Scraper APIs &amp; No-Code Scrapers

Over 1000 scrapers for all popular domains. Simplify your web scraping.

[See pricing](/pricing/web-scraper "See pricing")

Just want data? Skip scraping.

Hundreds of ready-to-use datasets from all popular domains.

[See pricing](/pricing/datasets "See pricing")







 [ ](https://news.ycombinator.com/submitlink?t=Is+There+a+Google+Search+API%3F+Options+After+Custom+Search&u=https://brightdata.com/blog/web-data/google-search-api) [ ](https://www.linkedin.com/shareArticle?mini=true&title=Is+There+a+Google+Search+API%3F+Options+After+Custom+Search&url=https://brightdata.com/blog/web-data/google-search-api) [ ](http://www.reddit.com/submit?title=Is+There+a+Google+Search+API%3F+Options+After+Custom+Search&url=https://brightdata.com/blog/web-data/google-search-api)







##  You might also be interested in

 [ ](https://brightdata.com/blog/ai/claude-code-routines "Claude Code Routines: Schedule Daily Web Monitoring with Bright Data")

 [AI





Satyam Tripathi

Technical Writer





### Claude Code Routines: Schedule Daily Web Monitoring with Bright Data

Learn how to give Claude Code routines web access with Bright Data and build a daily price monitor that checks every page it reads.



 18-Oct-2026

 12 min read

 ](https://brightdata.com/blog/ai/claude-code-routines)

 [ ](https://brightdata.com/blog/ai/snowflake-with-bright-data "Bright Data + Snowflake: From Web Data to AI-Ready Intelligence")

 [AI





Antonello Zanini

Technical Writer





### Bright Data + Snowflake: From Web Data to AI-Ready Intelligence

Learn how to send Bright Data web datasets to Snowflake, activate them with Cortex AI, and choose between stored datasets and live MCP access.



 15-Oct-2026

 14 min read

 ](https://brightdata.com/blog/ai/snowflake-with-bright-data)

 [ ](https://brightdata.com/blog/how-tos/openosint-with-bright-data "How to Use OpenOSINT With Bright Data for Automated Web Intelligence")

 [How Tos





Federico Trotta

Technical Writer





### How to Use OpenOSINT With Bright Data for Automated Web Intelligence

Learn how to set up OpenOSINT, connect it to Bright Data, and turn OSINT dork queries into live search results and scraped public pages.



 14-Oct-2026

 12 min read

 ](https://brightdata.com/blog/how-tos/openosint-with-bright-data)
