cse-compat_

cse-compat / guides / python

Google Custom Search shutdown: migrating google-api-python-client

tested · google-api-python-client 2.200.0 updated 2026-09-28 ~10 minutes

Short version

Google shuts down the Custom Search JSON API on January 1, 2027. If your Python code calls it through googleapiclient.discovery.build("customsearch", …), you can keep every line that reads the results. Point the client at a compatible endpoint by adding one client_options argument, and use a key for that endpoint.

The change

search.py
service = build(    "customsearch", "v1",-   developerKey=GOOGLE_API_KEY,+   developerKey=CSE_COMPAT_KEY,+   client_options={"api_endpoint": "https://cse.yourdomain.workers.dev"},+   static_discovery=True,)# Everything below is unchanged:res = service.cse().list(q=query, cx=CX, num=10).execute()for item in res.get("items", []):    print(item["title"], item["link"])

Why static_discovery=True: it makes the client use the API description bundled inside the library instead of downloading one from Google. That removes your last runtime dependency on Google's servers for this API. It is already the default in recent versions, but being explicit costs nothing.

Why it works: api_endpoint replaces the client's base URL. The library still builds the same /customsearch/v1?q=…&cx=…&key=… request, just sent to a different host that answers in the same format.

Step by step

1. Get a search key from a provider

cse-compat translates between Google's format and a modern search API that you sign up for yourself. Either works, and one is enough to start:

2. Deploy your endpoint (free Cloudflare tier)

terminal
git clone https://github.com/csecompat/cse-compat.gitcd cse-compat && npm installnpx wrangler deploynpx wrangler secret put SERPER_API_KEY   # or BRAVE_API_KEYnpx wrangler secret put PROXY_KEYS       # the key(s) your app will send

The deploy prints your endpoint URL. Use one of your PROXY_KEYS values as developerKey.

3. Make the change above, then test before January 1

Because both APIs speak the same format, you can run old and new side by side until the shutdown and compare results on your real queries.

What we verified

These checks were run with the official client (version 2.200.0) against a live cse-compat deployment:

BehaviorResult
cse().list(q, cx, num) request URL✓ /customsearch/v1?q=…&cx=…&num=…&key=…&alt=json
res["items"], title, link, snippet✓ same fields, same places
res["queries"]["nextPage"] pagination✓ present, correct startIndex
cse().siterestrict().list(…)✓ routed to /customsearch/v1/siterestrict
Invalid num=11✓ raises the usual HttpError 400 with Google's message
Quota exhausted✓ HttpError 429 RESOURCE_EXHAUSTED, so existing retry and backoff code keeps working

What is different

AreaDetail
Ranking≠ Results come from your provider's index, not Google's Custom Search ranking. Same shape, different order.
pagemap≠ Not returned yet. Guard reads with item.get("pagemap", {}) (Google omitted it for many results too).
searchType="image"≠ Not supported yet. It returns a clear 400 instead of wrong data.
Console features≠ Refinements, promotions and synonyms configured in the Programmable Search console do not carry over.

Not using the client library?

If your code calls the REST endpoint directly with requests, the migration is only the hostname:

search.py
res = requests.get(-   "https://www.googleapis.com/customsearch/v1",+   "https://cse.yourdomain.workers.dev/customsearch/v1",    params={"key": KEY, "cx": CX, "q": query},).json()

Try it before you change anything

Run a real query against the public demo endpoint and look at the JSON your code will receive.

Open the live demo View source on GitHub