# Migrate from Google Custom Search

> Google shuts down the Custom Search JSON API on January 1, 2027. SerpKite's GET /customsearch/v1 takes the same parameters and returns the same response shape, so you change the host and the key and keep your parser.

Google is retiring the Custom Search JSON API (`https://www.googleapis.com/customsearch/v1`) on **January 1, 2027**. SerpKite ships a compatible endpoint, [`GET /customsearch/v1`](https://serpkite.com/docs/endpoints/customsearch), that accepts the same query parameters and returns the same `customsearch#search` document. Code that reads `items[].title`, `items[].link`, `searchInformation.totalResults` or `queries.nextPage` keeps working.

## The change

```diff
- https://www.googleapis.com/customsearch/v1?key=GOOGLE_KEY&cx=ENGINE_ID&q=asyncio
+ https://api.serpkite.com/customsearch/v1?key=skt_live_...&cx=ENGINE_ID&q=asyncio
```

1. Replace the host `www.googleapis.com` (or `customsearch.googleapis.com`) with `api.serpkite.com`. The path stays `/customsearch/v1`.
2. Replace the Google API key with a SerpKite key. You can keep passing it as `key=`, or move it to an `Authorization: Bearer` header so it stays out of URLs and logs.
3. Leave `cx` as it is. It is accepted and ignored.

Paste an existing request URL into the [CSE migration helper](https://serpkite.com/tools/cse-migration) to get the rewritten URL and code.

## Parameters

| Parameter | Supported | Notes |
| --- | --- | --- |
| `q` | Yes | Required. |
| `key` | Yes | Your SerpKite key. `Authorization: Bearer` also works. |
| `cx` | Accepted, ignored | Results come from the whole web, not a Programmable Search Engine. |
| `start` | Yes | 1–91, in steps of 10 (1, 11, 21…). |
| `num` | Yes | 1–10. |
| `gl` | Yes | Country code. |
| `hl` | Yes | Interface language. |
| `lr` | Yes | Language restrict, e.g. `lang_de`. |
| `safe` | Yes | `active` or `off`. |
| `dateRestrict` | Yes | `d7`, `w2`, `m6`, `y1`… |
| `siteSearch` | Yes | Restrict to one site. |
| `searchType` | Yes | `image` for image results. |

> **If your engine was restricted to certain sites**
> A Programmable Search Engine could be limited to a list of sites. SerpKite ignores `cx`, so add the restriction to the request: `siteSearch=docs.example.com`, or a `site:` operator in `q` (`site:docs.example.com OR site:blog.example.com asyncio`).

## Response

The response keeps the CSE shape: `kind`, `searchInformation`, `items[]` (with `title`, `htmlTitle`, `link`, `displayLink`, `snippet`, `htmlSnippet`, `formattedUrl`, `pagemap`, `image` for image search) and `queries` with `request` and `nextPage`.

GET /customsearch/v1 · illustrative:

```json
{
  "kind": "customsearch#search",
  "searchInformation": {
    "searchTime": 0.94,
    "formattedSearchTime": "0.94",
    "totalResults": "1240000",
    "formattedTotalResults": "1,240,000"
  },
  "items": [
    {
      "kind": "customsearch#result",
      "title": "asyncio — Asynchronous I/O",
      "htmlTitle": "<b>asyncio</b> — Asynchronous I/O",
      "link": "https://docs.python.org/3/library/asyncio.html",
      "displayLink": "docs.python.org",
      "snippet": "asyncio is a library to write concurrent code using the async/await syntax."
    }
  ],
  "queries": {
    "request": [
      {
        "searchTerms": "site:docs.python.org asyncio",
        "count": 10,
        "startIndex": 1
      }
    ]
  }
}
```

Two differences to handle:

- **Errors** use SerpKite's shape, `{"error":{"code","message","request_id"}}`, not Google's `{"error":{"code":403,"errors":[…]}}`. Google put the HTTP status in `error.code`; SerpKite puts a string such as `unauthorized` or `insufficient_credits` there. See [Errors](https://serpkite.com/docs/errors).
- **Quota.** There is no 100-queries-per-day free cap and no 10,000-per-day ceiling. You pay 1 credit per call from a prepaid balance, and your plan sets a requests-per-second [rate limit](https://serpkite.com/docs/rate-limits).

## Code

### Python with requests

```python
import os
import requests

res = requests.get(
    "https://api.serpkite.com/customsearch/v1",
    params={"q": "site:docs.python.org asyncio", "cx": "any", "num": 10, "start": 1},
    headers={"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"},
    timeout=30,
)
res.raise_for_status()
data = res.json()
print(data["searchInformation"]["totalResults"])
for item in data.get("items", []):
    print(item["title"], item["link"])
```

### Python with google-api-python-client

If you use Google's client library, point it at SerpKite with `client_options`. The library builds URLs from the discovery document's root, so overriding the endpoint sends requests to `https://api.serpkite.com/customsearch/v1`:

```python
import os
from googleapiclient.discovery import build

service = build(
    "customsearch",
    "v1",
    developerKey=os.environ["SERPKITE_API_KEY"],
    client_options={"api_endpoint": "https://api.serpkite.com/"},
)
data = service.cse().list(q="site:docs.python.org asyncio", cx="any").execute()
for item in data.get("items", []):
    print(item["title"], item["link"])
```

The discovery document itself is still fetched from Google, so this path depends on Google's discovery service staying up after the shutdown. For a long-lived migration, prefer the plain `requests` version above.

### Node.js

```js
const url = new URL("https://api.serpkite.com/customsearch/v1");
url.search = new URLSearchParams({ q: "site:docs.python.org asyncio", cx: "any", num: "10" });

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.SERPKITE_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error.message);
const { items = [], searchInformation } = await res.json();
console.log(searchInformation.totalResults, items.map((i) => i.link));
```

### Go

```go
q := url.Values{}
q.Set("q", "site:docs.python.org asyncio")
q.Set("cx", "any")
q.Set("num", "10")

req, _ := http.NewRequest("GET", "https://api.serpkite.com/customsearch/v1?"+q.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SERPKITE_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

var out struct {
	Items []struct {
		Title string `json:"title"`
		Link  string `json:"link"`
	} `json:"items"`
}
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
	log.Fatal(err)
}
for _, it := range out.Items {
	fmt.Println(it.Title, it.Link)
}
```

## Pagination

As with CSE, page with `start`: `start=1` is results 1–10, `start=11` is 11–20, up to `start=91`. Each call costs 1 credit. If you need the top 100 in one go, the native [`/v1/search`](https://serpkite.com/docs/endpoints/search) endpoint with `num: 100` returns them in one call for 7 credits instead of 10 (see [Pagination and depth](https://serpkite.com/docs/pagination-and-depth)).

## Going further than CSE

Once you're on SerpKite, the native endpoints give you more than CSE ever returned: People Also Ask, the knowledge graph, the answer box, news, maps and more, plus [Markdown output](https://serpkite.com/docs/output-formats) for LLMs. The mapping from CSE is straightforward: `items[].link` is `results[].link`, `items[].title` is `results[].title`, `items[].snippet` is `results[].snippet`, `start` becomes `page`, and the CSE `gl` and `hl` parameters become `country` and `language`. With the [official SDKs](https://serpkite.com/docs/sdks):

cURL:

```bash
curl https://api.serpkite.com/v1/search \
  -H "Authorization: Bearer $SERPKITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q":"site:docs.python.org asyncio","country":"us","language":"en"}'
```

TypeScript:

```ts
import { SerpKite } from "serpkite";

const sk = new SerpKite(); // reads SERPKITE_API_KEY
const res = await sk.search({ q: "site:docs.python.org asyncio", country: "us", language: "en" });
console.log(res.results[0].title, res.meta.credits_used);
```

Python:

```python
from serpkite import SerpKite

sk = SerpKite()  # reads SERPKITE_API_KEY
res = sk.search("site:docs.python.org asyncio", country="us", language="en")
print(res.results[0].title, res.meta.credits_used)
```

Go:

```go
package main

import (
	"context"
	"fmt"
	"log"

	serpkite "github.com/serpkite/serpkite-go"
)

func main() {
	ctx := context.Background()
	c := serpkite.NewClient() // reads SERPKITE_API_KEY
	res, err := c.Search(ctx, serpkite.SearchParams{Q: "site:docs.python.org asyncio", Country: "us", Language: "en"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed)
}
```

## Related

- [CSE shutdown and alternatives](https://serpkite.com/migrate/google-custom-search-api): What is changing, the timeline and your options.
- [CSE migration helper](https://serpkite.com/tools/cse-migration): Paste a CSE URL, get the SerpKite equivalent.
- [Custom Search reference](https://serpkite.com/docs/endpoints/customsearch): Every parameter and response field.
- [Authentication](https://serpkite.com/docs/authentication): Header and query-string keys.