Migration guide
Replace the Google
Custom Search JSON API.
Google is shutting down the Custom Search JSON API on January 1, 2027. This guide maps each request parameter and response field to the PacketStream Search API, shows the code before and after, and lists what has no equivalent.
What Google says
Google’s Custom Search JSON API overview page, checked on October 28, 2026, says:
- The API is not available to new customers.
- Existing customers can use it until the service is discontinued on January 1, 2027.
- Paid queries cost $5 per 1,000, up to 10,000 queries per day.
After that date, code that calls customsearch/v1 needs a new search source.
How the PacketStream Search API differs
- It searches all of Google. Custom Search queries a Programmable Search Engine that you configure, identified by
cx. PacketStream runs a live Google search from the country you pick. There is no engine to configure. - $2.50 per 1,000 successful searches. Each search costs $0.0025 from a prepaid balance. Failed searches are free. There is no subscription; the minimum purchase is $50.
- More than web results. Besides web results, you get news, shopping, places, autocomplete, Google Scholar and Google AI Mode, plus the AI overview, related questions and related searches when Google shows them.
- JSON or Markdown. Markdown output is ready for a language-model prompt.
- Rate limit: 20 requests per second per key, with bursts of 200.
Request parameters
Custom Search takes query parameters on a GET. PacketStream accepts the same fields as a JSON body on POST /v1/search or as query parameters on GET /v1/search. Send the key in an Authorization: Bearer header, not in the URL.
| Custom Search | PacketStream | Notes |
|---|---|---|
key | Authorization: Bearer | One PacketStream key also works for the Fetch API and the MCP server. |
q | query | 1 to 400 bytes. Google operators such as site: work inside it. |
gl | country | Two-letter code. PacketStream searches from that country; the default is us. |
hl | language | Google interface language, such as en or pt-BR. |
start | page | page is 1 to 10, with up to 10 web results per page. start=1 is page: 1, start=11 is page: 2, and so on. Other start values have no exact equivalent. |
num | none | You cannot set the page size. A web search returns up to 10 results. |
safe | safe_search | active is true; off is false, the default. |
dateRestrict | time_range | hour, day, week, month or year. d1, w1, m1 and y1 map directly. Other numbers, such as d3, have no equivalent. |
siteSearch | site: in query | Write site:example.com to include a site, or -site:example.com to exclude it (siteSearchFilter). |
exactTerms, excludeTerms, fileType | operators in query | Use Google’s own syntax: "exact phrase", -word, filetype:pdf. |
googlehost | google_domain | Such as google.co.uk. |
cx | none | There is no custom engine. If your engine was limited to a few sites, add site: operators to the query. |
searchType=image | none | Image search is not available. |
lr, cr | none | There is no filter for document language or document country. Use language and country to search as a user in that market would. |
sort, filter, rights, linkSite, lowRange, highRange, c2coff, img* | none | No equivalent. |
PacketStream also accepts location (such as Austin,Texas,United States), uule, device, autocorrect and type, which Custom Search does not have. An unknown field returns 400 invalid_request instead of being ignored, so remove Custom Search parameter names when you switch. See all parameters.
Response fields
| Custom Search | PacketStream | Notes |
|---|---|---|
items | results | Each result also has a 1-based position. The list can be empty. |
items[].title | results[].title | Plain text. |
items[].link | results[].url | Always an absolute http or https URL, never a tracking redirect. |
items[].snippet | results[]. | Plain text. |
items[]. | host of results[].url | There is no separate host field. Read it from the URL. |
items[]. | results[]. | The URL as Google shows it, such as https://www.example.com › reviews. Present only when available. |
items[]., htmlSnippet, pagemap | none | Results are plain text. To read a page, use the Fetch API. |
searchInformation. | search_information. | A web search returns it when Google shows it. |
spelling. | search_information. | When Google corrected the query. |
queries.nextPage | none | Send the same request with page plus one. |
error | error.code, error.message | Handle errors by code. See errors and limits. |
Python, before and after
Before, with Custom Search:
import os
import requests
response = requests.get(
"https://customsearch.googleapis.com/customsearch/v1",
params={
"key": os.environ["GOOGLE_API_KEY"],
"cx": os.environ["GOOGLE_CSE_ID"],
"q": "coffee grinders",
"gl": "us",
"hl": "en",
"start": 11,
},
timeout=30,
)
response.raise_for_status()
for item in response.json().get("items", []):
print(item["title"], item["link"])After, with PacketStream:
import os
import requests
response = requests.post(
"https://api.packetstream.io/v1/search",
headers={"Authorization": f"Bearer {os.environ['PACKETSTREAM_API_KEY']}"},
json={
"query": "coffee grinders",
"country": "us",
"language": "en",
"page": 2,
},
timeout=35,
)
response.raise_for_status()
for result in response.json()["results"]:
print(result["title"], result["url"])The server finishes or gives up within 25 seconds, so use a client timeout of at least 35 seconds.
JavaScript, after
Run this on a server, never in a browser, so the key stays private. The API sends no CORS headers.
const response = await fetch("https://api.packetstream.io/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PACKETSTREAM_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "coffee grinders", country: "us", language: "en", page: 2 }),
signal: AbortSignal.timeout(35_000),
});
const payload = await response.json();
if (!response.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
for (const result of payload.results) console.log(result.title, result.url);Switch in four steps
- Create a PacketStream account and add funds. The minimum purchase is $50.
- Create an API key on the Search API page of the dashboard. It is shown once, so copy it when it appears.
- Change the endpoint, move the key into the
Authorizationheader, and rename the fields using the tables above. - Read
resultsinstead ofitems, andurlinstead oflink.
Run both side by side before January 1, 2027 to compare results on your own queries. GET /v1/account shows your balance and usage for free.
Search API pricing
$2.50 per 1,000 successful searches.
Failed searches are free, and every type and option costs the same. Searches use the same prepaid balance as PacketStream proxies.