Migration guide · Python

Move Python code off the Google Custom Search JSON API

Python code reaches the Custom Search JSON API through requests or through Google's own client, google-api-python-client. Either way the move to Searlo's drop-in is the host and the key: one changed URL with requests, one added argument to build() with the client. Your parsing, paging and error handling stay as they are, ahead of Google's January 1, 2027 cut-off.

The change

With requests, the URL and the key. With google-api-python-client, client_options={"api_endpoint": …}: the client keeps Google's paths (/customsearch/v1 and /customsearch/v1/siterestrict), which Searlo serves.

- API_URL = "https://www.googleapis.com/customsearch/v1"
- API_KEY = os.environ["GOOGLE_API_KEY"]
+ API_URL = "https://api.searlo.tech/customsearch/v1"
+ API_KEY = os.environ["SEARLO_API_KEY"]
- service = build("customsearch", "v1", developerKey=GOOGLE_API_KEY)
+ service = build("customsearch", "v1", developerKey=SEARLO_API_KEY,
+                 client_options={"api_endpoint": "https://api.searlo.tech"})

Switch in four steps

  1. 01Create a Searlo API keyIn the Searlo dashboard. Store it as SEARLO_API_KEY in your environment or secret manager.
  2. 02Change the hostrequests: replace https://www.googleapis.com/customsearch/v1 with https://api.searlo.tech/customsearch/v1. google-api-python-client: add client_options={"api_endpoint": "https://api.searlo.tech"} to build("customsearch", "v1", …).
  3. 03Swap the keyPass the Searlo key as key (requests) or developerKey (the client). Keep cx: it is accepted and has no effect.
  4. 04Restrict to your sites, if your engine didAdd siteSearch=example.com to the call. Searlo searches the whole web and cannot read the site list behind a cx.

With requests

A complete script: search, read items, follow queries.nextPage to the second page. Errors arrive in Google's envelope, so the same check works.

search.py

import os

import requests

API_URL = "https://api.searlo.tech/customsearch/v1"  # was https://www.googleapis.com/customsearch/v1
API_KEY = os.environ["SEARLO_API_KEY"]                 # was your Google API key


def search(query, start=1, num=10, **params):
    resp = requests.get(
        API_URL,
        params={"key": API_KEY, "cx": "YOUR_CX", "q": query, "num": num, "start": start, **params},
        timeout=30,
    )
    data = resp.json()
    if "error" in data:  # Google's error envelope, unchanged
        err = data["error"]
        raise RuntimeError(f"{err['code']} {err['errors'][0]['reason']}: {err['message']}")
    return data


# The first two pages, following queries.nextPage as you did on Google.
data = search("lectures")
for item in data.get("items", []):
    print(item["title"], item["link"])

next_page = data["queries"].get("nextPage")
if next_page:
    data = search("lectures", start=next_page[0]["startIndex"])
    for item in data.get("items", []):
        print(item["title"], item["link"])

With google-api-python-client

build() reads the customsearch v1 discovery document bundled with the library (static discovery is the default in version 2), so creating the client makes no call to Google. api_endpoint replaces the host it builds URLs on; developerKey goes out as the key parameter, as before.

search_client.py

import os

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

service = build(
    "customsearch",
    "v1",
    developerKey=os.environ["SEARLO_API_KEY"],                   # was your Google API key
    client_options={"api_endpoint": "https://api.searlo.tech"},  # the one new argument
)

try:
    res = service.cse().list(q="lectures", cx="YOUR_CX", num=10, start=1).execute()
except HttpError as err:
    # Errors keep Google's envelope, so status_code and reason (the message) read as before.
    print(err.status_code, err.reason)
    raise

for item in res.get("items", []):
    print(item["title"], item["link"])
print(res["searchInformation"]["totalResults"])

# The siterestrict resource works too; restrict with siteSearch, since cx has no effect.
res = service.cse().siterestrict().list(q="lectures", siteSearch="ocw.mit.edu").execute()
for item in res.get("items", []):
    print(item["title"], item["link"])

Notes

Each call is 1 credit, whatever num (1 to 10) asks for, so a loop that follows nextPage to the 100th result makes ten calls. A call that returns an error is not charged.

HttpError keeps working: status_code is the HTTP status and the body is Google's error envelope. Out of credits is 403 with reason dailyLimitExceeded, which clears when you top up rather than at midnight; a rate limit is 429 with a Retry-After header.

Parameters Searlo accepts with no effect, cx among them, are listed in the X-Searlo-Ignored-Params response header. A parameter it cannot honour (linkSite, rights, sort=date, a city location) returns a 400 that names it.

We ran both scripts on 2026-10-25 with Python 3.12, requests 2.34 and google-api-python-client 2.201.0.

FAQ

Questions

Does google-api-python-client work with a different host?

Yes. build() takes client_options, and when client_options.api_endpoint is set the client uses it as the base URL for every method. For customsearch v1 the paths are customsearch/v1 and customsearch/v1/siterestrict, both of which Searlo serves at https://api.searlo.tech.

Can I keep the response parsing code?

Yes. The response is Custom Search JSON: items with title, link, snippet, displayLink and the html fields, searchInformation, and queries with request and nextPage. cacheId, labels, spelling and most of pagemap are not returned, so code that requires them needs a fallback.

What about LangChain's GoogleSearchAPIWrapper?

It uses google-api-python-client internally. Replace its search_engine with a client built as above; the LangChain guide has the code.

Run your script on Searlo

Create a key, change two lines, run it.