Skip to content

New Official SDKs for TypeScript, Python and Go

SerpKite
Get API key
Docs menu / Migrate from Google Custom Search

Migrate from Google Custom Search

Google shuts down the Custom Search JSON API on January 1, 2027. SerpKite's GET /customsearch/v1 takes the same parameters and returns the same response shape, so you change the host and the key and keep your parser.

View as Markdown

Google is retiring the Custom Search JSON API (https://www.googleapis.com/customsearch/v1) on January 1, 2027. SerpKite ships a compatible endpoint, GET /customsearch/v1, that accepts the same query parameters and returns the same customsearch#search document. Code that reads items[].title, items[].link, searchInformation.totalResults or queries.nextPage keeps working.

The change

- https://www.googleapis.com/customsearch/v1?key=GOOGLE_KEY&cx=ENGINE_ID&q=asyncio
+ https://api.serpkite.com/customsearch/v1?key=skt_live_...&cx=ENGINE_ID&q=asyncio
  1. Replace the host www.googleapis.com (or customsearch.googleapis.com) with api.serpkite.com. The path stays /customsearch/v1.
  2. Replace the Google API key with a SerpKite key. You can keep passing it as key=, or move it to an Authorization: Bearer header so it stays out of URLs and logs.
  3. Leave cx as it is. It is accepted and ignored.

Paste an existing request URL into the CSE migration helper to get the rewritten URL and code.

Parameters

Parameter Supported Notes
q Yes Required.
key Yes Your SerpKite key. Authorization: Bearer also works.
cx Accepted, ignored Results come from the whole web, not a Programmable Search Engine.
start Yes 1–91, in steps of 10 (1, 11, 21…).
num Yes 1–10.
gl Yes Country code.
hl Yes Interface language.
lr Yes Language restrict, e.g. lang_de.
safe Yes active or off.
dateRestrict Yes d7, w2, m6, y1…
siteSearch Yes Restrict to one site.
searchType Yes image for image results.

If your engine was restricted to certain sites

A Programmable Search Engine could be limited to a list of sites. SerpKite ignores cx, so add the restriction to the request: siteSearch=docs.example.com, or a site: operator in q (site:docs.example.com OR site:blog.example.com asyncio).

Response

The response keeps the CSE shape: kind, searchInformation, items[] (with title, htmlTitle, link, displayLink, snippet, htmlSnippet, formattedUrl, pagemap, image for image search) and queries with request and nextPage.

GET /customsearch/v1 · illustrative
{
  "kind": "customsearch#search",
  "searchInformation": {
    "searchTime": 0.94,
    "formattedSearchTime": "0.94",
    "totalResults": "1240000",
    "formattedTotalResults": "1,240,000"
  },
  "items": [
    {
      "kind": "customsearch#result",
      "title": "asyncio — Asynchronous I/O",
      "htmlTitle": "<b>asyncio</b> — Asynchronous I/O",
      "link": "https://docs.python.org/3/library/asyncio.html",
      "displayLink": "docs.python.org",
      "snippet": "asyncio is a library to write concurrent code using the async/await syntax."
    }
  ],
  "queries": {
    "request": [
      {
        "searchTerms": "site:docs.python.org asyncio",
        "count": 10,
        "startIndex": 1
      }
    ]
  }
}

Two differences to handle:

  • Errors use SerpKite’s shape, {"error":{"code","message","request_id"}}, not Google’s {"error":{"code":403,"errors":[…]}}. Google put the HTTP status in error.code; SerpKite puts a string such as unauthorized or insufficient_credits there. See Errors.
  • Quota. There is no 100-queries-per-day free cap and no 10,000-per-day ceiling. You pay 1 credit per call from a prepaid balance, and your plan sets a requests-per-second rate limit.

Code

Python with requests

import os
import requests

res = requests.get(
    "https://api.serpkite.com/customsearch/v1",
    params={"q": "site:docs.python.org asyncio", "cx": "any", "num": 10, "start": 1},
    headers={"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"},
    timeout=30,
)
res.raise_for_status()
data = res.json()
print(data["searchInformation"]["totalResults"])
for item in data.get("items", []):
    print(item["title"], item["link"])

Python with google-api-python-client

If you use Google’s client library, point it at SerpKite with client_options. The library builds URLs from the discovery document’s root, so overriding the endpoint sends requests to https://api.serpkite.com/customsearch/v1:

import os
from googleapiclient.discovery import build

service = build(
    "customsearch",
    "v1",
    developerKey=os.environ["SERPKITE_API_KEY"],
    client_options={"api_endpoint": "https://api.serpkite.com/"},
)
data = service.cse().list(q="site:docs.python.org asyncio", cx="any").execute()
for item in data.get("items", []):
    print(item["title"], item["link"])

The discovery document itself is still fetched from Google, so this path depends on Google’s discovery service staying up after the shutdown. For a long-lived migration, prefer the plain requests version above.

Node.js

const url = new URL("https://api.serpkite.com/customsearch/v1");
url.search = new URLSearchParams({ q: "site:docs.python.org asyncio", cx: "any", num: "10" });

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.SERPKITE_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error.message);
const { items = [], searchInformation } = await res.json();
console.log(searchInformation.totalResults, items.map((i) => i.link));

Go

q := url.Values{}
q.Set("q", "site:docs.python.org asyncio")
q.Set("cx", "any")
q.Set("num", "10")

req, _ := http.NewRequest("GET", "https://api.serpkite.com/customsearch/v1?"+q.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SERPKITE_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

var out struct {
	Items []struct {
		Title string `json:"title"`
		Link  string `json:"link"`
	} `json:"items"`
}
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
	log.Fatal(err)
}
for _, it := range out.Items {
	fmt.Println(it.Title, it.Link)
}

Pagination

As with CSE, page with start: start=1 is results 1–10, start=11 is 11–20, up to start=91. Each call costs 1 credit. If you need the top 100 in one go, the native /v1/search endpoint with num: 100 returns them in one call for 7 credits instead of 10 (see Pagination and depth).

Going further than CSE

Once you’re on SerpKite, the native endpoints give you more than CSE ever returned: People Also Ask, the knowledge graph, the answer box, news, maps and more, plus Markdown output for LLMs. The mapping from CSE is straightforward: items[].link is results[].link, items[].title is results[].title, items[].snippet is results[].snippet, start becomes page, and the CSE gl and hl parameters become country and language. With the official SDKs:

curl https://api.serpkite.com/v1/search \
  -H "Authorization: Bearer $SERPKITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q":"site:docs.python.org asyncio","country":"us","language":"en"}'

Last updated: 2026-09-29