Migration guide · PHP
Move PHP code off the Google Custom Search JSON API
PHP code calls the Custom Search JSON API with cURL (or file_get_contents, Guzzle and friends) or with Google's google/apiclient. Either way the move to Searlo's drop-in is the host and the key: one changed URL, or one extra constructor argument. Responses and errors keep their shape, ahead of Google's January 1, 2027 cut-off.
The change
With a plain HTTP call, the URL and the key. With google/apiclient, pass the root URL as the second argument to Google\Service\CustomSearchAPI: the service keeps Google's paths, which Searlo serves.
- $apiUrl = 'https://www.googleapis.com/customsearch/v1';
- $apiKey = getenv('GOOGLE_API_KEY');
+ $apiUrl = 'https://api.searlo.tech/customsearch/v1';
+ $apiKey = getenv('SEARLO_API_KEY');- $client->setDeveloperKey($googleApiKey);
- $service = new Google\Service\CustomSearchAPI($client);
+ $client->setDeveloperKey($searloApiKey);
+ $service = new Google\Service\CustomSearchAPI($client, 'https://api.searlo.tech/');Switch in four steps
- 01Create a Searlo API keyIn the Searlo dashboard. Store it as SEARLO_API_KEY in your environment.
- 02Change the hostPlain HTTP: replace https://www.googleapis.com/customsearch/v1 with https://api.searlo.tech/customsearch/v1. google/apiclient: new Google\Service\CustomSearchAPI($client, 'https://api.searlo.tech/').
- 03Swap the keyPass the Searlo key as key, or with $client->setDeveloperKey(). Keep cx: it is accepted and has no effect.
- 04Restrict to your sites, if your engine didAdd 'siteSearch' => 'example.com' to the parameters. Searlo searches the whole web and cannot read the site list behind a cx.
With cURL
A complete script with the curl extension. An error status throws with Google's reason and message, read from the same envelope as before.
search.php
<?php
$apiUrl = 'https://api.searlo.tech/customsearch/v1'; // was https://www.googleapis.com/customsearch/v1
$apiKey = getenv('SEARLO_API_KEY'); // was your Google API key
function cse_search(string $apiUrl, array $params): array
{
$ch = curl_init($apiUrl . '?' . http_build_query($params));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode((string) $body, true);
if ($status !== 200) {
// Google's error envelope, unchanged
$reason = $data['error']['errors'][0]['reason'] ?? 'error';
throw new RuntimeException("$status $reason: " . ($data['error']['message'] ?? 'request failed'));
}
return $data;
}
$data = cse_search($apiUrl, ['key' => $apiKey, 'cx' => 'YOUR_CX', 'q' => 'lectures', 'num' => 10]);
foreach ($data['items'] ?? [] as $item) {
echo $item['title'], ' ', $item['link'], PHP_EOL;
}
// queries.nextPage[0].startIndex is the start of the next page, as on Google.
echo $data['queries']['nextPage'][0]['startIndex'] ?? 'no more results', PHP_EOL;
With google/apiclient
The generated service class takes the root URL as its second constructor argument; listCse and listCseSiterestrict both use it. Google\Service\Exception keeps working, with getErrors() returning Google's errors array.
search-client.php
<?php
require __DIR__ . '/vendor/autoload.php'; // composer require google/apiclient
$client = new Google\Client();
$client->setDeveloperKey(getenv('SEARLO_API_KEY')); // was your Google API key
$service = new Google\Service\CustomSearchAPI($client, 'https://api.searlo.tech/'); // the one new argument
try {
$results = $service->cse->listCse(['cx' => 'YOUR_CX', 'q' => 'lectures', 'num' => 10]);
foreach ($results->getItems() as $item) {
echo $item->getTitle(), ' ', $item->getLink(), PHP_EOL;
}
echo $results->getSearchInformation()->getTotalResults(), PHP_EOL;
// The siterestrict resource works too; restrict with siteSearch, since cx has no effect.
$site = $service->cse_siterestrict->listCseSiterestrict(['q' => 'lectures', 'siteSearch' => 'ocw.mit.edu']);
echo count($site->getItems()), ' results from ocw.mit.edu', PHP_EOL;
} catch (Google\Service\Exception $e) {
// Errors keep Google's envelope: 400 keyInvalid, 403 dailyLimitExceeded, 429 rateLimitExceeded...
echo $e->getCode(), ' ', $e->getErrors()[0]['reason'] ?? '', PHP_EOL;
exit(1);
}
Notes
Each call is 1 credit, whatever num (1 to 10) asks for. A call that returns an error is not charged.
If you install google/apiclient only for this API, Google's Composer cleanup task can drop the other services: add "google/apiclient-services": ["CustomSearchAPI"] under extra and "pre-autoload-dump": "Google\\Task\\Composer::cleanup" under scripts in composer.json.
On WordPress, plugins usually call the API through the WordPress HTTP API rather than google/apiclient; the WordPress guide redirects those calls without editing the plugin.
We ran both scripts on 2026-10-25 with PHP 8.3.36, google/apiclient 2.20.1 and google/apiclient-services 0.464.0.
FAQ
Questions
Does google/apiclient accept a different host?
Yes. Every generated service in google/apiclient-services takes a root URL as the second constructor argument. For CustomSearchAPI, 'https://api.searlo.tech/' sends cse->listCse() and cse_siterestrict->listCseSiterestrict() to Searlo with Google's paths.
Do I need to change how I read results?
No. getItems(), getSearchInformation() and getQueries() return the same objects, because the response is Custom Search JSON. cacheId, labels, spelling and most of pagemap are not returned.
Run your code on Searlo
Create a key, change the host, run your tests.