> Searlo API Docs — Custom Search JSON API (drop-in)
> Source: https://searlo.tech/docs#custom-search-json-api
> Full docs: https://searlo.tech/docs/full.md

# 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=lectures
```

Step-by-step guides, with code we ran before publishing: [Python](https://searlo.tech/google-custom-search-api-migration/python), [Node.js](https://searlo.tech/google-custom-search-api-migration/node), [PHP](https://searlo.tech/google-custom-search-api-migration/php), [LangChain](https://searlo.tech/google-custom-search-api-migration/langchain), [Open WebUI](https://searlo.tech/google-custom-search-api-migration/open-webui), [n8n](https://searlo.tech/google-custom-search-api-migration/n8n) and [WordPress](https://searlo.tech/google-custom-search-api-migration/wordpress). Google's dates and the full parameter table are on the [migration page](https://searlo.tech/google-custom-search-api-migration).

### Endpoint

```
GET https://api.searlo.tech/customsearch/v1
GET https://api.searlo.tech/customsearch/v1/siterestrict
GET https://api.searlo.tech/api/v1/customsearch/v1
```

All 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](#search-web) bills one: 1 credit per call, whatever `num` (1 to 10) and `start` are. A call with `searchType=image` is billed as [Image Search](#search-images): 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:

```json
{
  "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, `items` is left out, as Google does. `queries.nextPage` appears only when more results exist.
- Image results add `mime`, `fileFormat` and `image`. The `image` object holds `contextLink`, plus `width`, `height`, `thumbnailLink`, `thumbnailWidth` and `thumbnailHeight` when the image source reports them. `byteSize` is not returned.
- `pagemap` appears only when a result has a thumbnail (`pagemap.cse_thumbnail`). There is no `cacheId`, `labels`, `promotions` or `spelling`.
- 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:

```json
{
  "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](#rate-limiting), 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

- `cx` has no effect: results come from the whole web, not from an engine's site list. `/siterestrict` works the same way.
- Results come from Searlo's web search, so ranking, snippets and the `totalResults` estimate can differ from your Google engine.
- Location is country level only, through `gl` or `cr`. Boolean `cr` and `lr` expressions are not supported.
- Running out of credits is a `403 dailyLimitExceeded` that 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.

## Code Examples

### cURL

```bash
# Before: Google
curl "https://www.googleapis.com/customsearch/v1?key=$GOOGLE_API_KEY&cx=$CX&q=lectures&num=10"

# After: Searlo. Only the host and the key change
curl "https://api.searlo.tech/customsearch/v1?key=$SEARLO_API_KEY&cx=$CX&q=lectures&num=10"

# The same call with the key in a header
curl "https://api.searlo.tech/customsearch/v1?q=lectures&num=10" \
  -H "x-api-key: $SEARLO_API_KEY"

# Image search
curl "https://api.searlo.tech/customsearch/v1?key=$SEARLO_API_KEY&q=red+panda&searchType=image&imgSize=large"
```

### Python

```python
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

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 None
```

### JavaScript

```javascript
const API_URL = "https://api.searlo.tech/customsearch/v1"; // was https://www.googleapis.com/customsearch/v1
const 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 more
```


---
*From https://searlo.tech/docs — see full docs at https://searlo.tech/docs/full.md*