cse-compat / guides / python
Google Custom Search shutdown: migrating google-api-python-client
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
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:
- serper.dev: Google results, 2,500 free searches to start, then prepaid credits.
- Brave Search API: an independent index, about 1,000 free searches a month, then $5 per 1,000.
2. Deploy your endpoint (free Cloudflare tier)
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:
| Behavior | Result |
|---|---|
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
| Area | Detail |
|---|---|
| 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:
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