Search
Custom Search JSON API (drop-in)
Google's Custom Search JSON API is closed to new customers, and existing customers lose it on January 1, 2027. Searlo answers the same request in the same JSON shape, so you move by changing the host and the key:
- https://www.googleapis.com/customsearch/v1?key=GOOGLE_API_KEY&cx=YOUR_CX&q=lectures+ https://api.searlo.tech/customsearch/v1?key=SEARLO_API_KEY&cx=YOUR_CX&q=lecturesStep-by-step guides, with code we ran before publishing: Python, Node.js, PHP, LangChain, Open WebUI, n8n and WordPress. Google's dates and the full parameter table are on the migration page.
Endpoint#
GET https://api.searlo.tech/customsearch/v1GET https://api.searlo.tech/customsearch/v1/siterestrictGET https://api.searlo.tech/api/v1/customsearch/v1All three paths run the same search. /customsearch/v1 is Google's own path, so a client that lets you set the API host needs no other change.
Authentication: pass your Searlo API key (sk_…) in the key parameter, where Google takes its key. The x-api-key header and Authorization: Bearer sk_… also work, and keep the key out of URLs. Searlo strips key from its own logs.
Credits: a call is billed exactly as Web Search bills one: 1 credit per call, whatever num (1 to 10) and start are. A call with searchType=image is billed as Image Search: 1 credit. A call that returns an error is not charged.
Parameters#
| Parameter | Status | Notes |
|---|---|---|
q | Supported | Required. Up to 500 characters |
key | Supported | Your Searlo API key |
cx | Accepted, no effect | An engine's site list lives in your Google console, which Searlo cannot read, so Searlo searches the whole web. To restrict results, use siteSearch or site: in q |
num | Supported | 1 to 10 (default 10) |
start | Supported | 1-based. As on Google, the last result can be at most the 100th: start=91&num=10 is the last full page |
gl | Supported | Country, two letters |
cr | Supported | One country, as countryXX. Read as gl |
hl | Supported | Interface language |
lr | Supported | One language, as lang_xx. Read as the request language |
safe | Supported | active or off |
dateRestrict | Supported | d[n], w[n], m[n] or y[n]. Combining it with operator parameters (siteSearch, fileType, orTerms…) or with image search returns 400 |
siteSearch, siteSearchFilter | Supported | site: (i, the default) or -site: (e) |
exactTerms, excludeTerms, orTerms, fileType, hq, lowRange, highRange | Supported | Applied as the search operators Google applies |
searchType | Supported | image for image search |
imgSize, imgType, imgColorType, imgDominantColor | Supported | Image filters, with Google's values |
fields, prettyPrint, alt=json | Supported | Partial responses and pretty-printed JSON |
googlehost, quotaUser, userIp | Accepted, no effect | Listed in the X-Searlo-Ignored-Params header, with cx |
filter, c2coff, sort | Default only | filter=1, c2coff=0 and an empty sort are accepted. Any other value returns 400 |
linkSite, relatedSite, rights, callback | Not supported | Returns 400 with the reason |
location, uule and other city or point locations | Not supported | Country level only (gl, cr). Returns 400 |
| Any other parameter | Not supported | Returns 400, as Google does for an unknown parameter |
Response#
The response has Google's customsearch#search shape:
{ "kind": "customsearch#search", "url": { "type": "application/json", "template": "https://api.searlo.tech/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&…&alt=json" }, "queries": { "request": [ { "title": "Searlo Custom Search - lectures", "totalResults": "2410000", "searchTerms": "lectures", "count": 10, "startIndex": 1, "inputEncoding": "utf8", "outputEncoding": "utf8", "safe": "off", "cx": "YOUR_CX" } ], "nextPage": [{ "startIndex": 11, "count": 10, "…": "the same fields" }] }, "context": { "title": "Searlo" }, "searchInformation": { "searchTime": 0.21, "formattedSearchTime": "0.21", "totalResults": "2410000", "formattedTotalResults": "2,410,000" }, "items": [ { "kind": "customsearch#result", "title": "Lectures | MIT OpenCourseWare", "htmlTitle": "<b>Lectures</b> | MIT OpenCourseWare", "link": "https://ocw.mit.edu/…", "displayLink": "ocw.mit.edu", "snippet": "Video lectures from …", "htmlSnippet": "Video <b>lectures</b> from …", "formattedUrl": "https://ocw.mit.edu/…", "htmlFormattedUrl": "https://ocw.mit.edu/…" } ]}- When nothing matches,
itemsis left out, as Google does.queries.nextPageappears only when more results exist. - Image results add
mime,fileFormatandimage. Theimageobject holdscontextLink, pluswidth,height,thumbnailLink,thumbnailWidthandthumbnailHeightwhen the image source reports them.byteSizeis not returned. pagemapappears only when a result has a thumbnail (pagemap.cse_thumbnail). There is nocacheId,labels,promotionsorspelling.- Every response carries
X-Request-ID. Quote it when you contact support.
Errors#
Errors use Google's envelope, so code that reads error.code, error.errors[0].reason or error.status keeps working:
{ "error": { "code": 400, "message": "Request contains an invalid argument.", "errors": [ { "message": "Request contains an invalid argument.", "domain": "global", "reason": "badRequest", "location": "num", "locationType": "parameter" } ], "status": "INVALID_ARGUMENT", "details": [{ "@type": "type.googleapis.com/google.rpc.RequestInfo", "requestId": "…" }] }}| Status | reason | When |
|---|---|---|
400 | badRequest, invalid, required | A parameter is missing or invalid |
400 | keyInvalid | The key is not a valid Searlo API key (this includes a Google AIza… key) |
400 | unsupportedParameter, cityLocationUnavailable | A parameter Searlo does not take (see above) |
403 | forbidden | No key, or the account is suspended or has not confirmed its email |
403 | dailyLimitExceeded | The account is out of credits. The message links to the top-up |
429 | rateLimitExceeded | The rate limit, or too many searches in flight. Wait for Retry-After |
503 | backendError | Search is temporarily unavailable. Wait for Retry-After |
500 | backendError | Internal error |
None of these is charged.
What differs from Google#
cxhas no effect: results come from the whole web, not from an engine's site list./siterestrictworks the same way.- Results come from Searlo's web search, so ranking, snippets and the
totalResultsestimate can differ from your Google engine. - Location is country level only, through
glorcr. Booleancrandlrexpressions are not supported. - Running out of credits is a
403 dailyLimitExceededthat clears when you top up, not at midnight. - Call the API from a server. Browsers on other sites are blocked by CORS, and JSONP (
callback) is not supported.
Examples
# Before: Googlecurl "https://www.googleapis.com/customsearch/v1?key=$GOOGLE_API_KEY&cx=$CX&q=lectures&num=10" # After: Searlo. Only the host and the key changecurl "https://api.searlo.tech/customsearch/v1?key=$SEARLO_API_KEY&cx=$CX&q=lectures&num=10" # The same call with the key in a headercurl "https://api.searlo.tech/customsearch/v1?q=lectures&num=10" \ -H "x-api-key: $SEARLO_API_KEY" # Image searchcurl "https://api.searlo.tech/customsearch/v1?key=$SEARLO_API_KEY&q=red+panda&searchType=image&imgSize=large"import osimport requests API_URL = "https://api.searlo.tech/customsearch/v1" # was https://www.googleapis.com/customsearch/v1API_KEY = os.environ["SEARLO_API_KEY"] # was your Google API key params = {"key": API_KEY, "cx": "YOUR_CX", "q": "lectures", "num": 10, "start": 1}while params: data = requests.get(API_URL, params=params, timeout=30).json() if "error" in data: raise RuntimeError(data["error"]["message"]) for item in data.get("items", []): print(item["title"], item["link"]) next_page = data["queries"].get("nextPage") params = {**params, "start": next_page[0]["startIndex"]} if next_page else Noneconst API_URL = "https://api.searlo.tech/customsearch/v1"; // was https://www.googleapis.com/customsearch/v1const API_KEY = process.env.SEARLO_API_KEY; // was your Google API key const params = new URLSearchParams({ key: API_KEY, cx: "YOUR_CX", q: "lectures", num: "10" });const res = await fetch(`${API_URL}?${params}`);const data = await res.json();if (!res.ok) throw new Error(data.error.message); for (const item of data.items ?? []) console.log(item.title, item.link);const next = data.queries.nextPage?.[0]; // { startIndex: 11, count: 10, ... } when there is moreStuck? Write to [email protected], or check live status.