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.
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
- Replace the host
www.googleapis.com(orcustomsearch.googleapis.com) withapi.serpkite.com. The path stays/customsearch/v1. - Replace the Google API key with a SerpKite key. You can keep passing it as
key=, or move it to anAuthorization: Bearerheader so it stays out of URLs and logs. - Leave
cxas 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.
{
"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 inerror.code; SerpKite puts a string such asunauthorizedorinsufficient_creditsthere. 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"}'Related
Last updated: 2026-09-29