Back to BlogGuide

Google Custom Search JSON API Shutdown (January 1, 2027): Step-by-Step Migration Guide

September 29, 202615 min read

Google will discontinue the Custom Search JSON API on January 1, 2027, and it is already closed to new customers. To migrate, change the endpoint and the key, translate each parameter and response field, test on your real queries side by side, then move traffic behind a flag. This guide has the mapping tables, before-and-after code in Python and Node, and a rollout checklist.

What Google has announced, in its own words

Everything in this section is quoted from Google's own pages, read on 2026-09-29.

The Custom Search JSON API overview (last updated 2026-02-18) carries this note:

"The Custom Search JSON API is closed to new customers. Vertex AI Search is a favorable alternative for searching up to 50 domains. Alternatively, if your use case necessitates full web search, contact us to express your interest in and get more information about our full web search solution. Existing Custom Search JSON API customers have until January 1, 2027 to transition to an alternative solution."

Its pricing section names the date as the end of the service itself:

"The following pricing applies only to existing Custom Search JSON API customers until the service discontinuation on January 1, 2027. This API is not available for new customers. Custom Search JSON API provides 100 search queries per day for free. If you need more, you may sign up for billing in the API Console. Additional requests cost $5 per 1000 queries, up to 10k queries per day."

The announcement is a Programmable Search Engine blog post dated Tuesday, January 20, 2026. Its paragraph for API users:

"For users of the Custom Search JSON API: Vertex AI Search is a favorable alternative for up to 50 domains. Alternatively, if your use case necessitates full web search, contact us to express your interest in and get more information about our full web search solution. Your transition to an alternative solution needs to be completed by January 1, 2027."

And the change that took effect the same day:

"To prepare for this transition, as of today, all new engines must be configured to use the “Sites to search” feature. This change impacts only new engines; existing engines are not affected and can continue to use the “Search the entire web” option until January 1, 2027."

The help centre page Update sites in your search engine repeats the January 1, 2027 date for existing "Search the entire web" engines. It also carries one warning worth reading before you touch the control panel:

"Note: Once toggled to Off, this can not be toggled back to On."

One older product is easy to confuse with this one. The Custom Search Site Restricted JSON API, the siterestrict endpoint, has its own notice: "The Custom Search Site Restricted JSON API endpoints will cease to serve traffic on January 8, 2025" (page last updated 2026-09-03). This guide is about the main endpoint, customsearch/v1.

DateWhat Google's pages saySource
January 8, 2025The separate Site Restricted JSON API endpoints "will cease to serve traffic"Site Restricted JSON API, updated 2026-09-03
January 20, 2026New engines must use "Sites to search" (up to 50 domains). Existing "Search the entire web" engines continue until January 1, 2027Programmable Search Engine blog
In force on 2026-09-29The Custom Search JSON API "is closed to new customers"Overview, updated 2026-02-18
January 1, 2027"the service discontinuation"; existing customers must have moved to "an alternative solution"Overview

What Google has not said

Those pages leave gaps. We have not filled them with guesses:

• The exact cut-off. No page gives a time of day or a time zone for January 1, 2027, or says what a request will return after it: an error, an empty response or something else.

• Whether the date can move. None of the pages mentions an extension.

• What the "full web search solution" is and costs. Google publishes no price, limits or API documentation for it. It asks you to register interest through a form linked from the overview page.

• Whether Vertex AI Search is a drop-in replacement. Google calls Vertex AI Search "a favorable alternative for searching up to 50 domains". None of the pages describes it as the same API, so plan for a new integration whichever way you go.

Who has to migrate

Any code that calls https://www.googleapis.com/customsearch/v1, directly or through a Google client library. The overview speaks of "the service discontinuation", with no exception for engines that search only your own sites, and the blog post points JSON API users with 50 domains or fewer to Vertex AI Search. If what you run is the search box embedded on your own site, with 50 or fewer domains in "Sites to search", Google's blog says the Search Element "remains the optimal solution". This guide is for code that calls the API.

If you need results from across the web rather than from a list of sites, the rest of this guide moves you to Searlo, an independent API for Google search results. For the product comparison itself, see Google Custom Search API alternative and Searlo vs Google Search API. If you are here because a new project cannot get a key at all, Custom Search JSON API closed to new customers covers that case. This page is the how-to.

Step 1: Inventory every call

Search your code for customsearch. That catches direct REST calls and Google's client libraries. For each call site, write down:

• the parameters it sends, including hard-coded ones such as safe, lr or siteSearch;

• the response fields it reads: items[].title, link and snippet, and whether it also uses pagemap, searchInformation.totalResults, queries.nextPage or spelling;

• how it pages through results with start, and how it handles an empty result and a quota error.

Then export a sample of real queries from your logs, a few hundred is enough, and include the filtered ones (site, file type, date, exact terms). You need it in step 6.

Step 2: Map the request parameters

Searlo's base URL is https://api.searlo.tech/api/v1 and the key goes in an x-api-key header. There is no search engine ID. The Custom Search column uses Google's own parameter reference, cse.list (read 2026-09-29). The Searlo column was checked against the request validation in our API code on the same day.

Custom Search JSON APISearloWhat to know
key in the query stringx-api-key headerKeeps the key out of URLs and logs
cxNo equivalentNo engine to create. /search/web searches the open web; for one site, use /search/site
qqUp to 500 characters on /search/web, 400 on /serp. Sent to Google unchanged, operators included
num (1 to 10)limit (1 to 10)Same range, different name
start (index of the first result)page (page number, from 1)page = (start - 1) // limit + 1, so start=11 is page=2
glglTwo-letter country code
hlhlInterface language
lr (for example lang_de)lr on /search/webAccepted, but it sets the same language as hl: the lang_ prefix is dropped, and hl wins if you send both. There is no separate filter on the language a document is written in
safe (active or off)safe on /search/webSame values. On /serp it is a boolean: safe=true
siteSearch with siteSearchFilter=isite on GET /search/siteOne domain or URL per call
siteSearch with siteSearchFilter=e-site:example.com in qA minus in front of the site: operator
searchType=imageGET /search/imagesResults arrive in images[], not organic[]
exactTermsThe phrase in double quotes in qGoogle's exact-match operator
excludeTermsA minus before the word in qGoogle's exclude operator
fileTypefileType on GET /search/advancedAdds filetype: to the query
dateRestrict (d7, w2, m6, y1)tbs=qdr:d7 and so on, on GET /serpThe full results page, 3 credits per page instead of 1
sort, rights, cr, filter, orTerms, linkSite, lowRange, highRange, hq, c2coff, the img* filtersNo equivalentIgnored if sent, so delete them

The trap: unknown parameters are ignored, not rejected

Searlo drops parameters it does not recognise instead of returning an error. That is convenient until you migrate. A leftover num=5 still returns 10 results. A leftover start=11 returns page 1 again, so a pagination loop fetches the same ten results on every page. A leftover dateRestrict or siteSearch silently searches everything. Rename every parameter, delete the ones with no equivalent, and test each filter in step 6.

Site, file type, date and exact-term filters in practice

• One site. GET /search/site?q=...&site=example.com. site takes a domain or a URL (example.com, https://example.com/docs), not a control-panel pattern such as *.example.com/*. The endpoint takes q, site, limit and page; it has no gl or hl.

• Several sites, as in a "Sites to search" list: one /search/site call per site, merged on your side, at 1 credit each.

• Exact phrase, excluded word, excluded site. Quotes, a minus sign, and a minus in front of site:, all inside q. Quotes, the minus sign and site: are documented on Google's Refine Google searches help page (read 2026-09-29), and Searlo passes q through as you wrote it.

• File type. GET /search/advanced?q=...&fileType=pdf adds filetype:pdf to the query. For this job /search/advanced takes q, limit and fileType; it has no page, gl or hl. If you need those, write filetype:pdf into q on /search/web instead.

• Date restriction. GET /serp with tbs: dateRestrict=d7 becomes tbs=qdr:d7, m6 becomes qdr:m6. For dates it accepts qdr: followed by h, d, w, m or y and an optional number; other date values, such as a custom range, are ignored. /serp returns the full rendered results page at 3 credits per page instead of 1, and also takes q, gl, hl, num and pages; see the Google SERP API. Google's help page also documents after: and before: with a date inside q. If you would rather stay on the 1-credit endpoint, test those on your own queries first.

Step 3: Swap the client

The Searlo version keeps the function's name, its arguments and its return shape, so nothing that calls it has to change. Python first, before and after:

python
# cse_client.py: before, on the Custom Search JSON API import os import requests def search(query, page=1, per_page=10): resp = requests.get( "https://www.googleapis.com/customsearch/v1", params={ "key": os.environ["GOOGLE_API_KEY"], "cx": os.environ["GOOGLE_CSE_ID"], "q": query, "num": per_page, "start": (page - 1) * per_page + 1, "gl": "us", "hl": "en", }, timeout=30, ) resp.raise_for_status() data = resp.json() return [ {"title": i["title"], "url": i["link"], "snippet": i.get("snippet", "")} for i in data.get("items", []) ]
python
# searlo_client.py: after, on Searlo (same function, same return shape) import os import requests def search(query, page=1, per_page=10): resp = requests.get( "https://api.searlo.tech/api/v1/search/web", params={"q": query, "limit": per_page, "page": page, "gl": "us", "hl": "en"}, headers={"x-api-key": os.environ["SEARLO_API_KEY"]}, timeout=30, ) resp.raise_for_status() data = resp.json() return [ {"title": r["title"], "url": r["link"], "snippet": r.get("snippet", "")} for r in data.get("organic", []) ]

And Node 18 or later, which has fetch built in:

javascript
// cse-client.mjs: before, on the Custom Search JSON API export async function search(query, page = 1, perPage = 10) { const params = new URLSearchParams({ key: process.env.GOOGLE_API_KEY, cx: process.env.GOOGLE_CSE_ID, q: query, num: String(perPage), start: String((page - 1) * perPage + 1), gl: "us", hl: "en", }); const res = await fetch(`https://www.googleapis.com/customsearch/v1?${params}`, { signal: AbortSignal.timeout(30_000), }); if (!res.ok) throw new Error(`Custom Search returned ${res.status}`); const data = await res.json(); return (data.items ?? []).map((i) => ({ title: i.title, url: i.link, snippet: i.snippet ?? "" })); }
javascript
// searlo-client.mjs: after, on Searlo (same function, same return shape) export async function search(query, page = 1, perPage = 10) { const params = new URLSearchParams({ q: query, limit: String(perPage), page: String(page), gl: "us", hl: "en", }); const res = await fetch(`https://api.searlo.tech/api/v1/search/web?${params}`, { headers: { "x-api-key": process.env.SEARLO_API_KEY }, signal: AbortSignal.timeout(30_000), }); if (!res.ok) throw new Error(`Searlo returned ${res.status}`); const data = await res.json(); return (data.organic ?? []).map((r) => ({ title: r.title, url: r.link, snippet: r.snippet ?? "" })); }

Both versions throw on any error status. Before the Searlo one goes to production, add three rules. On a 429, or a 503 that carries a Retry-After header, wait that long and retry. On a 402 the account is out of credits: raise an alert instead of retrying. On any other 5xx, check the retryable flag in the body and back off briefly before a retry. A call that fails is not billed, so a retry never pays twice for the same failure. Successful responses carry X-Credits-Deducted and X-Credits-Remaining headers; log them during the rollout to watch spend.

Step 4: Map the response fields

The Custom Search field names come from Google's response reference (read 2026-09-29). The Searlo names are the ones our response formatters emit for /search/web.

Custom Search JSON APISearloWhat to know
items[]organic[]On a successful response, always an array, and empty when nothing matched
items[].titleorganic[].titlePlain text; there is no htmlTitle
items[].linkorganic[].linkThe result's URL
items[].snippetorganic[].snippetPlain text; there is no htmlSnippet
items[].displayLinkorganic[].displayedLinkNote the extra "ed"
Not in Custom Searchorganic[].domainThe hostname without www.
Not in Custom Searchorganic[].positionThe rank, continuing across pages: with 10 per page, page 2 starts at 11
Not in Custom Searchorganic[].faviconA favicon image URL for the result's domain
items[].pagemapdate, thumbnail, description when presentNot on every result or every response, so treat them as optional
searchInformation.totalResults (a string)totalResults (a number) when presentAn estimate, and not on every response
searchInformation.searchTimesearchTime when presentNot on every response
queries.nextPagenextPageThe next page number, or null
queries.request[0].startIndexpageA page number, not a result index
queries.request[0].searchTermssearchParameters.qsearchParameters also echoes gl, hl, num and page
spelling.correctedQueryNo guaranteed equivalentDo not depend on it
kind, url, context, promotions, cacheId, formattedUrl, labelsNo equivalentRemove them from your parser

For image search the results come back in images[]. items[].link becomes images[].imageUrl and items[].image.thumbnailLink becomes images[].thumbnailUrl, alongside title, source, sourceUrl, width and height. Check sourceUrl and the two dimensions on a live sample before you map them onto image.contextLink, image.width and image.height. The Image Search API page has the full response.

Step 5: Price the move

Google's rate for existing customers is on the overview page quoted above: 100 queries a day free, then "$5 per 1000 queries, up to 10k queries per day". Searlo charges credits: 1 per call on /search/web, /search/images, /search/site and /search/advanced, and 3 per results page on /serp. Credits come in one-time packs with no subscription, and a new account starts with 3,000 free credits. The rates below are from our pricing page:

PackPriceCreditsPer 1,000 web searchesPer 1,000 date-filtered searches on /serp
Micro$3.995,000$0.80$2.40
Starter$9.9920,000$0.50$1.50
Builder$29.9975,000$0.40$1.20
Scale$74.99250,000$0.30$0.90
Pro$199.99900,000$0.22$0.66
Enterprise$7994,000,000$0.20$0.60

Credits on new accounts are valid for 90 days, so compare 90 days of your real volume:

• 1,000 queries a day. Google: 900 billable queries a day for 90 days is 81,000 queries, or $405.00. Searlo: 90,000 credits, which one Builder pack and one Starter pack cover (95,000 credits) for $39.98.

• 10,000 queries a day, Google's ceiling. Google: 9,900 billable queries a day for 90 days is 891,000 queries, or $4,455.00. Searlo: 900,000 credits, exactly one Pro pack, $199.99.

Count any queries that need a date filter at 3 credits. How the two products compare beyond price is covered on Google Custom Search API alternative and Custom Search JSON API pricing.

Step 6: Shadow-test on your real queries

Before any user sees a Searlo result, run both clients on the query sample from step 1 and compare what comes back. The script below writes one row per query: the result count from each API, and how many URLs and how many domains they share. It counts shared domains as well as shared URLs, because small differences in a URL, such as a www. prefix, would otherwise hide a match.

python
# shadow_test.py: run both clients on real queries and compare what comes back import csv from urllib.parse import urlsplit import cse_client import searlo_client def domains(results): return {(urlsplit(r["url"]).hostname or "").removeprefix("www.") for r in results} with open("sample_queries.txt", encoding="utf-8") as f: queries = [q.strip() for q in f if q.strip()] with open("shadow_report.csv", "w", newline="", encoding="utf-8") as out: w = csv.writer(out) w.writerow(["query", "cse_results", "searlo_results", "shared_urls", "shared_domains"]) for q in queries: old, new = cse_client.search(q), searlo_client.search(q) shared_urls = len({r["url"] for r in old} & {r["url"] for r in new}) w.writerow([q, len(old), len(new), shared_urls, len(domains(old) & domains(new))])

Decide your pass mark before you read the numbers, and expect differences. A Custom Search engine can carry its own configuration, such as its site list, promotions and ranking settings, and any Google results page moves with location and time. A 500-query sample costs 500 Searlo credits, inside a new account's free 3,000, plus 500 Custom Search queries at your current rate.

Step 7: Roll out and cut over

1. Map every call site with the two tables above, and delete every Custom Search parameter that has no Searlo equivalent.

2. Test each filter on its own. A filtered query must return a different result set from the same query unfiltered; if it does not, the filter is not being applied.

3. Test paging: page 2 must not repeat page 1.

4. On a 429, or a 503 with Retry-After, wait that long; back off briefly on other 5xx responses; treat 402 (out of credits) as an alert, not a retry.

5. Log X-Credits-Deducted and X-Credits-Remaining from the first request, so spend is visible from day one.

6. Put the switch behind a flag and move a small share of traffic first, keeping Custom Search as the fallback while it still works.

7. Watch the empty-result rate and the error rate at each step, and roll back on the flag if either moves.

8. Move all traffic well before January 1, 2027. Google has not said what the API returns after that date, so do not plan a last-day cutover.

9. Then remove the Google API key and the engine ID from your configuration. Do not switch off "Search the entire web" on an engine you still depend on: Google says it cannot be switched back on.

Related pages

• Google Custom Search API alternative: how Searlo compares with the Custom Search JSON API.

• Custom Search JSON API closed to new customers: for projects that cannot get a key.

• Searlo vs Google Search API: the side-by-side comparison.

• Web Search API: the /search/web endpoint this guide migrates to.

• API reference and docs: every endpoint and parameter.

• Pricing: every pack and its rate per 1,000 credits.

Google's pages quoted here were read on 2026-09-29: the overview, the January 20, 2026 announcement, the help centre page, the Site Restricted JSON API page, the cse.list reference, the response reference and Refine Google searches. If one of them changes, tell us and we will update this guide.

Searlo is an independent service and is not affiliated with or endorsed by Google. Google is a trademark of Google LLC.

Frequently Asked Questions

When does the Google Custom Search JSON API shut down?

Google's overview page describes "the service discontinuation on January 1, 2027" and says existing customers "have until January 1, 2027 to transition to an alternative solution" (read 2026-09-29). Google gives no time of day and does not say what a request will return after that date, so plan to finish the move well before it.

Can new customers still sign up for the Custom Search JSON API?

No. Google's overview page says "The Custom Search JSON API is closed to new customers." Since January 20, 2026, every new Programmable Search Engine must also use "Sites to search", which covers up to 50 domains, according to Google's announcement of that date.

Does the shutdown affect engines that only search my own sites?

The API itself is being discontinued: the overview makes no exception for engines restricted to a site list, and Google points Custom Search JSON API users with 50 domains or fewer to Vertex AI Search. The search box embedded on your own site, with 50 or fewer domains, is different: Google's January 2026 blog post says the Search Element "remains the optimal solution" for that.

What does Google recommend instead of the Custom Search JSON API?

Vertex AI Search, which Google calls "a favorable alternative for searching up to 50 domains". For full web search it asks you to register interest in a "full web search solution", for which it publishes no price, limits or documentation. For results from across the web through a plain REST API, Searlo's /search/web takes a query and an x-api-key header, with no engine to configure.

How do the start and num parameters map to Searlo?

num becomes limit, with the same range of 1 to 10. start, the index of the first result, becomes page, a page number: page = (start - 1) // limit + 1, so start=11 with 10 results per page is page=2. Rename both. Searlo ignores parameters it does not recognise, so a leftover start would quietly return page 1 again.

Do I need a search engine ID (cx) with Searlo?

No. There is no engine to create. /search/web searches the open web, /search/site restricts a query to one domain or URL per call, and the API key goes in an x-api-key header instead of the query string.

How much will my Custom Search traffic cost on Searlo?

One credit per /search/web call, which is $0.20 to $0.80 per 1,000 queries depending on the pack, against Google's $5 per 1,000 after 100 free queries a day. Over 90 days, 1,000 queries a day costs $405.00 on Google's published rate and $39.98 in Searlo packs. Queries that need a date filter go through /serp at 3 credits each. See pricing for every pack.

Ready to try Searlo?

Get 3,000 free credits. No credit card required.

Get Free API Key