How to Scrape Google Search Results in PHP in 2026 (Why cURL Alone Fails)

By Serpent API Team · · 11 min read

You searched for "scrape Google search results PHP", found a tutorial, copied a chunk of cURL code, ran it… and got an empty page, a CAPTCHA, or a 429 error.

You are not doing anything wrong. The tutorial is simply out of date.

Almost every PHP guide on this topic teaches the same thing: fire curl at google.com/search, set an old User-Agent, and read the HTML back with a regex or DOM parser. That approach quietly stopped working, and most articles never got the memo.

This guide explains why raw cURL fails in 2026, then shows you the way that actually works from PHP — with two complete, copy-paste examples (native cURL and Guzzle) plus WordPress and Laravel notes.

TL;DR: Google results are JavaScript-rendered and aggressively bot-protected, so plain PHP cURL gets a blocked or empty HTML shell. The reliable fix is to call a SERP API over normal HTTP from PHP and decode the JSON — no headless Chrome, no proxy pool to manage. Both a cURL and a Guzzle example are below.

What everyone tries (and why it breaks)

Almost every "scrape Google in PHP" tutorial gives you a version of this snippet. It looks reasonable. It is also broken.

<?php
// The classic tutorial approach — DO NOT rely on this in 2026
$query = urlencode('best running shoes');
$url   = "https://www.google.com/search?q={$query}";

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT,
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
$html = curl_exec($ch);
curl_close($ch);

// People then try to regex the titles out of $html...
preg_match_all('/<h3[^>]*>(.*?)<\/h3>/', $html, $matches);
print_r($matches[1]);
?>

Run it from a real server and you will get one of three things: a near-empty page, a "Our systems have detected unusual traffic" CAPTCHA page, or an HTTP 429 after a handful of calls.

The preg_match_all at the end finds nothing, because the titles it is looking for were never in the HTML to begin with.

Why raw cURL fails in 2026

Plain cURL fails because it does only one job — download raw HTML — and Google's search page now needs much more than that. Three things break it.

1. The results are JavaScript-rendered. The modern Google results page ships a thin HTML shell and then builds the actual result list with JavaScript in the browser. cURL fetches that initial shell before any script runs, so the organic results simply are not in the bytes you receive.

2. Bot detection spots cURL instantly. Google inspects TLS fingerprints, header order, and behavior. A bare cURL request looks nothing like a real Chrome session, and its default fingerprint is a dead giveaway. You get served a challenge page instead of results.

3. One server IP gets blocked fast. All your requests leave from a single datacenter IP. Send a few in a row and Google flags the address, then every later call returns a CAPTCHA or a 429. Sending automated queries to Google also violates Google's spam policies and Terms of Service.

The class names people scrape (.g, .tF2Cxc, .VwiC3b) are also obfuscated and change often, so even a parser that does get HTML breaks within weeks. This is the same fragility we cover in why your SERP scraper breaks at 3am.

What you needRaw PHP cURLSERP API over HTTP
Runs the page's JavaScriptNoHandled for you
Survives bot detectionNoHandled for you
Rotating exit IPsYou build itHandled for you
Parses HTML into fieldsYou write fragile regexClean JSON returned
Breaks when Google redesignsYes, oftenNo, stable contract
Works on shared / WP hostingYes, but uselessYes, fully

The reliable way: a SERP API over HTTP

The reliable way to scrape Google from PHP is to not scrape Google from PHP at all — call a SERP API over plain HTTP and let it return structured JSON.

This matters more in PHP than in any other language. Python and Node devs can spin up Playwright or Puppeteer to drive a real browser. But PHP usually runs on shared hosting, inside WordPress, or in a Laravel app where launching headless Chrome per request is slow, fragile, and often outright blocked by the host.

A SERP API removes that whole problem. Your PHP code makes one normal GET request. There is no proxy pool or headless browser to manage — access is handled for you — and you get back the same clean JSON every time.

Here is the endpoint we will use throughout, from Serpent's Google SERP API:

GET https://api.apiserpent.com/api/search?q=best+running+shoes&engine=google&country=us
Header: X-API-Key: sk_live_your_key

The response is JSON with a top-level results.organic array. That array is what you loop over — no DOM parsing, no regex, no broken selectors. If you want the wider picture of API vs. DIY, see Google Search API vs scraping.

Working example 1: native PHP cURL

You do not need to install anything for this one — cURL ships with virtually every PHP build. The trick is that you point cURL at the SERP API, not at Google.

<?php
// scrape-google.php — native cURL, no dependencies
$apiKey = 'sk_live_your_key';          // keep this in an env var, not in code
$query  = 'best running shoes';

$params = http_build_query([
    'q'       => $query,
    'engine'  => 'google',
    'country' => 'us',
    'language'=> 'en',
    'num'     => 20,                    // 10–100, rounds up to nearest 10
]);

$ch = curl_init("https://api.apiserpent.com/api/search?{$params}");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: ' . $apiKey,
        'Accept: application/json',
    ],
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err    = curl_error($ch);
curl_close($ch);

if ($err) {
    exit("Request failed: {$err}\n");
}
if ($status !== 200) {
    exit("API returned HTTP {$status}\n");
}

$data = json_decode($body, true);
if (empty($data['success'])) {
    exit("API error: " . ($data['error'] ?? 'unknown') . "\n");
}

// Loop the organic results — clean fields, no HTML parsing
foreach ($data['results']['organic'] as $result) {
    printf("%2d. %s\n    %s\n    %s\n\n",
        $result['position'],
        $result['title'],
        $result['url'],
        $result['snippet'] ?? ''
    );
}
?>

Run it with php scrape-google.php and you get a clean, numbered list of real Google results — position, title, URL, and snippet — every time. No CAPTCHA, no 429, no regex.

Notice that the messy part of the old approach is gone. You never touch HTML. You decode JSON and read named keys.

Want to test before you write code? Open the live playground, run a Google query in the browser, and copy the exact JSON shape your PHP will receive. Grab a key on the sign-up page — 10 free Google searches, no card required.

Working example 2: Guzzle

If your project already uses Composer — and most Laravel and modern PHP apps do — Guzzle gives you a cleaner, more testable client. Install it with composer require guzzlehttp/guzzle.

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'base_uri' => 'https://api.apiserpent.com',
    'timeout'  => 30,
    'headers'  => [
        'X-API-Key' => getenv('SERPENT_API_KEY'),
        'Accept'    => 'application/json',
    ],
]);

try {
    $response = $client->get('/api/search', [
        'query' => [
            'q'       => 'best running shoes',
            'engine'  => 'google',
            'country' => 'us',
            'num'     => 20,
        ],
    ]);

    $data = json_decode($response->getBody()->getContents(), true);

    foreach ($data['results']['organic'] as $r) {
        echo "{$r['position']}. {$r['title']} — {$r['url']}\n";
    }

    // Bonus: People Also Ask, if present on this SERP
    foreach ($data['results']['peopleAlsoAsk'] ?? [] as $paa) {
        echo "Q: {$paa['question']}\n";
    }

} catch (RequestException $e) {
    // Network or 4xx/5xx — log it, do not crash the request
    error_log('Serpent request failed: ' . $e->getMessage());
}
?>

Guzzle builds the query string for you, throws clean exceptions on failure, and is trivial to mock in tests. It is the version we would ship in production.

Both examples hit the same endpoint and get the same JSON. cURL is zero-dependency; Guzzle is nicer to maintain. Pick whichever fits your stack.

Search parameters you can pass

Every example above used a handful of parameters — here is the full set you can send to /api/search.

ParameterExampleWhat it does
qbest running shoesThe search query (required)
enginegooglegoogle (default), bing, yahoo, or ddg
countryusGeo-localize results to a country
languageenInterface / results language
num10010–100 results, rounds up to nearest 10
pages3How many SERP pages to scrape (1–10)
formatfullfull (default) or simple, a leaner payload

One thing worth highlighting: asking for 100 results costs the same as asking for 10. Page depth does not multiply the price. This is a big deal since Google stopped supporting the num=100 shortcut, which made deep result collection far more expensive for everyone scraping directly. With a single API call you still get the full top 100 — see how to fix rank trackers after the num=100 removal.

The full JSON also includes ads, peopleAlsoAsk, relatedSearches, featuredSnippet, aiOverview, localPack, and inline videos on Google — all in the same response. The complete schema lives in the API docs.

WordPress & Laravel patterns

In a real PHP app you should never call the API on every page load — wrap it in your framework's HTTP client and cache the result. Here is the idiomatic way in each ecosystem.

WordPress. Use the built-in wp_remote_get() instead of cURL, and cache with a transient so you do not re-fetch on every request:

<?php
function serpent_google_search($query) {
    $cache_key = 'serpent_' . md5($query);
    $cached    = get_transient($cache_key);
    if ($cached !== false) {
        return $cached; // served from cache, no API call
    }

    $url = add_query_arg([
        'q'      => $query,
        'engine' => 'google',
        'country'=> 'us',
    ], 'https://api.apiserpent.com/api/search');

    $res = wp_remote_get($url, [
        'timeout' => 30,
        'headers' => ['X-API-Key' => SERPENT_API_KEY],
    ]);

    if (is_wp_error($res)) {
        return [];
    }

    $data = json_decode(wp_remote_retrieve_body($res), true);
    $organic = $data['results']['organic'] ?? [];

    set_transient($cache_key, $organic, HOUR_IN_SECONDS); // cache 1 hour
    return $organic;
}
?>

Laravel. Use the Http facade and Cache::remember(). Keep the key in config/services.php backed by an env var:

<?php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

function googleResults(string $q): array
{
    return Cache::remember("serp:{$q}", now()->addHour(), function () use ($q) {
        $res = Http::withHeaders([
                'X-API-Key' => config('services.serpent.key'),
            ])
            ->timeout(30)
            ->get('https://api.apiserpent.com/api/search', [
                'q'      => $q,
                'engine' => 'google',
                'country'=> 'us',
            ]);

        return $res->json('results.organic', []);
    });
}
?>

Both wrap the call in a one-hour cache, so a popular keyword hits the API once an hour instead of once per visitor. For a deeper look at caching strategy across requests, read build a SERP cache to cut your API bill.

Error handling & caching tips

Treat the API like any external service: always check the status, decode defensively, and cache aggressively. Three habits keep PHP apps stable.

Check before you trust. The response includes a top-level success boolean. Confirm it is true and that results.organic exists before looping. Use the null-coalescing operator (??) on optional fields like snippet and featuredSnippet, which may be absent on a given SERP.

Set a timeout. Every example above sets a 30-second timeout. A deep 100-result search takes longer than a quick one, so do not leave the default short timeout that some HTTP clients ship with.

Cache by query. Search results do not change second to second. Caching each query for 30–60 minutes (as shown above) slashes both latency and cost. If you only need about ten results and want the fastest response, hit /api/search/quick instead of the deep endpoint.

If you are comparing providers first, our tested round-up of free Google search APIs and what a SERP API actually is are good starting points before you commit.

Scrape Google from PHP without the headaches

Serpent returns up to 100 clean Google results in one HTTP call — no proxy pool, no headless Chrome, no broken selectors. Start with 10 free Google searches, then pay a flat rate from $0.60 down to $0.03 per 10,000 searches. No subscription.

Get Your Free API Key

Explore: Google SERP API · Docs · Pricing

FAQ

Can I scrape Google search results with PHP cURL?

Not reliably anymore. Google's results are JavaScript-rendered and protected by bot detection, so a raw cURL request returns a blocked or empty HTML shell. The dependable approach is to call a SERP API over plain HTTP from PHP, which returns clean JSON you can decode directly.

Do I need a headless browser to scrape Google in PHP?

No. Running headless Chrome from PHP is heavy and brittle, especially on shared or WordPress hosting. A SERP API does the rendering for you, so your PHP code only makes a normal HTTP GET request and parses JSON. No proxy pool or headless browser to manage.

How do I loop through organic results in PHP?

Decode the JSON response with json_decode($body, true), then iterate over $data['results']['organic']. Each item is an array with position, title, url, displayedUrl and snippet keys you can read directly inside a foreach loop.

Will this work inside WordPress or Laravel?

Yes. Because it is a plain HTTP call, it works anywhere PHP runs. In WordPress use wp_remote_get() and cache with transients; in Laravel use the Http facade and Cache::remember(). Both avoid extra processes or system dependencies.

How much does it cost to scrape Google with a PHP SERP API?

Serpent API charges a flat rate from $0.60 per 10,000 Google searches, dropping to $0.03 per 10,000 at scale. Page depth does not multiply the price, there is no subscription, and you get 10 free Google searches on signup.

Is scraping Google search results legal?

Sending automated queries directly to Google violates its Terms of Service. Using a SERP API moves that responsibility off your server, so your PHP app simply consumes a structured data feed instead of hammering Google directly. Always review the terms that apply to your use case.